---
# System prepended metadata

title: Taurus Protocol

---

# Taurus Protocol

The Taurus protocol offers liquidity on interest-bearing tokens. Depositors can mint Taurus' stablecoin ($TAU) by collateralizing their tokens. Taurus is an over-collateralized stablecoin protocol that allows users to borrow $TAU against the value of the collateral deposited. The collateral consists of interest-bearing tokens from perpetual swap platforms built on Arbitrum.


## Who is the team behind it?
Unknown at this stage

## How does Taurus work? 
Similar to MakerDAO's DAI and Liquity's LUSD, Taurus' smart contracts allow depositors to collateralize and mint the stablecoin $TAU. However, Taurus requires depositors to use yield-bearing collateral tokens like GLP, which earns rewards and fees. Taurus collects the yield from deposited collateral and uses it to repay users' debts, continuously reducing their debt. Unlike MakerDAO and Liquity, Taurus does not charge direct stability fees on loans made through its vaults.



## Deep dive to code and the findings

We believe that there are specific aspects of the Taurus protocol that require further investigation to determine if they are implemented correctly or if there are areas for improvement. Some of the items we want to examine include:


1-**How does Taurus distribute repaid debt among its users? If rewards are sold and immediately written on-chain, is it possible for an attacker to sandwich the yield by entering the contract and then immediately exiting after the sold rewards have been used to repay users' debts?**

The Taurus protocol uses the updateReward modifier every time a user generates more TAU, repays some TAU, increases collateral, withdraws some of the collateral, or liquidates some account positions. This modifier ensures that the latest debt is always updated, which is a correct approach.

    modifier updateReward(address _account) {
        // Disburse available yield from the drip feed
        _disburseTau();

        // If user has collateral, pay down their debt and recycle surplus rewards back into the tauDripFeed.
        uint256 _userCollateral = userDetails[_account].collateral;
        if (_userCollateral > 0) {
            // Get diff between global rewardPerCollateral and user lastUpdatedRewardPerCollateral
            uint256 _rewardDiff = cumulativeTauRewardPerCollateral -
                userDetails[_account].lastUpdatedRewardPerCollateral;

            // Calculate user's TAU earned since the last update, use it to pay off debt
            uint256 _tauEarned = (_rewardDiff * _userCollateral) / Constants.PRECISION;

            if (_tauEarned > 0) {
                uint256 _userDebt = userDetails[_account].debt;
                if (_tauEarned > _userDebt) {
                    // If user has earned more than enough TAU to pay off their debt, pay off debt and add surplus to drip feed
                    userDetails[_account].debt = 0;

                    _withholdTau(_tauEarned - _userDebt);
                    _tauEarned = _userDebt;
                } else {
                    // Pay off as much debt as possible
                    userDetails[_account].debt = _userDebt - _tauEarned;
                }

                emit TauEarned(_account, _tauEarned);
            }
        } else {
            // If this is a new user, add them to the userAddresses array to keep track of them.
            if (userDetails[_account].startTimestamp == 0) {
                userAddresses.push(_account);
                userDetails[_account].startTimestamp = block.timestamp;
            }
        }

        // Update user lastUpdatedRewardPerCollateral
        userDetails[_account].lastUpdatedRewardPerCollateral = cumulativeTauRewardPerCollateral;
        _;
    }

In order to comprehense this fully, we need the content of _disburseTau() so here it is:


```
function _disburseTau() internal {
        if (tauWithheld > 0) {
            uint256 _currentCollateral = IERC20(collateralToken).balanceOf(address(this));

            if (_currentCollateral > 0) {
                // Get tokens to disburse since last disbursal
                uint256 _timeElapsed = block.timestamp - tokensLastDisbursedTimestamp;

                uint256 _tokensToDisburse;
                if (_timeElapsed >= DRIP_DURATION) {
                    _tokensToDisburse = tauWithheld;
                    tauWithheld = 0;
                } else {
                    _tokensToDisburse = (_timeElapsed * tauWithheld) / DRIP_DURATION;
                    tauWithheld -= _tokensToDisburse;
                }

                // Divide by current collateral to get the additional tokensPerCollateral which we'll be adding to the cumulative sum
                uint256 _extraRewardPerCollateral = (_tokensToDisburse * Constants.PRECISION) / _currentCollateral;

                cumulativeTauRewardPerCollateral += _extraRewardPerCollateral;

                tokensLastDisbursedTimestamp = block.timestamp;
            }
        }
    }
```

