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: 6/12
Findings: 5
Award: $2,106.31
š Selected for report: 7
š Solo Findings: 2
hickuphh3
In withdraw()
, the withdrawal amount is the proportion of the normalized amounts of all the tokens in the vault and its strategies. However, this amount isn't un-normalized to the output token's decimals, thus leading to an erroneous token amount being withdrawn.
We take USDC as an example, since it has 6 decimals.
deposit(USDC, 1000e6)
: Deposit 1000
USDC into the vault. The user receives 1000
shares.
Attempt a full withdrawal withdraw(1000e18, USDC)
_amount = (balance().mul(_shares)).div(totalSupply()); = (1000e18) * 1000e18 / 1000e18 = 1000e18
The expected _amount
is 1000e6
as per the deposited amount. While the withdrawal will technically work because of the following lines:
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); } }
we note that this logic should only be applied under normal circumstances, where the _amount
is assumed to be in the token decimals.
_amount
needs to be un-normalised back to its native token decimals.
#0 - GalloDaSballo
2021-10-14T16:53:22Z
Duplicate of #131
š Selected for report: hickuphh3
hickuphh3
The vault treats all assets to be of the same price. Given that one can also deposit and withdraw in the same transaction, this offers users the ability to swap available funds held in the vault at parity, with the withdrawal protection fee (0.1%) effectively being the swap fee.
Due care and consideration should therefore be placed if new stablecoins are to be added to the vault (eg. algorithmic ones that tend to occasionally be off-peg), or when lowering the withdrawal protection fee.
setWithdrawalProtectionFee()
could have a requirement for the value to be non-zero. Zero withdrawal fee could be set in setHalted()
whereby only withdrawals will be allowed.#0 - GainsGoblin
2021-10-01T14:39:36Z
Duplicate of #2
#1 - GalloDaSballo
2021-10-14T00:05:36Z
Agree with finding, this vault accounting can be used for arbitrage opportunities as tokens are treated at exact value while they may have imbalances in price
This is not a duplicate as it's explaining a specific attack vector
#2 - GalloDaSballo
2021-10-14T00:06:06Z
Also raising risk valuation as this WILL be used to extract value from the system
š Selected for report: hickuphh3
139.546 YAXIS - $544.23
hickuphh3
Let us assume either of the following cases:
withdrawAll()
has been called on all active strategies to transfer funds into the vault._controller.strategies() = 0
Attempted withdrawals can be frontrun such that users will receive less, or even no funds in exchange for burning vault tokens. This is primarily enabled by the feature of having deposits in multiple stablecoins.
getPricePerFullShare()
of 1e18
(1 vault token = 1 stablecoin). Alice has 1000 vault tokens, while Mallory has 2000 vault tokens, with the vault holdings being 1000 USDC, 1000 USDT and 1000 DAI.getPricePerFullShare()
now returns 2e18
.Hence, Mallory is able to steal Alice's funds by frontrunning her withdrawal transaction.
The withdrawal amount could be checked against getPricePerFullShare()
, perhaps with reasonable slippage.
#0 - GainsGoblin
2021-09-30T22:35:22Z
Duplicate of #28
#1 - GalloDaSballo
2021-10-14T13:59:28Z
Disagree with duplicate label as this shows a Value Extraction, front-running exploit. Medium severity as it's a way to "leak value"
This can be mitigated through addressing the "Vault value all tokens equally" issue
#2 - GainsGoblin
2021-10-14T20:31:18Z
The issue is exactly the same as #28. Both issues present the exact same front-running example.
š Selected for report: hickuphh3
hickuphh3
Whenever a user withdraws from a vault, a withdrawal fee is applied, and is distributed proportionally to the remaining vault token holders.
Should the protocol be halted, if the withdrawal fee remains non-zero, we would have a "last man standing" situation since users who withdraw later will benefit from withdrawals before them.
There would also be the issue of unaccounted withdrawal fees when the last vault token holder withdraws his funds.
setHalted()
#0 - Haz077
2021-09-22T18:22:45Z
Suggested by #71
#1 - GalloDaSballo
2021-10-14T00:07:59Z
Agree with finding Would recommend to tweak architecture to issue shares on withdrawal free accrual, or simply transfer those fees out to avoid this type of situation
Mitigation suggested by warden seems simple enough as well
Is not a duplicate from what I can tell
š Selected for report: hickuphh3
46.5153 YAXIS - $181.41
hickuphh3
A token that has been added and subsequently removed through the manager will cause users' deposited funds in that token to be trapped in the vault.
The vault's deposit and withdraw modifier should be separate, such that users are still able to withdraw previously added (but subsequently removed) tokens from the vault.
#0 - uN2RVw5q
2021-10-03T19:51:42Z
The vault's deposit and withdraw modifier should be separate, such that users are still able to withdraw previously added (but subsequently removed) tokens from the vault.
Allowing this doesn't feel right to me.
As long as there is some token available, the user will be able to withdraw their share in an equivalent token, even if that token is different from the deposited token.
I would consider this to be a non-issue. Perhaps this can be converted into a documentation tag. Add some documentation about checking before a token is removed in Manager contract and see if an equivalent token is present in the vault sounds reasonable. In the worst case, a removed token can still be added back by the manager, and therefore, the tokens should be safe.
#1 - GalloDaSballo
2021-10-14T14:10:11Z
Agree with the original warden finding reduced severity to Low
The admin strategist
has the ability to remove tokens, this removal can impact accounting, notably Vault.sol'sĀ balanceOfThis
May want to add a check on removeToken
6.7734 YAXIS - $26.42
hickuphh3
L141: The safemath subtraction in strategies[_vault].addresses[index] = strategies[_vault].addresses[tail.sub(1)];
is unnecessary because this code block will not be executed in the case tail = strategies[_vault].addresses.length = 0
(vault has no strategies, found
will be false).
strategies[_vault].addresses[index] = strategies[_vault].addresses[tail - 1];
#0 - GainsGoblin
2021-09-18T11:34:02Z
Duplicate of #46
#1 - GalloDaSballo
2021-10-12T22:08:31Z
Duplicate of #46
2.7432 YAXIS - $10.70
hickuphh3
L172: _shares = _shares.add(_amount);
can simply be _shares = _amount
because the prior value of _shares
is always 0.
#0 - Haz077
2021-09-22T18:19:15Z
Same as #6
#1 - GalloDaSballo
2021-10-12T22:50:33Z
Duplicate of #118
2.7432 YAXIS - $10.70
hickuphh3
The notHalted
modifier in depositMultiple()
is redundant because it is checked (multiple times) by the underlying function call to deposit()
.
Further optimizations may be done to implement an internal _deposit()
function that will be called by both deposit()
and depositMultiple()
so that notHalted
is only checked once.
Remove the notHalted
modifier in depositMultiple()
.
#0 - GalloDaSballo
2021-10-12T22:41:48Z
Sponsor mitigated
š Selected for report: hickuphh3
15.052 YAXIS - $58.70
hickuphh3
_vaultDetails[_vault].balance
in L367 can be changed to the already fetched value _balance
.
_vaultDetails[_vault].balance = _vaultDetails[_vault].balance.sub(_amount);
#0 - GalloDaSballo
2021-10-12T22:48:39Z
Sponsor acknowledge and mitigated, LG
š Selected for report: hickuphh3
15.052 YAXIS - $58.70
hickuphh3
The negation of the disjunction form of the canHarvest()
function will help save gas. In other words, instead of !(A || B)
, return (!A && !B)
.
function canHarvest( address _vault ) public view returns (bool) { Strategy storage strategy = strategies[_vault]; // only can harvest if there are strategies, and when sufficient time has elapsed // solhint-disable-next-line not-rely-on-time return (strategy.addresses.length > 0 && strategy.lastCalled <= block.timestamp.sub(strategy.timeout)); }
#0 - GalloDaSballo
2021-10-12T22:49:55Z
Nice gas optimization by using boolean short circuit
#1 - GalloDaSballo
2021-10-12T22:50:00Z
Sponsor has mitigated