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: 5/12
Findings: 6
Award: $2,715.34
🌟 Selected for report: 10
🚀 Solo Findings: 0
0xsanson
During Controller.setCap
we change _vaultDetails[_vault].balance
to _vaultDetails[_vault].balance.sub(_balance)
. This is wrong, and the correct value should be _vaultDetails[_vault].balance.sub(_diff)
, because _diff
is the value withdrawn from the strategy.
High risk because this is an accounting error that propagates though the function balance()
in Vault.sol, so for all deposits/withdraws.
editor
Correct using _diff
instead of _balance
.
#0 - gpersoon
2021-09-29T12:36:56Z
Duplicate of #1
#1 - GalloDaSballo
2021-10-12T23:34:44Z
Duplicate of #1
0xsanson
The Controller contract can call converter.convert
inside earn
and withdraw
functions, after transferring amount
of tokens to the Converter contract. This contract assumes that it has received exactly amount
tokens, however this isn't true for fee-on-transfer tokens. This will cause the aforementioned functions to revert, basically making the entire protocol unusable.
https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L426 https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/converters/StablesConverter.sol#L110
editor
The Converter.convert function should look at its token's balance and use that variable as _inputAmount
.
#0 - Haz077
2021-09-28T16:42:12Z
Same as #127
#1 - GalloDaSballo
2021-10-12T23:22:34Z
Agree with finding, simple mitigation is to check actual balance in contract
Additional simple mitigation is to NOT use any token with feeOnTransfer
#2 - GalloDaSballo
2021-10-12T23:25:45Z
Duplicate of #127
#3 - GalloDaSballo
2021-10-13T15:39:16Z
Downgraded to Medium as while this is potentially a serious risk, it can be mitigated by simply not using tokens that have fees
0xsanson
In the Vault.withdraw
function an user burns _shares
quantity of VaultTokens to get _amount
of outputTokens back from the vault.
If the vault doesn't have enough tokens, even after withdrawing from the controller, they receive less tokens than they should; in other terms, they could have burned less tokens to receive the same output quantity.
Even if the users check the vault before sending the withdraw transaction, they can still be frontrun since there's no slippage-like parameter.
editor
Suggested adding a way to compensate an user. For example by giving change in VaultTokens.
#0 - uN2RVw5q
2021-10-02T16:10:37Z
#1 - GalloDaSballo
2021-10-13T23:21:20Z
Duplicate of #121
62.7957 YAXIS - $244.90
0xsanson
In the NativeStrategyCurve3Crv._harvest
there are two instances that a bad actor could use to frontrun the harvest.
First, when we are swapping WETH to a stablecoin by calling _swapTokens(weth, _stableCoin, _remainingWeth, 1)
the function isn't checking the slippage, leading to the risk to a frontun (by imbalancing the Uniswap pair) and losing part of the harvesting profits.
Second, during the _addLiquidity
internal function: this calls stableSwap3Pool.add_liquidity(amounts, 1)
not considering the slippage when minting the 3CRV tokens.
editor
In the function _harvest(_estimatedWETH, _estimatedYAXIS)
consider adding two additional estimated quantities: one for the swapped-out stablecoin and one for the minted 3CRV.
#0 - uN2RVw5q
2021-10-02T14:58:02Z
On second thought, I think this is a valid issue.
consider adding two additional estimated quantities: one for the swapped-out stablecoin and one for the minted 3CRV.
This suggestion should be considered.
#1 - GalloDaSballo
2021-10-14T15:38:22Z
Warden identified two paths for front-running
Since these are ways to extract value, severity is Medium
🌟 Selected for report: 0xsanson
46.5153 YAXIS - $181.41
0xsanson
NativeStrategyCurve3Crv._harvest
calls getMostPremium
to get the best stablecoin to convert to. This function however is wrong in the case of balancesUSDC = balancesUSDT < balancesDAI
, because it returns DAI, when it should be USDC or USDT.
This is naturally a rare occasion, but a bad actor can set the balances (by depositing/withdrawing the Curve pool) like this just before the harvest function is called. Since this would imbalance even more the pool, the bad actor could also gain a profit by making the right swaps after the harvest.
editor
Convert all <
into <=
inside getMostPremium()
.
#0 - BobbyYaxis
2021-09-26T23:09:58Z
Harvest cannot be called by an external party to initiate, nor is there a warning when that is about to happen.
#1 - Haz077
2021-09-28T16:50:12Z
I totally agree, this should be labelled as low-risk
#2 - uN2RVw5q
2021-10-02T15:41:38Z
I'll implement this.
I'll also do a refactoring along with this. There is no need to use a memory array, just regular local variables should be enough. It'll also be cheaper than going through memory.
#3 - uN2RVw5q
2021-10-02T16:04:32Z
Implemented in https://github.com/code-423n4/2021-09-yaxis/pull/32
#4 - GalloDaSballo
2021-10-12T23:29:40Z
Sponsor Mitigated Agree with low severity as funds are not at risk, and harvest cannot be just called by anyone Would recommend also using Flashbots or Private Transactions as further mitigation
0xsanson
In BaseStrategy.approveForSpender
there's a _token.safeApprove(_spender, _amount)
call. This call will revert if the allowance is already different from zero.
Openzeppelin's SafeERC20: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/utils/SafeERC20.sol#L53
editor
Add a _token.safeApprove(_spender, 0)
first.
#0 - uN2RVw5q
2021-10-02T15:35:58Z
Duplicate of https://github.com/code-423n4/2021-09-yaxis-findings/issues/63
Implemented and merged: https://github.com/code-423n4/2021-09-yaxis/pull/13
#1 - GalloDaSballo
2021-10-13T23:43:45Z
Duplicate of #63
🌟 Selected for report: 0xsanson
46.5153 YAXIS - $181.41
0xsanson
When adding a strategy in Controller.sol, a variable cap
is passed to _vaultDetails[_vault].caps[_strategy]
. I guess this is the maximum balance allowed for this strategy, but this is actually never used in the current implementation.
editor
Check if this is a wanted feature of the protocol, and (if positive) add some checks to enforce it.
#0 - Haz077
2021-09-30T21:52:49Z
I also don't see where is cap used.
#1 - uN2RVw5q
2021-10-07T19:13:59Z
I also don't see where cap
is being used. If it's supposed to represent the maximum a vault can hold, it's not very obvious how to enforce it on chain.
#2 - transferAndCall
2021-10-07T19:37:07Z
Caps aren't enforced in code. Since users can't deposit directly to strategies, it's up to the strategist to manage caps of strategies. We could likely refactor to remove caps altogether and save some gas.
#3 - GalloDaSballo
2021-10-13T23:38:18Z
Finding acknowledge, may want to remove / refactor that functionality
🌟 Selected for report: 0xsanson
46.5153 YAXIS - $181.41
0xsanson
In Controller.reorderStrategies
there's no check that _strategy1
and 2 are strategies of _vault
. If the strategist by mistake calls this function using a different strategy, index
evaluates as zero, effectively replacing the first vault's strategy with a non-appropriate one.
https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L215
editor
Consider adding require(_vaultStrategies[_strategy1] == _vault)
(idem for _strategy2
). These checks can also replace require(manager.allowedStrategies(_strategy1/2), "!_strategy1/2")
.
#0 - uN2RVw5q
2021-10-02T15:21:38Z
#1 - GalloDaSballo
2021-10-13T23:45:15Z
Sponsor mitigated
🌟 Selected for report: 0xsanson
46.5153 YAXIS - $181.41
0xsanson
In Vault.sol, the state variable totalDepositCap
is 'the maximum amount of value that can be deposited to the metavault at a time' according to the comment at L97.
The way it's used instead suggests a different meaning, i.e. 'the maximum amount of value that can be deposited to the metavault in total' (see L200).
https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Vault.sol#L97 https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Vault.sol#L97
editor
Check what's the intended behavior and either change the documentation or the usage in the deposit function.
#0 - GalloDaSballo
2021-10-13T23:39:17Z
Agree with finding and severity as documentation mismatches functionality
🌟 Selected for report: 0xsanson
46.5153 YAXIS - $181.41
0xsanson
In Controller.sol it's possible to change maxStrategies
to a different value. There's no check however that the new value is greater than the present number of strategies.
https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L316
editor
It's possible that this is an intended behaviour. If not, add a check for _maxStrategies >= _vaultDetails[_vault].strategies.length
.
#0 - Haz077
2021-09-29T17:38:15Z
In my opinion, that should be taken care of when resetting maxStrategies, also it will not cause any bug in removing/adding other strategies.
#1 - GalloDaSballo
2021-10-13T23:51:26Z
maxStrategies
ends up being usable exclusively to gatekeep further strategies from being added
That said the finding from the warden is correct, the variable could be set to a number lower than the current number of strategies
This has no impact on the system, beside offering incorrect information
I believe Low Risk is correct here
20.9319 YAXIS - $81.63
0xsanson
Functions Earn and Harvest of Harvester.sol work also when the manager is set to halted. Probably this is an unintented behaviour.
editor
Add notHalted modifier to aforementioned functions.
#0 - Haz077
2021-09-29T17:46:12Z
harvestStrategy
in Controller.sol and earn
in Vault.sol have notHalted
modifier which makes it unnecessary to add it again in Earn and Harvest of Harvester.sol
#1 - uN2RVw5q
2021-10-01T16:02:09Z
It seems that harvesters can be added using https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Harvester.sol#L150 and it allows any address to be a harvestor. So if an arbitrary address is added as a harvestor (that is, it is not a Controller
contract), then it would be able to call harvestStrategy
even when the contract is halted.
If this is possible, then adding the modifier is recommend. If addHarvestor
is always a Controller
contract, then consider documenting this and changing the tag to documentation.
#2 - uN2RVw5q
2021-10-03T15:08:34Z
I think this is a duplicate of https://github.com/code-423n4/2021-09-yaxis-findings/issues/10
#3 - GalloDaSballo
2021-10-13T23:53:15Z
Sponsor ended up mitigating and agreeing in substance
Duplicate of #10
4.064 YAXIS - $15.85
0xsanson
In StablesConverter.convert
there are multiple storage reads of tokens[i]
that add up gas. Consider saving the variable in memory.
editor
Rewrite
IERC20 _token; // add this for (uint8 i = 0; i < 3; i++) { _token = tokens[i]; // add this //if (_output == address(tokens[i])) { if (_output == address(_token)) { //uint256 _before = tokens[i].balanceOf(address(this)); uint256 _before = _token.balanceOf(address(this)); stableSwap3Pool.remove_liquidity_one_coin( _inputAmount, i, _estimatedOutput ); //uint256 _after = tokens[i].balanceOf(address(this)); uint256 _after = _token.balanceOf(address(this)); _outputAmount = _after.sub(_before); //tokens[i].safeTransfer(msg.sender, _outputAmount); _token.safeTransfer(msg.sender, _outputAmount); return _outputAmount; } }
#0 - uN2RVw5q
2021-10-02T15:26:53Z
Consider saving the variable in memory.
It should be stack instead of memory. But the suggestion is still correct.
#1 - uN2RVw5q
2021-10-02T15:34:15Z
Implemented in https://github.com/code-423n4/2021-09-yaxis/pull/31
#2 - GalloDaSballo
2021-10-12T22:14:49Z
Sponsor mitigated
Quoting from #56 Warm SLOADs cost 100 gas and CALLs cost 2600 gas after Berlin upgrade. MLOADs cost only 3 gas units.

