Platform: Code4rena
Start Date: 09/09/2021
Pot Size: $60,000 USDC
Total HM: 24
Participants: 12
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 14
Id: 30
League: ETH
Rank: 7/12
Findings: 2
Award: $979.62
🌟 Selected for report: 1
🚀 Solo Findings: 0
WatchPug
The wantToken of the strategy may be different from the _token
argument of Controller.withdraw(address _token, uint256 _amount)
according to code at line 469-474 of Controller.sol
.
if (_want != _token) { address _converter = _vaultDetails[msg.sender].converter; IERC20(_want).safeTransfer(_converter, _amounts[i]); // TODO: do estimation for received IConverter(_converter).convert(_want, _token, _amounts[i], 1); }
However, the token address of _vaultDetails[_vault].balances[_strategy]
will always be the strategy's wantToken address according to code at line 635.
_vaultDetails[_vault].balances[_strategy] = IStrategy(_strategy).balanceOf();
return balanceOfWant().add(balanceOfPool());
At line 610-620 of Controller.getBestStrategyWithdraw(address _token, uint256 _amount)
, it just plainly compare and compute with _vaultDetails[_vault].balances[_strategy]
(in strategy's wantToken) and _amount
(in _token
argument). Result in miscalculation here without necessary conversion.
_balance = _vaultDetails[_vault].balances[_strategy]; // if the strategy doesn't have the balance to cover the withdraw if (_balance < _amount) { // withdraw what we can and add to the _amounts _amounts[i] = _balance; _amount = _amount.sub(_balance); } else { // stop scanning if the balance is more than the withdraw amount _amounts[i] = _amount; break; }
And line 473-478 of Controller.withdraw(address _token, uint256 _amount)
, it will convert amounts[i]
of wantToken to _token
, and transfer the converted _token
to msg.sender.
If the token price of wantToken is lower than _token
, Controller.withdraw(...)
will transfer insufficient amount of _token
to msg.sender
(Vault contract);
If the token price of wantToken is higher than _token
, Controller.withdraw(...)
will transfer overmuch _token
to msg.sender
(Vault contract).
IConverter(_converter).convert(_want, _token, _amounts[i], 1); } } _amount = IERC20(_token).balanceOf(address(this)); _vaultDetails[msg.sender].balance = _vaultDetails[msg.sender].balance.sub(_amount); IERC20(_token).safeTransfer(msg.sender, _amount);
Therefore after Vault.withdraw(...)
called controller.withdraw(_output, _toWithdraw)
, the received output token amount _diff
may be less than expected _toWithdraw
.
This causes the _amount
of _output
transferred to the user to be less than the amount it should have been. This is not because there is not enough balance in the strategy, but because there is a miscalculation and not enough want tokens are withdrawn from the underlying.
if (_balance < _amount) { IController _controller = IController(manager.controllers(address(this))); uint256 _toWithdraw = _amount.sub(_balance); if (_controller.strategies() > 0) { _controller.withdraw(_output, _toWithdraw); } uint256 _after = IERC20(_output).balanceOf(address(this)); uint256 _diff = _after.sub(_balance); if (_diff < _toWithdraw) { _amount = _balance.add(_diff); } } IERC20(_output).safeTransfer(msg.sender, _amount);
Users may get a fewer amount of token when call vault.withdraw(...) while the user's stored balance gets deducted for a larger amount.
If there is a BTCB vault with BNB as wantToken
, and we assume that there is no wallet balance of BTCB token in the vault.
An user may do the following steps:
btcbVault.withdraw(112e18, btcbToken)
;controller.withdraw(btcbToken, 112e18)
controller.getBestStrategyWithdraw
returns ([bnbWantedStrategy], [112e18])
;controller.withdraw(...)
L469-474 will swap 112e18 of bnbToken to 1e18 of btcbToken;controller.withdraw(...)
L476-478 will transfer 1e18 of btcbToken to msg.sender (btcbVault);Vault.withdraw(...)
L259 will update the _amount to 1e18.As a result, the user will lose funds.
Make sure to swap tokens when converting from tokenA to tokenB.
#0 - GainsGoblin
2021-10-06T19:42:53Z
Duplicate of #8
#1 - GalloDaSballo
2021-10-14T16:59:52Z
Duplicate of #28
125.5914 YAXIS - $489.81
WatchPug
The total balance should NOT be simply added from different tokens' tokenAmounts, considering that the price of tokens may not be the same.
function balanceOfThis() public view returns (uint256 _balance) { address[] memory _tokens = manager.getTokens(address(this)); for (uint8 i; i < _tokens.length; i++) { address _token = _tokens[i]; _balance = _balance.add(_normalizeDecimals(_token, IERC20(_token).balanceOf(address(this)))); } }
function harvestStrategy( address _strategy, uint256 _estimatedWETH, uint256 _estimatedYAXIS ) external override notHalted onlyHarvester onlyStrategy(_strategy) { uint256 _before = IStrategy(_strategy).balanceOf(); IStrategy(_strategy).harvest(_estimatedWETH, _estimatedYAXIS); uint256 _after = IStrategy(_strategy).balanceOf(); address _vault = _vaultStrategies[_strategy]; _vaultDetails[_vault].balance = _vaultDetails[_vault].balance.add(_after.sub(_before)); _vaultDetails[_vault].balances[_strategy] = _after; emit Harvest(_strategy); }
/** * @notice Returns the total balance of the vault, including strategies */ function balance() public view override returns (uint256 _balance) { return balanceOfThis().add(IController(manager.controllers(address(this))).balanceOf()); }
An attacker can steal funds from multi-token vaults. Result in fund loss of all other users.
If there is a multi-token vault with 3 tokens: DAI, USDC, USDT, and their price in USD is now 1.05, 0.98, and 0.95. If the current balances are: 2M, 1M, and 0.5M.
An attacker may do the following steps:
As 2M of DAI + 1M of USDC worth much more than 3M of USDT. The attacker will profit and all other users will be losing funds.
Always consider the price differences between tokens.
#0 - GalloDaSballo
2021-10-13T15:54:16Z
Fully agree with the finding, assuming price of tokens is the same exposes the Vault and all depositors to risk of Single Sided Exposure
This risk has been exploited multiple times, notably in the Yearn Exploit
The solution for for managing tokens with multiple values while avoiding being rekt is to have an index that ensures your LP Token maintains it's peg, curve's solution is called virtual_price
Having a virtual price would allow to maintain the Vault Architecture, while mitigating exploits that directly use balances