Within the modifier, the _disburseTau() function is called to distribute available yield from the drip feed. The tauWithheld variable holds the cumulative TAU for repaying the debt of users. In order to prevent sandwich attacks, Taurus spreads the tauWithheld over a time interval. TAU is distributed according to the formula tokensToDisburse = (_timeElapsed * tauWithheld) / DRIP_DURATION;, where timeElapsed is the time passed since the last time disburseTau was called and DRIP_DURATION is the maximum amount of spread, which is 1 day.

For example, if there are 100 TAU to repay the debt of users, tauWithheld will be set to 100, and this TAU will be repaying the debt of the users for the entire day, increasing linearly up to reach 100 after 1 day. So after 12 hours, 50 TAU will be repaid, and after another 12 hours, the remaining 50 TAU will be repaid to users. Since TAU is repaid every second for a day, there are no sandwich attack vulnerabilities.

*Overall, the Taurus protocol's updateReward modifier and disburseTau function provide a secure and effective way to distribute rewards and repay debts.*

2-**Is there a risk of keepers acting maliciously and not selling the rewards properly, given that the rewards will be collected from the vault contract and sold for TAU in the Taurus protocol?**

 Unfortunately, there is a risk. During our audit process, we found a critical vulnerability in the keeper functions, as well as some logical errors. The code snippet below shows the keeper's swap function:

```
function swapForTau(
        address _yieldTokenAddress,
        uint256 _yieldTokenAmount,
        uint256 _minTauReturned,
        bytes32 _swapAdapterHash,
        uint256 _rewardProportion,
        bytes calldata _swapParams
    ) external onlyKeeper whenNotPaused {
        // Ensure keeper is allowed to swap this token
        if (_yieldTokenAddress == collateralToken) {
            revert tokenCannotBeSwapped();
        }

        if (_yieldTokenAmount == 0) {
            revert zeroAmount();
        }

        // Get and validate swap adapter address
        address swapAdapterAddress = SwapAdapterRegistry(controller).swapAdapters(_swapAdapterHash);
        if (swapAdapterAddress == address(0)) {
            // The given hash has not yet been approved as a swap adapter.
            revert unregisteredSwapAdapter();
        }

        // Calculate portion of tokens which will be swapped for TAU and disbursed to the vault, and portion which will be sent to the protocol.
        uint256 protocolFees = (feeMapping[Constants.GLP_VAULT_PROTOCOL_FEE] * _yieldTokenAmount) /
            Constants.PERCENT_PRECISION;
        uint256 swapAmount = _yieldTokenAmount - protocolFees;

        // Transfer tokens to swap adapter
        IERC20(_yieldTokenAddress).safeTransfer(swapAdapterAddress, swapAmount);

        // Call swap function, which will transfer resulting tau back to this contract and return the amount transferred.
        // Note that this contract does not check that the swap adapter has transferred the correct amount of tau. This check
        // is handled by the swap adapter, and for this reason any registered swap adapter must be a completely trusted contract.
        uint256 tauReturned = BaseSwapAdapter(swapAdapterAddress).swap(tau, _swapParams);

        if (tauReturned < _minTauReturned) {
            revert tooMuchSlippage(tauReturned, _minTauReturned);
        }

        // Burn received Tau
        ERC20Burnable(tau).burn(tauReturned);

        // Add Tau rewards to withheldTAU to avert sandwich attacks
        _disburseTau();
        _withholdTau((tauReturned * _rewardProportion) / Constants.PERCENT_PRECISION);

        // Send protocol fees to FeeSplitter
        IERC20(_yieldTokenAddress).safeTransfer(
            Controller(controller).addressMapper(Constants.FEE_SPLITTER),
            protocolFees
        );

        // Emit event
        emit Swap(_yieldTokenAddress, protocolFees, swapAmount, tauReturned);
    }
```

As can be seen, the inputs to the keepers are not thoroughly checked. Here are some of the critical issues we found:



Before we delve into these issues, let's first understand what the rewardProportion variable that the keeper is passing to the function means. According to the comment in the Taurus original code:
 ```
@param _rewardProportion refers to the proportion of received tau which will be rewarded (i.e. pay back user loans). The remainder will simply be burned without
     * being distributed to users. This undistributed tau cancels out bad debt in the vault. All vaults retain a growing reserve of yield to ensure bad debt
     * will always be covered.
     * _rewardProportion has a precision of 1e18. If _rewardProportion = 1e18, all tau will be disbursed to users. If _rewardProportion = 0, none of the burned tau will be disbursed.
```

