xanoutas/solidity-security-findings huggingface dataset:
Anyone can permissionlessly destroy the Singularity.sol contract
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/singularity/Singularity.sol#L625
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/singularity/Singularity.sol#L92


# Vulnerability details

## Impact

Anyone can permissionlessly destroy the Singularity.sol contract.

## Proof of Concept

In Singularity contrac, the code replies on the [delegatecall to execute the module](https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/singularity/Singularity.sol#L625)

```solidity
   function _executeModule(
        Module _module,
        bytes memory _data
    ) private returns (bytes memory returnData) {
        bool success = true;
        address module = _extractModule(_module);

        (success, returnData) = module.delegatecall(_data);
        if (!success) {
            revert(_getRevertMsg(returnData));
        }
    }
```

this calls _extractModule extract a module contract

```solidity
    function _extractModule(Module _module) private view returns (address) {
        address module;
        if (_module == Module.Borrow) {
            module = address(borrowModule);
        } else if (_module == Module.Collateral) {
            module = address(collateralModule);
        } else if (_module == Module.Liquidation) {
            module = address(liquidationModule);
        } else if (_module == Module.Leverage) {
            module = address(leverageModule);
        }
        if (module == address(0)) {
            revert("SGL: module not set");
        }

        return module;
    }
```

but the issue is, anyone can [frontrun the init function](https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/singularity/Singularity.sol#L92) to set a malicious module contract 

```solidity
liquidationModule = SGLLiquidation(_liquidationModule);
collateralModule = SGLCollateral(_collateralModule);
borrowModule = SGLBorrow(_borrowModule);
leverageModule = SGLLeverage(_leverageModule);
```

and then executes a function and call self-destruct in delegate call to destroy the Singularity.sol

POC:

```solidity
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.0;

import {DSTest} from "ds-test/test.sol";
import {Vm} from "forge-std/Vm.sol";

library console {
    address constant CONSOLE_ADDRESS =
        address(0x000000000000000000636F6e736F6c652e6c6f67);

    function _sendLogPayload(bytes memory payload) private view {
        uint256 payloadLength = payload.length;
        address consoleAddress = CONSOLE_ADDRESS;
        assembly {
            let payloadStart := add(payload, 32)
            let r := staticcall(
                gas(),
                consoleAddress,
                payloadStart,
                payloadLength,
                0,
                0
            )
        }
liquidate() is vulnerable to frontrunning attacks due to the sequential liquidation logic.
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/singularity/Singularity.sol#L440-L459


# Vulnerability details

## Impact
A frontrunner could potentially watch for liquidate() calls, then quickly borrow from or repay loans of the users being liquidated before the external liquidation logic executes. 

## Proof of Concept
liquidate() calls out to an external contract (the liquidation module) using delegatecall. This means the liquidation logic is not contained within the Singularity contract itself. There is no way to make the liquidation atomic or pause the contract to prevent trades during liquidation. The contract relies on an external swapper contract to handle selling collateral/repaying debt. This swapper could be frontrun.There is no check that ensures the state of the users being liquidated has not changed between when liquidate() is called and when the external liquidation logic executes.


## Tools Used 
Manual 

## Recommended Mitigation Steps
Make liquidations atomic, pause trading during liquidation, or check user balances/positions immediately before liquidating. 


## Assessed type

Other
Underflow Vulnerability in getDebtRate() Function May Halt ‘BigBang’ Contract Operations
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L180-L201


# Vulnerability details

## Impact

Within the getDebtRate() function of the contract, a vulnerability exists that could potentially hinder the operation of the contract. This vulnerability arises when _currentDebt falls below debtStartPoint, triggering an overflow in the computation of debtPercentage. As the getDebtRate() function is indirectly called in each important function (through ‘_accrue()’) such as borrow(), repay(), removeCollateral(), and liquidate(), this overflow issue could break the functionality of the contract.


This issue is particularly impactful under the following scenarios:

1) At the beginning of the contract deployment when _currentDebt is low due to lack of borrowing.
2) When significant liquidations occur, reducing _currentDebt drastically.
3) When a user chooses to repay a position that brings the total borrow amount below debtStartPoint. 

Note that the last point is a way for a malicious user to manipulate and break the protocol. 


## Proof of Concept
The proof of concept is embedded in the contract code. In the getDebtRate() function, the calculation for debtPercentage is executed as follows:

     uint256 debtPercentage = ((_currentDebt - debtStartPoint) * DEBT_PRECISION) / (_maxDebtPoint - debtStartPoint);     
                
However, no condition exists that checks whether _currentDebt is greater than or equal to debtStartPoint. If _currentDebt falls below debtStartPoint, this subtraction will trigger an underflow, causing the contract to revert. This prevents the getDebtRate() function from returning a valid "protocol debt rate", which could cause the contract to revert on most of the contract’s function.

## Tools Used
Manual Audit


## Recommended Mitigation Steps
To prevent this issue, a check should be added before the computation of debtPercentage:

     if (debtStartPoint >= _currentDebt) return 0;

This check ensures that if _currentDebt is less than debtStartPoint, the function returns 0, preventing a potential underflow and maintaining the operation of the contract.



## Assessed type

Under/Overflow
Reentrancy  in the ‘flashLoan()’ Function allowing excessive token minting
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/usd0/USDO.sol#L81-L104


# Vulnerability details

## Impact

The ‘flashLoan()’ function inside the USDO contract prevent users from performing a excessively large flash mint through the following condition : 

     require(maxFlashLoan(token) >= amount, "USDO: amount too big");

However, the ‘receiver.onFlashLoan()’ external call observed latter in the function opens the door for a potential reentrancy. Indeed, if the receiver contract calls back the ‘onFlashLoan()’ multiple times with an amount lower or equals to ‘maxFlashLoan’, he will be able to bypassed the condition and borrow an amount way larger than the maximum value defined within the same ‘transaction’ and avoid the rule set by the protocol.

## Proof of Concept
The flashLoan function in the provided Solidity contract allows for flash loans, but contains a reentrancy vulnerability. Here is the related code snippet:

     require(maxFlashLoan(token) >= amount, "USDO: amount too big");                           
     require(amount > 0, "USDO: amount not valid");
     uint256 fee = flashFee(token, amount);
      _mint(address(receiver), amount);

      require(receiver.onFlashLoan(msg.sender, token, amount, fee, data) ==  FLASH_MINT_CALLBACK_SUCCESS, "USDO: failed");

During the execution of flashLoan, the contract verifies if the amount to be borrowed is less than or equal to maxFlashLoan. If it is, the contract mints the amount for the receiver. It then calls onFlashLoan on the receiver, and it's during this call that the vulnerability can be exploited.

If the receiver is a malicious contract, it can reenter the flashLoan function multiple times. Each reentry would check the maxFlashLoan constraint, but the amount for each reentry could be designed such that it is less than maxFlashLoan. This means multiple reentries can occur within a single transaction, each minting tokens for the malicious receiver. In the end, this allows the malicious contract to mint much more tokens than the intended maxFlashLoan limit.


## Tools Used
Manual Audit

## Recommended Mitigation Steps

Use a reentrancy guard for the flashloan() function: 

     bool internal locked;

     modifier noReentrant() {
        require(!locked, "No re-entrancy");
        locked = true;
        _;
        locked = false;
     }


## Assessed type

Reentrancy
Users can infinitely increase their collateral share inside ‘BigBang’ without adding collateral
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L282-L290
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L543-L558
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L691-L706


# Vulnerability details

## Impact
The smart contract contains a flaw that allows users to infinitely increase their individual collateral balance (userCollateralShare) without adding any additional collateral. 
The _addCollateral() function takes as input a 'skim' parameter. Specifically, the function is set up such that:

1) If skim is true, The ‘share’ amount should be subtracted from the ‘from’ account's deposit balance inside the BigBang contract and internally added to the ‘to’ account.

2) if  skim is false, the function will trigger a transfer of ‘share’ in the YieldBox contract from the ‘from’ account to the BigBang address. Subsequently, within the BigBang contract, the balance of the ‘to’ account will be incremented.

Here are the two involved functions: 

    function _addCollateral(
        address from,
        address to,
        bool skim,
        uint256 amount,
        uint256 share
    ) internal {
        if (share == 0) {
            share = yieldBox.toShare(collateralId, amount, false);          
        }
        userCollateralShare[to] += share;                                   
        uint256 oldTotalCollateralShare = totalCollateralShare;
        totalCollateralShare = oldTotalCollateralShare + share;                         
        _addTokens(from, collateralId, share, oldTotalCollateralShare, skim);
        emit LogAddCollateral(skim ? address(yieldBox) : from, to, share);
    }

    function _addTokens(
        address from,
        uint256 _tokenId,
        uint256 share,
        uint256 total,
        bool skim
    ) internal {
        if (skim) {
            require(
                share <= yieldBox.balanceOf(address(this), _tokenId) - total,
                "BigBang: too much"
            );   
        } else {
            yieldBox.transfer(from, address(this), _tokenId, share);
        }
    }

Upon examining the _addCollateral() and _addTokens() functions, it appears that scenario 2 (where skim is false) is properly implemented. Indeed, both userCollateralShare[to] and totalCollateralShare are incremented and a transfer from the from account to the BigBang address is executed within the YieldBox contract.

However, the situation differs when skim is true. Two crucial aspects are not handled correctly. Firstly, there's no reduction in the internal collateral balance of the from account (userCollateralShare[from]). Secondly, the totalCollateralShare is increased, which is inaccurate, given that there is no addition of new collateral to the contract, but merely a transfer
collateral shares could be stolen from users
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L282-L290
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L543-L558
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/MarketERC20.sol#L99-L101
https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/MarketERC20.sol#L84-L91


# Vulnerability details

## Impact

The vulnerability resides in the 'addCollateral()' function of the BigBang contract. Looking at the function definition: 

      function addCollateral(
         address from,
         address to,
         bool skim,
         uint256 amount,
         uint256 share
      ) public allowedBorrow(from, share) notPaused {

we see an 'allowedBorrow()' modifier that checks if the spender (msg.sender) is authorized to spend a value of ‘share’ from an account ‘from’.  This check can be bypassed by providing a 0 value for 'share' and a non-zero value for 'amount'. We then reach the following part of the function : 

        if (share == 0) {
            share = yieldBox.toShare(collateralId, amount, false);          
        }
        userCollateralShare[to] += share;
        uint256 oldTotalCollateralShare = totalCollateralShare;
        totalCollateralShare = oldTotalCollateralShare + share;  

By setting 'share' to zero, the protocol will compute a 'share' value based on the 'amount' within the function. The contract then increments the spender's share balance (userCollateralShare[to]) without checking the allowance requirement, essentially allowing any user to bypass the approval check and steal collateral shares from another user.

This means that an attacker can steal collateral shares from any user by exploiting this vulnerability, causing significant asset loss to the affected users and potentially disrupting the entire protocol. 


## Proof of Concept

Consider the following scenario:

     contractInstance.addCollateral(fromAddress, toAddress, false, 1000000, 0);

In the above example, an attacker calls the 'addCollateral()' function, specifying 'fromAddress' as the victim's address and 'toAddress' as the attacker's own address. The 'share' is passed as 0, and 'amount' is passed as 1000000. As a result, the 'allowedBorrow()' check is bypassed, and 'amount' is converted to 'share' within the function. Consequently, 1000000 units are transferred from the victim's account to the attacker's account without the proper authorization check.


## Tools Used
Manual Audit


## Recommended Mitigation Steps

To prevent such a bypass, it is recommended to check the allowance after the conversion of ‘amount’ to ‘share’ : 

     function _addCollateral(
         address from,
         address to,
         bool skim,
         uint256 amount,
         uint256
default values are returned for  multicall() and multicallValue() after contract deployment with the TapiocaDeployer contract
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-periph-audit/blob/023751a4e987cf7c203ab25d3abba58f7344f213/contracts/Multicall/Multicall3.sol#L41-L61
https://github.com/Tapioca-DAO/tapioca-periph-audit/blob/023751a4e987cf7c203ab25d3abba58f7344f213/contracts/Multicall/Multicall3.sol#L63-L91


# Vulnerability details

## Impact
In the ```Multicall3.sol``` contract, when ```multicall(...)``` and ```multicallValue(...) are called respectively, default (or possibly empty) values are returned, this can lead to mishaps during contract deployment with the TapiocaDeployer contract .

## Proof of Concept
### In the ```multicall(...)``` case
The ```multicall(...)``` contract as shown below returns an array of struct each holding a boolean and a byte element data. However due to its implementation and array ```Result``` struct holding default values is return.
```
    function multicall(
        Call[] calldata calls
    ) public payable returns (Result[] memory returnData) {
        ...
        for (uint256 i = 0; i < length; ) {
            // @audit at this point, each returnData[i] is default value
            Result memory result = returnData[i];
            calli = calls[i];

            // @audit however result is only used to cache each return values for this particular call and as such it is not an array element and will not be recorded or automatically pushed into the returnData array
            (result.success, result.returnData) = calli.target.call(
                calli.callData
            );
            if (!result.success) {
                _getRevertMsg(result.returnData);
            }

            // @note add the line returnData[i] = result here
            unchecked {
                ++i;
            }
        }
    }
```
as shown below, each returnData[i] is default value 
```
Result memory result = returnData[i];
```
and result is used to cache this value which as shown below is used to hold the return value when a call is made to the ```calli``` target address as shown below:
```
(result.success, result.returnData) = calli.target.call( calli.callData );
```
Then the result call is checked for ```success``` but unfortunately, ```returnData[i]``` is not updated with the value of the ```result``` of this particular call and the loops index is increased to make the next call to the hence ```returnData[i]``` still holds default values.

### In the ```multicallValue(...)``` case
Similar explanation holds true for calling ```multicallValue(...)```. To reduce verbosity, i'll summarise as shown below, each returnData[i] is default value 
```
Result memory result = returnData[i];
```
and result is used to cache this value which as shown below is used to hold the return value when a call is made to the ```calli``` target address as shown below:
```
(result.success, result.returnData) = calli.target.call{value: val}(calli.callData);
```
Then the result call is checked for ```success``` but unfortunately, ```returnData[i]``` is not update
Logical error in the ‘transfer()’ function of the MarketERC20 Contract prevents token transfers.
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/MarketERC20.sol#L135-L152


# Vulnerability details

## Impact
Within the ‘transfer()’ function of the MarketERC20.sol contract, a logic error is observed in the first ‘if’ statement, preventing token holders from transferring their tokens to different accounts. This essentially results in a non-compliant ERC20 token and an unusable ‘transfer()’ function. Moreover, although no transfer effectively takes place, events are still emitted, creating a distortion between the event log and the reality. This faulty implementation can mislead users and external contract interactions, as they might rely on these events for state changes or triggering other contract functions. 

## Proof of Concept

The problematic code segment is as follows:

     if (amount != 0 || msg.sender == to) {                                                  
            uint256 srcBalance = balanceOf[msg.sender];
            require(srcBalance >= amount, "ERC20: balance too low");
            if (msg.sender != to) {                                                              
                require(to != address(0), "ERC20: no zero address"); 
                balanceOf[msg.sender] = srcBalance - amount;                   
                balanceOf[to] += amount;                                                               
            }
        }

In the transfer() function, the conditional check (amount != 0 || msg.sender == to) should actually be (amount != 0 && msg.sender != to). As it is, when msg.sender != to, no transfer takes place, even if amount != 0. This means that token transfers to other accounts are effectively disallowed, which is a critical flaw in any token contract.

This can be demonstrated by trying to call the transfer() function with a non-zero amount and a to address different from msg.sender. The transfer will not take place, illustrating the error in the implementation.


## Tools Used
Manual Audit

## Recommended Mitigation Steps

Replace the current ‘require()’ statement with : 

     if (amount != 0 && msg.sender != to) { 
        …
     }


## Assessed type

Context
An attacker could batch calls to execute any state changing function in the contract, like transferring assets or liquidating accounts
Audit: 2023-07-tapioca-findings
Severity: HIGH

# Lines of code

https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/singularity/Singularity.sol#L181-L195


# Vulnerability details

## Impact
So an attacker could:
- Encode calls to transferAssets() or liquidate() with necessary parameters
- Pass an array of these encoded calls to execute()
- execute() would delegatecall and run each function, mutating state


## Proof of Concept
There is no check on who is calling the execute() function. Anyone can call it. It loops through an array of encoded call data and executes each one via delegatecall. The delegatecall is to the Singularity contract itself, so calls can trigger any function in the contract. If a call fails, it will only revert if the revertOnFail flag is true, otherwise execution continues.

## Tools Used
Manual
## Recommended Mitigation Steps
execute() should check that the caller is allowed to make batch calls 
 Additional Suggestion: 
• Move execute() to a separate contract so it doesn't have access to make state changes
• Maintain an allowlist of callable functions for execute()
• Emit events on each call made via execute() for visibility



## Assessed type

Other