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.
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).
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.
Handle
WatchPug
Vulnerability details
The wantToken of the strategy may be different from the
_token
argument ofController.withdraw(address _token, uint256 _amount)
according to code at line 469-474 ofController.sol
.https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/controllers/Controller.sol#L469-L474
However, the token address of
_vaultDetails[_vault].balances[_strategy]
will always be the strategy's wantToken address according to code at line 635.https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/controllers/Controller.sol#L635
https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/strategies/BaseStrategy.sol#L212
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.https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/controllers/Controller.sol#L610-L620
And line 473-478 of
Controller.withdraw(address _token, uint256 _amount)
, it will convertamounts[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
tomsg.sender
(Vault contract);If the token price of wantToken is higher than
_token
,Controller.withdraw(...)
will transfer overmuch_token
tomsg.sender
(Vault contract).https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/controllers/Controller.sol#L473-L478
Therefore after
Vault.withdraw(...)
calledcontroller.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.https://github.com/code-423n4/2021-09-yaxis/blob/cf7d9448e70b5c1163a1773adb4709d9d6ad6c99/contracts/v3/Vault.sol#L250-L263
Impact
Users may get a fewer amount of token when call vault.withdraw(...) while the user's stored balance gets deducted for a larger amount.
Proof of Concept
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.
Recommended Mitigation Steps
Make sure to swap tokens when converting from tokenA to tokenB.