As we can see from the example provided in the comment, rewardProportion should be an integer 0 < rewardProportion < 1e18. However, there are no checks inside the contract verifying that rewardProportion is within these limits. If a keeper were to give a very large integer value for this variable, then basically all of the debt would be repaid from the users. So the keeper be clearing the debt of the users. Let's dig even deeper!

The rewardProportion variable is used as an input of the _withholdTau() function, which is responsible for incrementing the tauWithheld variable, which is the TAU used to repay the users' debt.

```
function _withholdTau(uint256 amount) internal {
        // Update block.timestamp in case it hasn't been updated yet this transaction.
        tokensLastDisbursedTimestamp = block.timestamp;
        tauWithheld += amount;
    }
```

As you may recall from the previous finding, tauWithheld is used in the _disburseTau() function. The following lines in the _disburseTau() function are important to check:

```
if (_timeElapsed >= DRIP_DURATION) {
                    _tokensToDisburse = tauWithheld;
                    tauWithheld = 0;
                } else {
                    _tokensToDisburse = (_timeElapsed * tauWithheld) / DRIP_DURATION;
                    tauWithheld -= _tokensToDisburse;
                }

                // Divide by current collateral to get the additional tokensPerCollateral which we'll be adding to the cumulative sum
                uint256 _extraRewardPerCollateral = (_tokensToDisburse * Constants.PRECISION) / _currentCollateral;

                cumulativeTauRewardPerCollateral += _extraRewardPerCollateral;
```

It has been discovered that the rewardProportion input by the keeper can be manipulated to cause an issue in the updateReward modifier. Specifically, if a very large value is inputted, the resulting _tokensToDisburse will also be very high, leading to an excessively high cumulativeTauRewardPerCollateral.

If a user then calls the updateReward modifier, the following lines of code become problematic:

```
if (_userCollateral > 0) {
        // Get diff between global rewardPerCollateral and user lastUpdatedRewardPerCollateral
        uint256 _rewardDiff = cumulativeTauRewardPerCollateral -
            userDetails[_account].lastUpdatedRewardPerCollateral;

        // Calculate user's TAU earned since the last update, use it to pay off debt
        uint256 _tauEarned = (_rewardDiff * _userCollateral) / Constants.PRECISION;

        if (_tauEarned > 0) {
            uint256 _userDebt = userDetails[_account].debt;
            if (_tauEarned > _userDebt) {
                // If user has earned more than enough TAU to pay off their debt, pay off debt and add surplus to drip feed
                userDetails[_account].debt = 0;

                _withholdTau(_tauEarned - _userDebt);
                _tauEarned = _userDebt;
            } else {
                // Pay off as much debt as possible
                userDetails[_account].debt = _userDebt - _tauEarned;
            }
```

Since the cumulativeTauRewardPerCollateral has been artificially increased, the _rewardDiff and resulting _tauEarned values will be excessively high. In this case, if the tau earned is greater than the user's debt, the debt will be completely erased. *This is clearly an unintended behavior and needs to be addressed.*


## Conclusion
Taurus protocol enables depositors to use yield-bearing tokens as collateral to generate the stablecoin $TAU. Unlike traditional systems, Taurus collects all reward tokens from the deposited collateral and sells them for $TAU to repay users' debts, rather than allowing the collateral position to increase. However, we believe this system may face challenges for several reasons.

Firstly, Taurus requires external liquidity to enable keepers to sell reward tokens for $TAU to repay debts. If there is insufficient liquidity to cover the constant sell pressure, trades may be inefficient, resulting in a return of $TAU that does not fully compensate the sold reward token amount. Even with sufficient external liquidity, swap fees will impact the yield's power, leading to a loss when converting reward tokens to $TAU. In addition to these, Taurus also takes some of the reward tokens as fee to their treasury.

Additionally, users must generate some $TAU and create liquidity pools elsewhere for the keepers to obtain the necessary liquidity. While there is currently no information on how to incentivize these liquidity providers, we speculate that Taurus governance token TGT will be awarded to attract more liquidity to these pools.

Lastly, depositors must generate some $TAU on their collateral position; otherwise, the yield of the collateral will be utilized to cover the debts of other $TAU generators, leading to potential complications.

If a user is not generating TAU over their yield-bearing collateral, the collateral will be used to repay the debt of other TAU generators. To optimize the use of their collateral, users must generate some TAU. This ensures that they can fully benefit from their collateral while avoiding the risk of inadvertently helping to repay the debt of other users. By doing so, they can maximize the returns on their investments and maintain a fair and balanced ecosystem within the Taurus protocol.