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: 105/132
Findings: 2
Award: $43.30
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/curve/TricryptoNativeStrategy.sol#L151-L179 https://github.com/Tapioca-DAO/tapioca-yieldbox-strategies-audit/blob/05ba7108a83c66dada98bc5bc75cf18004f2a49b/contracts/curve/TricryptoLPStrategy.sol#L160-L196
compound()
has no access controls and performs swapping with the strategy reward funds. This can be gamed by an attacker as the pool can be atomically manipulated to provide significantly off-market price for the swap.
An attacker can skew the pool and call compound()
, which will execute the rewardToken
to wrappedNative
swap at an unfavourable price, benefitting the attacker, who will return the pool to initial state after the call, pocketing the crvAmount * price_difference
of value. This way a self-called sandwiching is possible: attacker will run compound()
whenever there is a big enough amount of rewards due and sandwich this transaction.
Also, minAmount
is calculated off the pool data and provides no protection against the attack.
Strategy CRV
rewards due can be stolen this way.
TricryptoNativeStrategy's compound()
has no access controls and swaps minted CRV
on the market:
@> function compound(bytes memory) public { uint256 claimable = lpGauge.claimable_tokens(address(this)); if (claimable > 0) { uint256 crvBalanceBefore = rewardToken.balanceOf(address(this)); minter.mint(address(lpGauge)); uint256 crvBalanceAfter = rewardToken.balanceOf(address(this)); if (crvBalanceAfter > crvBalanceBefore) { uint256 crvAmount = crvBalanceAfter - crvBalanceBefore; ISwapper.SwapData memory swapData = swapper.buildSwapData( address(rewardToken), address(wrappedNative), crvAmount, 0, false, false ); uint256 calcAmount = swapper.getOutputAmount(swapData, ""); @> uint256 minAmount = calcAmount - (calcAmount * 50) / 10_000; //0.5% @> swapper.swap(swapData, minAmount, address(this), ""); uint256 queued = wrappedNative.balanceOf(address(this)); _addLiquidityAndStake(queued); emit AmountDeposited(queued); } } }
The same is for TricryptoLPStrategy:
@> function compound(bytes memory) public { uint256 claimable = lpGauge.claimable_tokens(address(this)); if (claimable > 0) { uint256 crvBalanceBefore = rewardToken.balanceOf(address(this)); minter.mint(address(lpGauge)); uint256 crvBalanceAfter = rewardToken.balanceOf(address(this)); if (crvBalanceAfter > crvBalanceBefore) { uint256 crvAmount = crvBalanceAfter - crvBalanceBefore; ISwapper.SwapData memory swapData = swapper.buildSwapData( address(rewardToken), address(wrappedNative), crvAmount, 0, false, false ); @> uint256 calcAmount = swapper.getOutputAmount(swapData, ""); uint256 minAmount = calcAmount - (calcAmount * 50) / 10_000; //0.5% swapper.swap(swapData, minAmount, address(this), ""); uint256 wrappedNativeAmount = wrappedNative.balanceOf( address(this) ); calcAmount = lpGetter.calcWethToLp(wrappedNativeAmount); @> minAmount = calcAmount - (calcAmount * 50) / 10_000; //0.5% uint256 lpAmount = lpGetter.addLiquidityWeth( wrappedNativeAmount, minAmount ); lpGauge.deposit(lpAmount, address(this), false); emit AmountDeposited(lpAmount); } } }
Minimum amount calculated from the pool data, which can be skewed by the attacker, so such approach provides no real control:
/// @notice Computes amount out for amount in /// @param swapData operation data /// @param dexOptions AMM data function getOutputAmount( SwapData calldata swapData, bytes calldata dexOptions ) external view override returns (uint256 amountOut) { uint256[] memory tokenIndexes = abi.decode(dexOptions, (uint256[])); (uint256 amountIn, ) = _getAmounts( swapData.amountData, swapData.tokensData.tokenInId, swapData.tokensData.tokenOutId, yieldBox ); >> amountOut = curvePool.get_dy( int128(int256(tokenIndexes[0])), int128(int256(tokenIndexes[1])), amountIn ); }
Manual Review
Consider allowing for permissioned runs of compound()
only, and basing minAmount
requirement on the input from this trusted caller.
Additionally comparing the provided minimum with Oracle price is advised to deal with semi-trusted callers (this will not eliminate the surface, but will limit the damage).
Access Control
#0 - c4-pre-sort
2023-08-06T17:38:01Z
minhquanym marked the issue as duplicate of #245
#1 - c4-judge
2023-09-13T14:59:22Z
dmvt marked the issue as duplicate of #158
#2 - c4-judge
2023-09-13T15:00:51Z
dmvt changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-09-13T15:05:42Z
dmvt marked the issue as satisfactory
#4 - c4-judge
2023-09-19T11:51:32Z
dmvt marked the issue as duplicate of #245