xanoutas/solidity-security-findings huggingface dataset:
[M9] Chained lzcompose calls can be denied by using up a single permit.
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


# Vulnerability details

## Impact

The crosschain TAP token supports lzcompose messages. Thus users can create multiple transactions that are chained together, and they will be called one by one at the destination chain via lzcompose functions. The multiple call functionality is highlighted in the following lines in the base omnichain receiver contract.

```solidity
(uint16 msgType_,,, bytes memory tapComposeMsg_, bytes memory nextMsg_) =
    TapiocaOmnichainEngineCodec.decodeToeComposeMsg(oftComposeMsg_);
if (nextMsg_.length > 0) {
    _lzCompose(srcChainSender_, _guid, nextMsg_);
}
```

Thus users can send a long list of interactions, and the contract will execute them one after the other by decoding a single operation, running it, and then calling the same function with the `nextMsg_` parameter, which is essentially the list of transactions with the first one removed.

The issue is that if any of these transactions fail, the entire chain of transactions will fail.

One of the various transactions the user can call via this method is the permit function. Users can call signature based permit functions on yieldbox, markets, erc20, erc721 and pearlmit contracts. The main point to note here is that these require the user to reveal their signatures on the base chain since the v,r and s as well as the call digest is part of the lzcompose message sent to the endpoint.

On almost every permit based function, there is a `nonce` which is incremented whenever a signature is used up, to prevent replay attacks. So a malicious user can look at these signatures on the base chain, and then use them up on the target chain. This would result in the lzcompose transaction failing, since one of the signatures will be already used up and the nonce will not match anymore.

This is a recognized issue in any protocol which supports signature based permit functions. Anyone can use up broadcasted signatures, since the sender sends a valid signature with the transaction, which will therefore cause the original transaction to fail. Pure permit calls are not affected by this, since the outcome is the same either way: the approval gets updated. However, chained operations like here are affected since while the approval chain is correctly reflected, the rest of the operations fail.

