Platform: Code4rena
Start Date: 15/06/2022
Pot Size: $30,000 USDC
Total HM: 5
Participants: 55
Period: 3 days
Judge: Jack the Pug
Id: 138
League: ETH
Rank: 4/55
Findings: 3
Award: $2,059.54
π Selected for report: 1
π Solo Findings: 0
π Selected for report: Picodes
Also found by: 0x1f8b, 0x52, Chom, GimelSec, IllIllI, berndartmueller, cccz, defsec, georgypetrov, hyh, kenzo, minhquanym, oyc_109, scaraven, unforgiven
50.7077 USDC - $50.71
https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L249 https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L259 https://github.com/Badger-Finance/vested-aura/blob/d504684e4f9b56660a9e6c6dfb839dcebac3c174/contracts/MyStrategy.sol#L275
Autocompounding auraBAL
rewards into AURA
requires multiple swaps (auraBAL -> BAL/ETH BPT -> WETH -> AURA
) within MyStrategy._harvest
.
The swaps are at risk of being front-run / sandwiched, resulting in a loss of funds.
Since MEV is very prominent I think the chance of that happening is pretty high.
uint256 balEthBptEarned = BALANCER_VAULT.swap(singleSwap, fundManagement, 0, type(uint256).max);
IBalancerVault.ExitPoolRequest memory exitPoolRequest = IBalancerVault.ExitPoolRequest({ assets: assets, minAmountsOut: new uint256[](2), // @audit-info missing slippage protection userData: abi.encode(ExitKind.EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, balEthBptEarned, BPT_WETH_INDEX), toInternalBalance: false });
harvested[0].amount = BALANCER_VAULT.swap(singleSwap, fundManagement, 0, type(uint256).max);
Manual review
Consider adding slippage checks during swapping.
See https://dev.balancer.fi/resources/joins-and-exits/pool-exits#minamountsout for slippage protection info.
#0 - GalloDaSballo
2022-06-18T15:46:59Z
We use private harvest, the recommended mitigation is ineffective <img width="803" alt="Screenshot 2022-06-18 at 17 46 49" src="https://user-images.githubusercontent.com/13383782/174446191-15ea3b5f-7c49-48d5-a87e-a1c8c71bda41.png">
#1 - KenzoAgada
2022-06-22T10:29:21Z
Duplicate of #155
π Selected for report: scaraven
Also found by: GimelSec, berndartmueller, cccz, dipp, kenzo, kirk-baird, unforgiven
235.5937 USDC - $235.59
Twice the amount of BADGER
tokens are sent to badgerTree
within the MyStrategy._sendBadgerToTree
function.
Due to insufficient BADGER
tokens, this will break the following functionalities:
MyStrategy.sweepRewardToken
(L107-L113)
function sweepRewardToken(address token) public nonReentrant { _onlyGovernanceOrStrategist(); _onlyNotProtectedTokens(token); uint256 toSend = IERC20Upgradeable(token).balanceOf(address(this)); _handleRewardTransfer(token, toSend); // @audit-info reverts for BADGER tokens due to sending twice the amount of tokens which are available }
MyStrategy.claimBribesFromHiddenHand
(L328-L334)
... else { uint256 difference = IERC20Upgradeable(token).balanceOf(address(this)).sub(beforeBalance[i]); if (difference > 0) { nonZeroDiff = true; _handleRewardTransfer(token, difference); // @audit-info reverts for BADGER tokens due to sending twice the amount of tokens which are available } } ...
function _sendBadgerToTree(uint256 amount) internal { IERC20Upgradeable(BADGER).safeTransfer(BADGER_TREE, amount); _processExtraToken(address(BADGER), amount); // @audit-info BADGER tokens are sent to vault }
Manual review
The intended behavior is to send BADGER
tokens to the badger tree. This is done within BaseStrategy._processExtraToken
, therefore it is not necessary to transfer BADGER
tokens to BADGER_TREE
.
/// Calls `Vault.reportAdditionalToken` to process fees and forward amount to badgerTree to be emitted.
Remove IERC20Upgradeable(BADGER).safeTransfer(BADGER_TREE, amount);
from the MyStrategy._sendBadgerToTree
function on L429.
#0 - GalloDaSballo
2022-06-18T15:46:04Z
Dup of #2
π Selected for report: berndartmueller
Also found by: minhquanym
1773.2447 USDC - $1,773.24
All funds can be migrated (withdrawn) at once to the caller vault by using the BaseStrategy.withdrawToVault
function which internally calls MyStrategy._withdrawAll
.
The latter function has the following check in place:
require( balanceOfPool() == 0 && LOCKER.balanceOf(address(this)) == 0, "You have to wait for unlock or have to manually rebalance out of it" );
Funds can only be withdrawn (migrated) if the balance in LOCKER
is fully unlocked.
By locking a small amount of want tokens via AuraLocker.lock
with the strategy
address, a malicious individual can cause DoS and prevent withdrawing and migrating funds to the vault.
The following test case will replicate the DoS attack by locking "dust" want tokens for the strategy
address. This causes vault.withdrawToVault
to revert.
def test_frontrun_migration(locker, deployer, vault, strategy, want, governance, keeper): # Setup randomUser = accounts[6] snap = SnapshotManager(vault, strategy, "StrategySnapshot") startingBalance = want.balanceOf(deployer) depositAmount = startingBalance // 2 assert startingBalance >= depositAmount # End Setup # Deposit want.approve(vault, MaxUint256, {"from": deployer}) snap.settDeposit(depositAmount, {"from": deployer}) chain.sleep(15) chain.mine() vault.earn({"from": keeper}) chain.snapshot() # Test no harvests chain.sleep(86400 * 250) ## Wait 250 days so we can withdraw later chain.mine() before = {"settWant": want.balanceOf(vault), "stratWant": strategy.balanceOf()} strategy.prepareWithdrawAll({"from": governance}) want.approve(locker, 1, {"from": deployer}) locker.lock(strategy, 1, { "from": deployer }) # Donate "dust" want tokens to strategy vault.withdrawToVault({"from": governance}) # @audit-info reverts with "You have to wait for unlock or have to manually rebalance" after = {"settWant": want.balanceOf(vault), "stratWant": strategy.balanceOf()} assert after["settWant"] > before["settWant"] assert after["stratWant"] < before["stratWant"] assert after["stratWant"] == 0
Manual review
Call LOCKER.processExpiredLocks(false);
in MyStrategy._withdrawAll
directly and remove the check which enforces unlocking all want tokens on L184-L187.
#0 - GalloDaSballo
2022-06-18T15:45:46Z
I have to agree with the evidence that _withdrawAll
will be ineffective.
The implications are that no strategy migration is possible for this set of Vault <-> Strategy as even 1 wei would cause the setStrategy
to fail.
In terms of impact, ultimately the warden didn't show how withdrawals would be denied nor broken, end users can always withdraw via _withdraw
meaning that the vault would still allow user withdrawals but governance would be unable to move tokens away from the strategy.
#1 - GalloDaSballo
2022-06-18T16:00:58Z
I have to correct myself, we actually have a way to send all tokens back to the vault in manualSendAuraToVault
I want to commend the warden for finding an interesting find, however I believe impact is further reduced as long as we accept that the strategy will not be changeable
#2 - GalloDaSballo
2022-07-13T22:34:56Z
Acknowledged, I believe this effectively means that we won't be able to replace the locking strategy. In the future we may end up using lockingProxies (separate contract just for locking) although that may create further trust issues.
End users can still withdraw their tokens at any time, however the finding confirms that if we ever want to do a "bveAURA V2", we'll need to deploy a new Vault