Platform: Code4rena
Start Date: 13/05/2022
Pot Size: $30,000 USDC
Total HM: 8
Participants: 65
Period: 3 days
Judge: hickuphh3
Total Solo HM: 1
Id: 125
League: ETH
Rank: 1/65
Findings: 4
Award: $4,163.28
π Selected for report: 1
π Solo Findings: 1
π Selected for report: pedroais
Also found by: 0x4non, 0x52, 0xf15ers, 0xliumin, CertoraInc, Dravee, GimelSec, IllIllI, MaratCerby, StErMi, TerrierLover, WatchPug, berndartmueller, cccz, dipp, fatherOfBlocks, hake, hickuphh3, hyh, isamjay, mtz, oyc_109, p4st13r4, peritoflores, rotcivegaf, saian, simon135, sorrynotsorry, sseefried, tabish, z3s
14.8433 USDC - $14.84
Currently the ETH withdrawing transfer result control is unreachable in _withdrawFromYieldPool as the function returns early and address.call result remains unchecked.
If the call weren't successful for any reason the user funds end up frozen in the contract as the deposit's state is updated, while funds aren't delivered in this case.
Setting the severity to high as that's a violation of the logic of the system and funds freeze impact for the collateral depositor.
_withdrawFromYieldPool doesn't check ETH sending call success as the function now exit beforehand:
// send ETH to user (bool sent, bytes memory data) = address(_to).call{value: receivedETHAmount}(''); return receivedETHAmount; require(sent, Errors.VT_COLLATERAL_WITHDRAW_INVALID);
This way if call be unsuccessful for any reason the corresponding user's funds will be frozen within the contract as withdrawCollateral will go through and the withdrawAmount will be accounted as successfully withdrawn from the lending pool:
// withdraw from lendingPool, it will convert user's aToken to stAsset uint256 _amountToWithdraw = ILendingPool(_addressesProvider.getLendingPool()).withdrawFrom( _stAsset, _stAssetAmount, msg.sender, address(this) ); // Withdraw from vault, it will convert stAsset to asset and send to user // Ex: In Lido vault, it will return ETH or stETH to user uint256 withdrawAmount = _withdrawFromYieldPool(_asset, _amountToWithdraw, _to);
Consider enabling require check by switching the lines:
// send ETH to user (bool sent, bytes memory data) = address(_to).call{value: receivedETHAmount}(''); -+ require(sent, Errors.VT_COLLATERAL_WITHDRAW_INVALID); -+ return receivedETHAmount;
#0 - sforman2000
2022-05-18T03:11:16Z
π Selected for report: hyh
3734.9479 USDC - $3,734.95
Now there are no checks for the amounts to be transferred via _transferYield and _processTreasury. As reward token list is external and an arbitrary token can end up there, in the case when such token doesn't allow for zero amount transfers, the reward retrieval can become unavailable.
I.e. processYield() can be fully blocked for even an extended period, with some low probability, which cannot be controlled otherwise as pool reward token list is external.
Setting the severity to medium as reward gathering is a base functionality for the system and its availability is affected.
_transferYield proceeds with sending the amounts to treasury and yieldManager without checking:
// transfer to treasury if (_vaultFee > 0) { uint256 treasuryAmount = _processTreasury(_asset, yieldAmount); yieldAmount = yieldAmount.sub(treasuryAmount); } // transfer to yieldManager address yieldManager = _addressesProvider.getAddress('YIELD_MANAGER'); TransferHelper.safeTransfer(_asset, yieldManager, yieldAmount);
function _processTreasury(address _asset, uint256 _yieldAmount) internal returns (uint256) { uint256 treasuryAmount = _yieldAmount.percentMul(_vaultFee); IERC20(_asset).safeTransfer(_treasuryAddress, treasuryAmount); return treasuryAmount; }
The incentive token can be arbitrary. Some ERC20 do not allow zero amounts to be sent:
https://github.com/d-xo/weird-erc20#revert-on-zero-value-transfers
In a situation of such a token added to reward list and zero incentive amount earned the whole processYield call will revert, making reward gathering unavailable until either such token be removed from pool's reward token list or some non-zero reward amount be earned. Both are external processes and arenβt controllable.
Consider running the transfers in _transferYield only when yieldAmount is positive:
+ if (yieldAmount > 0) { // transfer to treasury if (_vaultFee > 0) { uint256 treasuryAmount = _processTreasury(_asset, yieldAmount); yieldAmount = yieldAmount.sub(treasuryAmount); } // transfer to yieldManager address yieldManager = _addressesProvider.getAddress('YIELD_MANAGER'); TransferHelper.safeTransfer(_asset, yieldManager, yieldAmount); + }
#0 - HickupHH3
2022-06-03T04:30:18Z
Agree with issue, there have been tokens that require the transfer amount to be non-zero. The other enabler is that the reward token list is arbitrary
367.5749 USDC - $367.57
distributeYield uses Uniswap swaps via _convertAssetToExchangeToken and Curve swaps via _convertToStableCoin. UniswapAdapter and CurveswapAdapter do use Oracle for price estimation, but distributeYield calls use hard coded 5% SLIPPAGE, which is wide enough to make sandwich attacks economically viable, specifically for Uniswap case, where price manipulation can be less costly.
As a result trades converting assets to _exchangeToken can happen at a manipulated price and end up receiving fewer _exchangeTokens than current market price dictates.
Placing severity to medium as impact here is a partial fund loss conditional only on big enough asset amount to be swapped: sandwich attacks are common and can be counted to happen almost always as long as economic viability is present.
_convertAssetToExchangeToken and _convertToStableCoin use SLIPPAGE for DEX output control:
function _convertAssetToExchangeToken(address asset, uint256 amount) internal { UniswapAdapter.swapExactTokensForTokens( _addressesProvider, asset, _exchangeToken, amount, UNISWAP_FEE, SLIPPAGE ); }
receivedAmount = CurveswapAdapter.swapExactTokensForTokens( _addressesProvider, _pool, _exchangeToken, _tokenOut, _amount, SLIPPAGE );
SLIPPAGE is hard coded to be 5%, which is excessively wide and can be exploited whenever the asset amount is big enough:
uint256 public constant SLIPPAGE = 500; // 5%
Consider adding slippage argument to the distributeYield
, so it can be tuned each time accordingly to the current market conditions.
#0 - sforman2000
2022-05-18T02:29:15Z
Duplicate of https://github.com/code-423n4/2022-05-sturdy-findings/issues/133 (high risk)
#1 - HickupHH3
2022-06-06T04:29:30Z
Wrong duplicate identifier. It's a duplicate of #61
π Selected for report: IllIllI
Also found by: 0x1f8b, 0x4non, 0xNazgul, 0xf15ers, 0xkatana, 0xliumin, AlleyCat, BouSalman, Dravee, Funen, GimelSec, Hawkeye, MaratCerby, Picodes, StErMi, TerrierLover, WatchPug, Waze, berndartmueller, bobirichman, cryptphi, csanuragjain, defsec, delfin454000, dipp, fatherOfBlocks, hake, hickuphh3, hyh, joestakey, kebabsec, mics, mtz, oyc_109, p4st13r4, p_crypt0, robee, rotcivegaf, sikorico, simon135, sorrynotsorry, tintin
45.925 USDC - $45.92
If LP token and curve pool be switched in-between system operations, the corresponding assets invested can be frozen within old pool. Users will not be able to run most of the system functionality after that.
Introducing two functions, initialization and switching, is one of the best ways to handle such a dependency. Initialization one should be immutable (for example, via initializer modifier), while pool switching one should ensure no funds are left in the old pool and all the corresponding variables are reset/redirected.
It is now one function that can be run multiple times with a variety of system breaking outcomes:
function setConfiguration(address _lpToken, uint256 _poolId) external onlyAdmin { require(internalAssetToken == address(0), Errors.VT_INVALID_CONFIGURATION); convexBooster = 0xF403C135812408BFbE8713b5A23a04b3D48AAE31; curveLPToken = _lpToken; convexPoolId = _poolId; SturdyInternalAsset _interalToken = new SturdyInternalAsset( string(abi.encodePacked('Sturdy ', IERC20Detailed(_lpToken).symbol())), string(abi.encodePacked('c', IERC20Detailed(_lpToken).symbol())), IERC20Detailed(_lpToken).decimals() ); internalAssetToken = address(_interalToken); }
_lpToken and _poolId needs to be set only once on initialization.
The switching requires asset removal from the old pool and reinvesting to the new one.
I.e. consider introducing independent initialization and switching logic, while making initialization to be able to run only once.
Consider fixing the typo in _amount description:
* @param _amount The mount of asset
#0 - HickupHH3
2022-06-06T05:13:49Z
Low: one step approval NC: Issue 2 Invalid: Issue 1 (core config only settable once)