This is generally deemed as a medium severity issue as described in the blog post [here](https://www.trust-security.xyz/post/permission-denied).

## Proof of Concept

Since the signatures are available on chain, users can use them up on the target chain. Due to the layerzero system having inherent delays, its even easier than normal and does not require any frontrunning either. Just monitor the transactions to the endpoint on chain A, and use up the signature on chain B. The endpoint will wait until block confirmati
[M7] TWAML weights can be griefed by burning tokens
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/oTAP.sol#L99-L104


# Vulnerability details

## Impact

Users can lock their liquidity in the TOLP contract and mint OTAP tokens in the TOb contract. The TOB contract has a special mechanism called TWAML to balance out how much rewards they emit over time.

Basically, if a user commits OTAP tokens worth more than a minimum amount of shares, they are eligible to sway the votes.

```solidity
bool hasVotingPower =
    lock.ybShares >= computeMinWeight(pool.totalDeposited + VIRTUAL_TOTAL_AMOUNT, MIN_WEIGHT_FACTOR);
```

This allows them to influence the magnitude, divergence as well as the TWAML value of this asset id.

```solidity
pool.averageMagnitude = (pool.averageMagnitude + magnitude) / pool.totalParticipants; // compute new average magnitude
    // Compute and save new cumulative
    divergenceForce = lock.lockDuration >= pool.cumulative;
    if (divergenceForce) {
        pool.cumulative += pool.averageMagnitude;
    } else {
        if (pool.cumulative > pool.averageMagnitude) {
            pool.cumulative -= pool.averageMagnitude;
        } else {
            pool.cumulative = 0;
        }
    }

    // Save new weight
    pool.totalDeposited += lock.ybShares;

    twAML[lock.sglAssetID] = pool
```

These values determine how large of a discount the users can get when exercising their options.

Similarly, when users decide to exit their position, or if their lock has expired, either they themselves or other users can kick them out of the TOB system and reset the twAML values to the values it was before.

```solidity
if (!isSGLInRescueMode && participation.hasVotingPower) {
    TWAMLPool memory pool = twAML[lock.sglAssetID];

    if (participation.divergenceForce) {
    //...
```

So the twAML change a single user can cause is limited to their lock duration.

However, users also have another option: they can directly burn their OTAP token after participating. This is because the OTAP contract has an open burn function.

```solidity
function burn(uint256 _tokenId) external {
    if (!_isApprovedOrOwner(msg.sender, _tokenId)) revert NotAuthorized();
    _burn(_tokenId);

    emit Burn(msg.sender, _tokenId, options[_tokenId]);
}
```

Now, these user's contributions to the twAML calculations cannot be wiped out after their lock expires. This is because the `exitPosition` function calls `otap.burn` which will fail since the user has already burnt their tokens.

So users can affect the twAML calculations for an infinite amount of time by burning tokens. This scenario is specifically prevented in the TOLP contract which has a max lock duration enforced with `MAX_LOCK_DURATION`.

Since users can permanently affect the twAML calculations, this is a medium severity issue.

## Proof of Concept

Users can directly burn tokens due to OTAP's open burn function as evident from the linked code. This prevents `exitPosition` being called i
[M6] A single second in an epoch makes an user eligible for the entire epoch's rewards
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionBroker.sol#L527-L534


# Vulnerability details

## Impact

A user can lock their LP tokens into the TOLP contract, and then put their TOLP into the TOB contract to be eligible for option rewards. These option rewards are paid out every epoch and distributed proportionally to all the participants in that epoch. When locking LP tokens in `TOLP`, the user can choose their own `_lockDuration`.

The TOB contract has 2 important functions. One is the `exitPosition`, which allows users to take out their TOLP token, and the other is the `exerciseOption` function, which allows the users to collect the option rewards. The issue is that the TOLP contract records lock duration in seconds, while the TOB contract processes them in epochs.

So if a user creates a lock position where the lock end time is a single second after the epoch change, the epoch when the lock is due to expire in the TOB contract will be calculated as shown.

```solidity
uint128 lockExpiry = lock.lockTime + lock.lockDuration;
uint256 lastEpoch = _timestampToWeek(lockExpiry);
```

So if a user lock expires a single second into an epoch, the `lastEpoch` will be calculated as that epoch and the user will be eligible for that entire epoch's rewards.

Moreover, this also allows a peculiar situation where a user can take out their TOLP token and exit their position and then come back and collect their token rewards. This is because when users want to exit their position, the contract checks if the lock has expired.

```solidity
if (!isSGLInRescueMode) {
    if (block.timestamp < lock.lockTime + lock.lockDuration) {
        revert LockNotExpired();
    }
}
```

But when a user wants to exercise their options, the contract checks for the liveness of the position in different terms, the epoch.

```solidity
uint256 expiryWeek = _timestampToWeek(_lock.lockTime + _lock.lockDuration);
isPositionActive = epoch <= expiryWeek;
```

So the first method and the second method can disagree if a position is active. If the expiry time is in the middle of an epoch, for the entire second half of the epoch, the `exitPosition` function will treat the position as expired and will allow the user to take out their TOLP token, but the `exerciseOption` contract will treat the position as active and will allow the user to collect rewards as well. And due to the discussion above, the user can have only a single second of lock time remaining in this second epoch, so for the entire duration of 7 days, the user can have no TOLP locked in the contract but still be eligible to withdraw rewards.

## Proof of Concept

The situation can be created in the following manner.

1. Say the current timestamp is 100. Say the epoch is scheduled to end at 200. Alice creates a lock with the lock duration of 101 seconds.
2. The expiry epoch is calculated as 2, since epoch 1 ends at 200 but the position expir
[M2] Rescue request timestamp not reset in TapiocaOptionLiquidityProvision.sol contract
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionLiquidityProvision.sol#L294-L305


# Vulnerability details

## Impact

In order to put an asset in rescue mode, the admin first needs to call `requestSglPoolRescue`. This will start a cooldown period and the rescue can only be triggered after the cooldown period has passed. This is evident from the code in the `activateSglPoolRescue` function.

```solidity
if (block.timestamp < sglRescueRequest[sgl.sglAssetID] + rescueCooldown) revert RescueCooldownNotReached();
```

The issue is that once the rescue has been carried out, the value in `sglRescueRequest[sgl.sglAssetID]` is not reset. This means if the same asset is added back in, it will now not have any cooldown anymore. The contract will treat the asset as if the rescue request is still ongoing.

The `requestSglPoolRescue` cannot be called to reset the cooldown, since the value stored is non 0. Since this bypasses a protection mechanism put in place by the devs, this is a medium severity issue.

## Proof of Concept

The problem arises when an asset is rescued, and then added back in. `sglRescueRequest[sgl.sglAssetID]` is left untouched, and so after being added back in it can be immediately rescued without any cooldown time.

## Tools Used

Manual Review

## Recommended Mitigation Steps

Delete the entry in the `sglRescueRequest` mapping when the asset is rescued via the `activateSglPoolRescue` function.



## Assessed type

Other
[H6] Incorrect token transfer addresses in option broker contracts
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionBroker.sol#L252-L255
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/option-airdrop/AirdropBroker.sol#L508-L512
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionBroker.sol#L560-L564
https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionLiquidityProvision.sol#L206-L210


# Vulnerability details

## Impact

The `TapiocaOptionBroker` contract's `participate` function allows operations from two different users. One is from the owner of the NFT themselves, in which case it works smoothly, and the other is any approved actor by the owner of the NFT. This allows third parties to operate the NFT tokens on behalf of a user and call `participate` on behalf of them. The access check is done as shown below.

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

So either the owner themselves, or an approved user can operate on the NFT. This is mainly due to the `Magnetar` contracts, which are helper contracts in the system. This means the user only needs to give the Magnetar contract the approval too operate on their behalf, and that should be enough. Crucially, the MAGNETAR contract is **not designed to hold funds**. Any funds or tokens in the magnetar contract can be taken out, and that is by design. So users should never be forced to transfer tokens out to the Magnetar contract before calling a magnetar function.

The issue here is that the `participate` function transfers out the NFT token from `msg.sender`. So it expects the Magnetar contract to own the NFT, which is a thing not allowed in the system.

```solidity
bool isErr = pearlmit.transferFromERC721(msg.sender, address(this), address(tOLP), _tOLPTokenID);
```

So the Magnetar contract is expected to own the ERC721, which is something forbidden in the system, unless specifically handled. This is further bolstered by the code in the Magnetar contract, in `Magnetar.sol`, where the contract actually calls `participate()`.

```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);
    return;
}
```

As seen above, the users can call `participate` via magnetar with 1 single call. This means that the NFT token must be transferred out to the Magnetar contract before this call takes place. This is bad, since once the NFT tokens is transferred out, any user can call this and essentially ste
[H2] Options can be exercised preemptively due to timing delays
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tap-token/blob/20a83b1d2d5577653610a6c3879dff9df4968345/contracts/options/TapiocaOptionBroker.sol#L365-L403


# Vulnerability details

## Impact

When a user participates in the options system via `TapiocaOptionBroker.sol`, they mint an otap position in exchange for their tolp position. The time of creation is recorded, and the user is allowed to exercise the option only after a full epoch duration, which is a week.

```solidity
 if (block.timestamp < oTAPPosition.entry + EPOCH_DURATION) {
        revert OneEpochCooldown();
    }
```

The variable `netDepositedForEpoch` records how much liquidity the system expects to hold in any given epoch, and is used to calculate the rewards for the users. On calling `participate`, this variable is increased for the next epoch, and decreased on the expiration epoch.

```solidity
netDepositedForEpoch[epoch + 1][lock.sglAssetID] += int256(uint256(lock.ybShares));
netDepositedForEpoch[lastEpoch + 1][lock.sglAssetID] -= int256(uint256(lock.ybShares));
```

The issue is this accounting is done via `epoch`, while the cooldown is accounted via `block.timestamp`. So if a user participates at the very beginning of an epoch, their option will be exercisable after 7 days, which is very close to the time when the epoch number can be incremented. If the user is able to call `exerciseOption` before the epoch number is incremented, they can even participate in the current epoch! This is because the `epoch` number has not yet been updated, but the `EPOCH_DURATION` has already passed.

In this scenario, the user gets access to the rewards of the very same epoch they participated in, which should be impossible. They can also cause the contract to run out of rewards, as they are claiming rewards they shouldnt be owed. The contract calculates rewards based on the current net deposit, which hasnt been updated yet since the epoch number hasnt been updated yet.

```solidity
uint256 eligibleTapAmount = muldiv(tOLPLockPosition.ybShares, gaugeTotalForEpoch, netAmount);
eligibleTapAmount -= oTAPCalls[_oTAPTokenID][cachedEpoch];
```

Thus the user can take out rewards based on the wrong `netAmount`, which can lead to other users not getting their rewards, since the `eligibleAmount` is decreased by users not owed any rewards.

## Proof of Concept

The attack is carried out in the following steps. Lets assume the current epoch has JUST started, and is epoch 1. Let's assume the netAmount for this epoch is 100.

1. Alice calls `participate`. The netAmount[2] (next epoch) is increased by her deposit amount of 100.
2. Alice waits 1 week.
3. Alice then calls `exerciseOption`. Lets assume `newEpoch()` hasnt been called yet.
4. Alice passes the `OneEpochCooldown` check. Alice gets assigned rewards. Alice's rewards are = 100 * gaugeTotalForEpoch / netAmount[1], which is = 100*total/100=total.
5. The other depositors who hadnt collected their rewards yet cannot collect hem anymore. This is because th
[H1] Omnichain tokens can be stolen due to bad approval checks.
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/tapiocaOmnichainEngine/BaseTapiocaOmnichainEngine.sol#L60-L74


# Vulnerability details

## Impact

The function `transferFrom` in `BaseTapiocaOmnichainEngine` does a bad approval check which allows any user to steal tap tokens. The Tap token is used as an example here since it inherits the `BaseTapiocaOmnichainEngine` but is applicable for all such tokens.

The `transferFrom` function checks the allowance of the token. If insufficient, it triggers a transfer via the pearlmit permitc.

```solidity
if (allowance(from, spender) < value) {
        // _transfer(from, to, value);
        bool isErr = pearlmit.transferFromERC20(from, to, address(this), value);
        if (isErr) revert BaseTapiocaOmnichainEngine_NotValid();
    }
```

The issue is that if there is no allowance, the `pearlmit.transferFromERC20` function is called with by the tap token. So for the permitC contract, the `msg.sender` is the tap token contract itself. In the `transferFromERC20` function in permitC, the approval of the `msg.sender` is checked.

```solidity
PackedApproval storage approval = _getPackedApprovalPtr(owner, token, id, ZERO_BYTES32, msg.sender);
```

So users can actually abuse the permit given by users to the tap token contract and use that to transfer out tokens to themselves. The omnichain tap token contract is expected to have permitC approvals already, since the crosschain receiver does a bunch of stuff like locking liquidity and participating in airdrops etc.

So if any user gives allowance via the permitc contract so that the tap contract can operate on tokens on their behalf, this allowance can be stolen by any other user.

## Proof of Concept

Can be demonstrated in the following steps:

1. Alice gives approval in permitC to the TAP contract to use her tap tokens.
2. Bob calls `transferFrom` in the TAP contract and tries to send the tokens to himself.
3. Since Bob doesnt have allowance, the `transferFrom` function calls `pearlmit.transferFromERC20` which is called by the TAP contract.
4. The allowance given by Alice in step 1 is used and Bob gets all the tokens.

## Tools Used

Manual Review

## Recommended Mitigation Steps

The allowance of the msg.sender (BOB) in the pearlmit contract should also be checked. However this cannot be reduced since the caller for the transferfromerc20 function is the tap token contract itself.



## Assessed type

Access Control
_internalRemoteTransferSendPacket() can't send the difference back to the user
Audit: 2024-02-tapioca-findings
Severity: MEDIUM

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph/blob/032396f701be935b04a7e5cf3cb40a0136259dbc/contracts/tapiocaOmnichainEngine/TapiocaOmnichainReceiver.sol#L261


# Vulnerability details

## Vulnerability details
in `TapiocaOmnichainReceiver`
when the user executes MSG_REMOTE_TRANSFER
If the srcChain amount request is bigger than the debited one, overwrite the amount to credit with the amount debited and send the difference back to the user.

`lzCompose()` -> `_remoteTransferReceiver()`->`_internalRemoteTransferSendPacket()`
```solidity
    function _internalRemoteTransferSendPacket(
        address _srcChainSender,
        LZSendParam memory _lzSendParam,
        bytes memory _composeMsg
    ) internal returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {
...

        // If the srcChain amount request is bigger than the debited one, overwrite the amount to credit with the amount debited and send the difference back to the user.
        if (_lzSendParam.sendParam.amountLD > amountDebitedLD_) {
            // Overwrite the amount to credit with the amount debited
@>          _lzSendParam.sendParam.amountLD = amountDebitedLD_;
            _lzSendParam.sendParam.minAmountLD = amountDebitedLD_;
            // Send the difference back to the user
@>          _transfer(address(this), _srcChainSender, _lzSendParam.sendParam.amountLD - amountDebitedLD_);
        }
```

The above code, first modify ` _lzSendParam.sendParam.amountLD = amountDebitedLD_`
Then call ` _transfer(address(this), _srcChainSender, _lzSendParam.sendParam.amountLD - amountDebitedLD_);`
This way the difference is always `0

Correctly `transfer()` the difference first, and then modify `_lzSendParam.sendParam.amountLD`


## Impact

the difference token be left in the contract

## Recommended Mitigation
```diff
    function _internalRemoteTransferSendPacket(
        address _srcChainSender,
        LZSendParam memory _lzSendParam,
        bytes memory _composeMsg
    ) internal returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) {
...

        // If the srcChain amount request is bigger than the debited one, overwrite the amount to credit with the amount debited and send the difference back to the user.
        if (_lzSendParam.sendParam.amountLD > amountDebitedLD_) {
+          _transfer(address(this), _srcChainSender, _lzSendParam.sendParam.amountLD - amountDebitedLD_);
            // Overwrite the amount to credit with the amount debited
            _lzSendParam.sendParam.amountLD = amountDebitedLD_;
            _lzSendParam.sendParam.minAmountLD = amountDebitedLD_;
            // Send the difference back to the user
-           _transfer(address(this), _srcChainSender, _lzSendParam.sendParam.amountLD - amountDebitedLD_);
        }
```



## Assessed type

Context