🌟 Selected for report: 0xsanson
15.052 YAXIS - $58.70
0xsanson
The harvester can initiate the earn process by calling the Vault.earn
. At the end of this function an Earn(_token, _balance)
event is emitted. Before this, the execution is passed to the Controller.earn
function, which emits another event Earn(_token, _strategy)
.
Since these two events are emitted always together (Controller.earn
can be called only inside Vault.earn
), it's more efficient to emit a single event Earn(_token, _strategy, _amount)
at the end of Controller.earn
. This should gain a little gas (one less indexed data) and it's less confusing.
https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/controllers/Controller.sol#L437 https://github.com/code-423n4/2021-09-yaxis/blob/main/contracts/v3/Vault.sol#L157
editor
#0 - GalloDaSballo
2021-10-12T22:20:07Z
Using a single event simplifies the code and saves gas
Sponsor mitigated
🌟 Selected for report: 0xsanson
15.052 YAXIS - $58.70
0xsanson
During the _harvest
function in NativeStrategyCurve3Crv.sol, there's a call to _deposit()
only if (balanceOfWant() > 0)
. This if-statement can be removed since _deposit
calculates again balanceOfWant()
and makes the same check.
This way the function saves a .balanceOf
call.
editor
#0 - GalloDaSballo
2021-10-12T22:21:44Z
Redundant call to balanceOfWant()
Sponsor mitigated
🌟 Selected for report: 0xsanson
15.052 YAXIS - $58.70
0xsanson
In Harvester.sol, harvestNextStrategy
harvests the first strategy then shifts the array such that the second strategy becomes the first (so on future calls, all the strategy get harvested sequentially).
A more efficient way (in terms of gas) to do this would be to have a state variable uint256 lastStrategyHarvest
that starts at 0
and acts like an index for the last used strategy.
editor
harvestNextStrategy
would be rewritten like the following. Also removeStrategy
may need some modifications depending on index
.
function harvestNextStrategy( address _vault, uint256 _estimatedWETH, uint256 _estimatedYAXIS ) external { require(canHarvest(_vault), "!canHarvest"); uint256 k = strategies[_vault].addresses.length; if (lastStrategyHarvest >= k) { lastStrategyHarvest = 0; } address strategy = strategies[_vault].addresses[lastStrategyHarvest]; harvest(controller, strategy, _estimatedWETH, _estimatedYAXIS); lastStrategyHarvest = lastStrategyHarvest + 1; // solhint-disable-next-line not-rely-on-time strategies[_vault].lastCalled = block.timestamp; }
#0 - Haz077
2021-09-30T21:55:38Z
I like this solution, I think it's so better than shifting the array.
#1 - GalloDaSballo
2021-10-12T22:22:33Z
Sponsor acknowledged