# Audit Recap #3: Taurus Protocol *We regularly publish short recaps on a decentralized audit in which we participated. This time, we cover the audit of [Taurus Protocol](https://www.taurus.loans).* ![](https://i.imgur.com/EldORCY.png) *brainbot is a web3 service provider, offering consulting and development services as well as [smart contract audits](https://brainbot.com/smart-contract-audits/). To gain more experience in auditing, our security researchers regularly participate in [decentralized audits](https://medium.com/@brainbot/decentralised-audits-are-here-to-stay-bcee6d1118a8). In this series, we will publish recaps of audits in which we participated in order to provide some insight into the functioning, the smart contract architecture and our findings for the respective protocols.* ## 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 – Taurus Protocol is currently working in "stealth mode". ### 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? ```solidity 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: ```solidity 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: ```solidity 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. ``` solidity 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: ```solidity 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: ```solidity 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. ## What's next? We'll continue to regularly publish audit recaps for different protocols. Meanwhile, you can also have a look at our other publications on [Medium](https://medium.com/@brainbot). You can also find our previous audit recaps on our [overview page](https://hackmd.io/@brainbot-services). ## Hard Facts **Decentralized Audit Platform:** [Sherlock](https://www.sherlock.xyz) **Audited Protocol:** [Taurus Protocol](https://app.sherlock.xyz/audits/contests/45) **Security Researchers:** Côme du Crest (ranked 2/159 in this contest), Murathan Selimoğlu (ranked 10/159 in this contest)