Platform: Code4rena
Start Date: 31/01/2023
Pot Size: $90,500 USDC
Total HM: 47
Participants: 169
Period: 7 days
Judge: LSDan
Total Solo HM: 9
Id: 211
League: ETH
Rank: 4/169
Findings: 12
Award: $3,503.37
🌟 Selected for report: 2
🚀 Solo Findings: 1
🌟 Selected for report: waldenyan20
Also found by: 0xRobocop, KIntern_NA, Ruhum, cccz, hansfriese, mert_eren, minhtrng, peanuts
92.501 USDC - $92.50
MultiRewardStaking.changeRewardSpeed()
calculates the rewardsEndTimestamp
longer than real value.
As a result, rewards for every user will be inflated and some users won't claim their rewards because the contract doesn't have enough balance.
changeRewardSpeed()
is used to change the rewardsPerSecond
by admin.
function changeRewardSpeed(IERC20 rewardToken, uint160 rewardsPerSecond) external onlyOwner { ... uint256 remainder = rewardToken.balanceOf(address(this)); uint32 prevEndTime = rewards.rewardsEndTimestamp; uint32 rewardsEndTimestamp = _calcRewardsEnd( //@audit wrong endTime prevEndTime > block.timestamp ? prevEndTime : block.timestamp.safeCastTo32(), rewardsPerSecond, remainder ); rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; rewardInfos[rewardToken].rewardsEndTimestamp = rewardsEndTimestamp; }
Currently, it uses a remainder
amount(existing reward token balance inside the contract) for the calculation but this amount shouldn't be used as it contains an unclaimed rewards also.
As a result, rewardsEndTimestamp
will be calculated larger than it should and rewards for users will be accrued larger in _accrueStatic()
.
function _accrueStatic(RewardInfo memory rewards) internal view returns (uint256 accrued) { uint256 elapsed; if (rewards.rewardsEndTimestamp > block.timestamp) { elapsed = block.timestamp - rewards.lastUpdatedTimestamp; } else if (rewards.rewardsEndTimestamp > rewards.lastUpdatedTimestamp) { elapsed = rewards.rewardsEndTimestamp - rewards.lastUpdatedTimestamp; } accrued = uint256(rewards.rewardsPerSecond * elapsed); }
So the total amounts of rewards to pay will be more than the actual balance of reward token inside the contract and some users can't claim their rewards.
Manual Review
In changeRewardSpeed()
, we should calculate the new end time using an unpaid rewards only like below.
function changeRewardSpeed(IERC20 rewardToken, uint160 rewardsPerSecond) external onlyOwner { RewardInfo memory rewards = rewardInfos[rewardToken]; if (rewardsPerSecond == 0) revert ZeroAmount(); if (rewards.lastUpdatedTimestamp == 0) revert RewardTokenDoesntExist(rewardToken); if (rewards.rewardsPerSecond == 0) revert RewardsAreDynamic(rewardToken); _accrueRewards(rewardToken, _accrueStatic(rewards)); uint32 prevEndTime = rewards.rewardsEndTimestamp; uint32 rewardsEndTimestamp = _calcRewardsEnd( prevEndTime > block.timestamp ? prevEndTime : block.timestamp.safeCastTo32(), rewardsPerSecond, 0 //+++++++++++++++++++++++ ); rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; rewardInfos[rewardToken].rewardsEndTimestamp = rewardsEndTimestamp; }
#0 - c4-judge
2023-02-16T03:56:35Z
dmvt marked the issue as duplicate of #156
#1 - c4-sponsor
2023-02-18T11:59:16Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T15:39:01Z
dmvt marked the issue as not a duplicate
#3 - c4-judge
2023-02-23T15:39:16Z
dmvt marked the issue as duplicate of #191
#4 - c4-judge
2023-02-23T22:51:58Z
dmvt marked the issue as partial-50
#5 - c4-judge
2023-02-25T14:48:43Z
dmvt changed the severity to 3 (High Risk)
🌟 Selected for report: 0xdeadbeef0x
Also found by: 0Kage, 0xNazgul, 0xRobocop, Aymen0909, KIntern_NA, Kenshin, KingNFT, Krace, Kumpa, SadBase, aashar, apvlki, btk, cccz, critical-or-high, eccentricexit, fs0c, gjaldon, hansfriese, immeas, mert_eren, mgf15, mrpathfindr, orion, peanuts, rvi0x, rvierdiiev, supernova, ulqiorra, waldenyan20, y1cunhui
4.6115 USDC - $4.61
Users would claim rewards several times if the reward token is an ERC777 one.
MultiRewardStaking.claimRewards()
resets accruedRewards
after transferring funds.
function claimRewards(address user, IERC20[] memory _rewardTokens) external accrueRewards(msg.sender, user) { for (uint8 i; i < _rewardTokens.length; i++) { uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]); EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]]; if (escrowInfo.escrowPercentage > 0) { _lockToken(user, _rewardTokens[i], rewardAmount, escrowInfo); //@audit reentrancy emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, true); } else { _rewardTokens[i].transfer(user, rewardAmount); //@audit reentrancy emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, false); } accruedRewards[user][_rewardTokens[i]] = 0; } }
So users can call this function several times if the reward token has a hook.
In this case, users can drain all rewards from the protocol.
Manual Review
We should modify like the below.
function claimRewards(address user, IERC20[] memory _rewardTokens) external accrueRewards(msg.sender, user) { for (uint8 i; i < _rewardTokens.length; i++) { uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]); accruedRewards[user][_rewardTokens[i]] = 0; //++++++++++++++++++++++ EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]]; if (escrowInfo.escrowPercentage > 0) { _lockToken(user, _rewardTokens[i], rewardAmount, escrowInfo); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, true); } else { _rewardTokens[i].transfer(user, rewardAmount); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, false); } } }
#0 - c4-judge
2023-02-16T07:40:02Z
dmvt marked the issue as duplicate of #54
#1 - c4-sponsor
2023-02-18T12:11:12Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:48:26Z
dmvt marked the issue as partial-50
#3 - c4-judge
2023-02-23T01:09:09Z
dmvt changed the severity to 3 (High Risk)
#4 - c4-judge
2023-03-01T00:39:12Z
dmvt marked the issue as satisfactory
🌟 Selected for report: waldenyan20
Also found by: KingNFT, hansfriese, ulqiorra
704.9309 USDC - $704.93
MultiRewardStaking._withdraw()
doesn't track the user's rewards correctly when caller != owner
.
As a result, users will lose their rewards when they withdraw their balances using other accounts.
MultiRewardStaking._withdraw()
tracks the rewards using an accrueRewards
modifier.
function _withdraw( address caller, address receiver, address owner, uint256 assets, uint256 shares ) internal override accrueRewards(caller, receiver) { //@audit should update owner instead of caller if (caller != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); _burn(owner, shares); IERC20(asset()).safeTransfer(receiver, assets); emit Withdraw(caller, receiver, owner, assets, shares); }
But the accrueRewards
modifier updates a caller
instead of owner
and the below scenario would be possible.
caller(another account)
and receiver
were updated but not his original account.function _accrueUser(address _user, IERC20 _rewardToken) internal { ... uint256 deltaIndex = rewards.index - oldIndex; // Accumulate rewards by multiplying user tokens by rewardsPerToken index and adding on unclaimed uint256 supplierDelta = balanceOf(_user).mulDiv(deltaIndex, uint256(rewards.ONE), Math.Rounding.Down); //@audit 0 balance userIndex[_user][_rewardToken] = rewards.index; accruedRewards[_user][_rewardToken] += supplierDelta; }
As a result, he receives 0 rewards although he staked 100 shares for 1 month.
Manual Review
We should modify like the below.
function _withdraw( address caller, address receiver, address owner, uint256 assets, uint256 shares ) internal override accrueRewards(owner, receiver) { //+++++++++++++++++ if (caller != owner) _approve(owner, msg.sender, allowance(owner, msg.sender) - shares); _burn(owner, shares); IERC20(asset()).safeTransfer(receiver, assets); emit Withdraw(caller, receiver, owner, assets, shares); }
#0 - c4-judge
2023-02-16T06:49:29Z
dmvt marked the issue as duplicate of #385
#1 - c4-sponsor
2023-02-18T12:06:39Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-sponsor
2023-02-18T12:06:43Z
RedVeil marked the issue as disagree with severity
#3 - c4-judge
2023-02-28T17:30:14Z
dmvt marked the issue as satisfactory
🌟 Selected for report: hansfriese
Also found by: rbserver
678.8223 USDC - $678.82
Total assets of yearn vault are not correct so the calculation between the asset and the shares will be wrong.
In YearnAdapter
the total assets of current yValut
are extracted using _yTotalAssets
.
function _yTotalAssets() internal view returns (uint256) { return IERC20(asset()).balanceOf(address(yVault)) + yVault.totalDebt(); //@audit }
But in the yearn vault implementation, self.totalIdle
is used instead of current balance.
def _totalAssets() -> uint256: # See note on `totalAssets()`. return self.totalIdle + self.totalDebt
In yearn valut, totalIdle
is the tracked value of tokens, so it is same as vault's balance in most cases, but the balance can be larger due to an attack or other's fault. Even sweep
is implemented for the case in the vault implementation.
if token == self.token.address: value = self.token.balanceOf(self) - self.totalIdle log Sweep(token, value) self.erc20_safe_transfer(token, self.governance, value)
So the result of _yTotalAssets
can be inflated than the correct total assets and the calculation between the asset and the shares will be incorrect.
Manual Review
Use yVault.totalIdle
instead of balance.
#0 - c4-judge
2023-02-16T07:52:32Z
dmvt marked the issue as duplicate of #578
#1 - c4-sponsor
2023-02-18T12:13:16Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-25T23:39:24Z
dmvt marked the issue as selected for report
🌟 Selected for report: hansfriese
1508.4941 USDC - $1,508.49
The raw rewardsPerSecond
might be too big for some ERC20 tokens and it wouldn't work as intended.
As we can see from _accrueStatic()
, the rewardsPerSecond
is a raw amount without any multiplier.
function _accrueStatic(RewardInfo memory rewards) internal view returns (uint256 accrued) { uint256 elapsed; if (rewards.rewardsEndTimestamp > block.timestamp) { elapsed = block.timestamp - rewards.lastUpdatedTimestamp; } else if (rewards.rewardsEndTimestamp > rewards.lastUpdatedTimestamp) { elapsed = rewards.rewardsEndTimestamp - rewards.lastUpdatedTimestamp; } accrued = uint256(rewards.rewardsPerSecond * elapsed); }
But 1 wei for 1 second would be too big amount for some popular tokens like WBTC(8 decimals) and EURS(2 decimals).
For WBTC, it will be 0.31536 WBTC per year (worth about $7,200
) to meet this requirement, and for EURS, it must be at least 315,360 EURS per year (worth about $315,000
).
Such amounts might be too big as rewards and the protocol wouldn't work properly for such tokens.
Manual Review
Recommend introducing a RATE_DECIMALS_MULTIPLIER = 10**9(example)
to increase the precision of rewardsPerSecond
instead of using the raw amount.
#0 - c4-sponsor
2023-02-18T14:02:45Z
RedVeil marked the issue as sponsor confirmed
#1 - c4-judge
2023-02-25T23:40:11Z
dmvt marked the issue as selected for report
🌟 Selected for report: rbserver
Also found by: 0xjuicer, hansfriese, ladboy233
211.4793 USDC - $211.48
YearnAdapter.pause
can revert and pause will not work in an emergency
When YearnAdapter
should be paused, all funds are withdrawn first and then _pause
is called.
function pause() external onlyOwner { _protocolWithdraw(totalAssets(), totalSupply()); // Update the underlying balance to prevent inflation attacks underlyingBalance = 0; _pause(); }
YearnAdapter._protocolWithdraw
calls yVault.withdraw
directly with asset amount only. But in the yValut.withdraw there is another maxLoss parameter.
def withdraw( maxShares: uint256 = MAX_UINT256, recipient: address = msg.sender, maxLoss: uint256 = 1, # 0.01% [BPS] ) -> uint256:
pause
can be called in an emergency and this withdraw amount can be huge. In this case yearn vault balance is not enough and it needs withdraw from strategies in withdraw queue.
if value > vault_balance: ... for strategy in self.withdrawalQueue: ... loss: uint256 = Strategy(strategy).withdraw(amountNeeded) ... assert totalLoss <= maxLoss * (value + totalLoss) / MAX_BPS
After it withdraws from strategies, it checks totalLoss
using maxLoss
. This withdraw can revert for the default maxLoss
parameter and it will prevent pause in emergency.
Manual Review
Add maxLoss
parameter in YearnAdapter._protocolWithdraw
in line 171
#0 - c4-judge
2023-02-16T04:36:33Z
dmvt marked the issue as duplicate of #23
#1 - c4-sponsor
2023-02-18T12:01:50Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:12:07Z
dmvt marked the issue as satisfactory
🌟 Selected for report: imare
Also found by: 7siech, Ch_301, Hawkeye, KIntern_NA, Malatrax, Ruhum, Walter, bin2chen, eccentricexit, hansfriese, ladboy233, peakbolt, rbserver, rvierdiiev, thecatking
14.932 USDC - $14.93
lastHarvest
is not updated so the cooldown logic does not work.
Cooldown functionality is implemented in AdapterBase.harvest
. harvest
can't be run after cooltime from the last harvest time.
function harvest() public takeFees { if ( address(strategy) != address(0) && ((lastHarvest + harvestCooldown) < block.timestamp) ) {
The cooldown check is correct but lastHarvest
is not updated after harvest in the harvest
function implementation. As a result lastHarvest
is not changed from the initial value so the cooldown logic does not work.
Manual Review
Set lastHarvest = block.timestamp
after harvest()
is called by delegatecall on line 444.
#0 - c4-judge
2023-02-16T04:26:28Z
dmvt marked the issue as duplicate of #199
#1 - c4-sponsor
2023-02-18T12:01:08Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T22:29:28Z
dmvt marked the issue as satisfactory
🌟 Selected for report: rbserver
Also found by: bin2chen, cccz, chaduke, eccentricexit, hansfriese, ustas
44.0481 USDC - $44.05
This modifier will affect the fee calculation.
Vault.redeem
don't have syncFeeCheckpoint
modifier. So highWaterMark
is not updated when redeem
is called.
modifier syncFeeCheckpoint() { _; highWaterMark = convertToAssets(1e18); }
And accruedPerformanceFee
will be wrong as a result.
Manual Review
Add syncFeeCheckpoint
modifier in Vault.redeem
#0 - c4-judge
2023-02-16T02:59:33Z
dmvt marked the issue as duplicate of #118
#1 - c4-sponsor
2023-02-18T11:52:23Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T11:28:11Z
dmvt marked the issue as partial-50
🌟 Selected for report: Lirios
Also found by: KIntern_NA, csanuragjain, hansfriese, ladboy233, thecatking
114.1988 USDC - $114.20
A malicious user can make the Vault
pay the fees unnecessarily using Vault.changeAdapter()
.
The main reason for this issue is that any users can call changeAdapter()
again and again and make the vault pay fees unnecessarily.
The admin can change the adapter using proposeAdapter()
and changeAdapter()
.
function proposeAdapter(IERC4626 newAdapter) external onlyOwner { if (newAdapter.asset() != address(asset)) revert VaultAssetMismatchNewAdapterAsset(); proposedAdapter = newAdapter; proposedAdapterTime = block.timestamp; emit NewAdapterProposed(newAdapter, block.timestamp); } /** * @notice Set a new Adapter for this Vault after the quit period has passed. * @dev This migration function will remove all assets from the old Vault and move them into the new vault * @dev Additionally it will zero old allowances and set new ones * @dev Last we update HWM and assetsCheckpoint for fees to make sure they adjust to the new adapter */ function changeAdapter() external takeFees { //@audit lose fee again by anyone if (block.timestamp < proposedAdapterTime + quitPeriod) revert NotPassedQuitPeriod(quitPeriod); adapter.redeem( adapter.balanceOf(address(this)), address(this), address(this) ); asset.approve(address(adapter), 0); emit ChangedAdapter(adapter, proposedAdapter); adapter = proposedAdapter; asset.approve(address(adapter), type(uint256).max); adapter.deposit(asset.balanceOf(address(this)), address(this)); }
In changeAdapter()
, it redeems all amounts from the old adapter and deposits to the new adapter.
And as we can see from BeefyAdapter.previewRedeem()
, the adapters charge some fees during deposit/redeem.
function previewRedeem(uint256 shares) public view override returns (uint256) { uint256 assets = _convertToAssets(shares, Math.Rounding.Down); IBeefyStrat strat = IBeefyStrat(beefyVault.strategy()); uint256 beefyFee = strat.withdrawalFee(); if (beefyFee > 0) assets -= assets.mulDiv( beefyFee, BPS_DENOMINATOR, Math.Rounding.Up ); return assets; }
So the below scenario would be possible.
Vault.sol
, the admin proposed a new adapter and changed it after quitPeriod
.changeAdapter()
again.proposedAdapter
and proposedAdapterTime
weren't reset after the admin call and it will work properly.The malicious user can call this function as many times as he likes and it should be fixed even though there is no benefit to the attacker.
Manual Review
We should reset proposedAdapter
after calling changeAdapter()
and make the function revert if proposedAdapter = address(0)
.
function changeAdapter() external takeFees { if (block.timestamp < proposedAdapterTime + quitPeriod || proposedAdapter == address(0)) //++++++++++++++++ revert NotPassedQuitPeriod(quitPeriod); adapter.redeem( adapter.balanceOf(address(this)), address(this), address(this) ); asset.approve(address(adapter), 0); emit ChangedAdapter(adapter, proposedAdapter); adapter = proposedAdapter; proposedAdapter = address(0); //+++++++++++++++++++++ asset.approve(address(adapter), type(uint256).max); adapter.deposit(asset.balanceOf(address(this)), address(this)); }
#0 - c4-judge
2023-02-16T07:18:27Z
dmvt marked the issue as duplicate of #441
#1 - c4-sponsor
2023-02-18T12:07:46Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-28T17:31:01Z
dmvt marked the issue as satisfactory
🌟 Selected for report: gjaldon
Also found by: 0x52, Aymen0909, KIntern_NA, hansfriese, rvierdiiev
57.0994 USDC - $57.10
MultiRewardStaking.claimRewards()
will revert when escrowPercentage = 100%
and the reward token is a Revert on Zero Value Transfers
one.
In this case, users can't claim their rewards.
When users claim their rewards, claimRewards
function locks some percent in an escrow.
function claimRewards(address user, IERC20[] memory _rewardTokens) external accrueRewards(msg.sender, user) { for (uint8 i; i < _rewardTokens.length; i++) { uint256 rewardAmount = accruedRewards[user][_rewardTokens[i]]; if (rewardAmount == 0) revert ZeroRewards(_rewardTokens[i]); EscrowInfo memory escrowInfo = escrowInfos[_rewardTokens[i]]; if (escrowInfo.escrowPercentage > 0) { _lockToken(user, _rewardTokens[i], rewardAmount, escrowInfo); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, true); } else { _rewardTokens[i].transfer(user, rewardAmount); emit RewardsClaimed(user, _rewardTokens[i], rewardAmount, false); } accruedRewards[user][_rewardTokens[i]] = 0; } }
And _lockToken()
transfers the remaining amount to the user after locking a escrowPercentage
of rewards.
function _lockToken( address user, IERC20 rewardToken, uint256 rewardAmount, EscrowInfo memory escrowInfo ) internal { uint256 escrowed = rewardAmount.mulDiv(uint256(escrowInfo.escrowPercentage), 1e18, Math.Rounding.Down); uint256 payout = rewardAmount - escrowed; rewardToken.safeTransfer(user, payout); //@audit 0 transfer escrow.lock(rewardToken, user, escrowed, escrowInfo.escrowDuration, escrowInfo.offset); }
But there is no requirement of escrowInfo.escrowPercentage < 100%
and payout
will be 0 when the escrowPercentage
is 100%.
In this case, this function will revert if the reward token is a Revert on Zero Value Transfers and users can't claim their rewards.
Manual Review
We should modify _lockToken()
like below.
function _lockToken( address user, IERC20 rewardToken, uint256 rewardAmount, EscrowInfo memory escrowInfo ) internal { uint256 escrowed = rewardAmount.mulDiv(uint256(escrowInfo.escrowPercentage), 1e18, Math.Rounding.Down); uint256 payout = rewardAmount - escrowed; if(payout > 0) { //+++++++++++++++++++++ rewardToken.safeTransfer(user, payout); } escrow.lock(rewardToken, user, escrowed, escrowInfo.escrowDuration, escrowInfo.offset); }
#0 - c4-judge
2023-02-16T05:48:16Z
dmvt marked the issue as duplicate of #251
#1 - c4-sponsor
2023-02-18T12:03:06Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T21:04:11Z
dmvt changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-02-23T22:33:31Z
dmvt marked the issue as satisfactory
#4 - c4-judge
2023-02-23T23:20:33Z
This previously downgraded issue has been upgraded by captainmangoC4
#5 - c4-judge
2023-02-25T15:55:50Z
dmvt marked the issue as partial-50
🌟 Selected for report: rvierdiiev
Also found by: Lirios, Ruhum, ayeslick, bin2chen, critical-or-high, hansfriese, hashminer0725, immeas, jasonxiale, mookimgo
36.7818 USDC - $36.78
Vault.fees
can be set to 0 by any user.
The main reason for this issue is that changeFees()
works properly when proposedFeeTime = 0
.
The fees
can be changed by admin using proposeFees()
and changeFees()
.
function proposeFees(VaultFees calldata newFees) external onlyOwner { if ( newFees.deposit >= 1e18 || newFees.withdrawal >= 1e18 || newFees.management >= 1e18 || newFees.performance >= 1e18 ) revert InvalidVaultFees(); proposedFees = newFees; proposedFeeTime = block.timestamp; emit NewFeesProposed(newFees, block.timestamp); } /// @notice Change fees to the previously proposed fees after the quit period has passed. function changeFees() external { //@audit can set fees = 0 if (block.timestamp < proposedFeeTime + quitPeriod) revert NotPassedQuitPeriod(quitPeriod); emit ChangedFees(fees, proposedFees); fees = proposedFees; }
And changeFees()
can be called by anyone even though the admin didn't propose new fee settings.
So users can set fees = 0
(empty struct) by calling the function and the protocol can't charge fees for a while(at least 1 day because quitPeriod
>= 1 days).
Manual Review
We should modify changeFees()
to revert when proposedFeeTime = 0
.
function changeFees() external { if (block.timestamp < proposedFeeTime + quitPeriod || proposedFeeTime == 0) //+++++++++++++++ revert NotPassedQuitPeriod(quitPeriod); emit ChangedFees(fees, proposedFees); fees = proposedFees; }
#0 - c4-judge
2023-02-16T08:09:36Z
dmvt marked the issue as duplicate of #78
#1 - c4-sponsor
2023-02-18T12:16:44Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:25:43Z
dmvt marked the issue as satisfactory
🌟 Selected for report: IllIllI
Also found by: 0x3b, 0xAgro, 0xBeirao, 0xMirce, 0xNineDec, 0xRobocop, 0xSmartContract, 0xTraub, 0xWeiss, 2997ms, 41i3xn, Awesome, Aymen0909, Bauer, Bnke0x0, Breeje, Cryptor, DadeKuma, Deathstore, Deekshith99, DevABDee, DevTimSch, Dewaxindo, Diana, Ermaniwe, Guild_3, H0, IceBear, Inspectah, JDeryl, Kaiziron, Kaysoft, Kenshin, Mukund, Praise, RaymondFam, Rickard, Rolezn, Ruhum, Sathish9098, SkyWalkerMan, SleepingBugs, UdarTeam, Udsen, Walter, aashar, adeolu, apvlki, arialblack14, ast3ros, btk, chaduke, chandkommanaboyina, chrisdior4, climber2002, codetilda, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, descharre, dharma09, doublesharp, eccentricexit, ethernomad, fs0c, georgits, halden, hansfriese, hashminer0725, immeas, lukris02, luxartvinsec, matrix_0wl, merlin, mookimgo, mrpathfindr, nadin, olegthegoat, pavankv, rbserver, rebase, savi0ur, sayan, scokaf, seeu, shark, simon135, tnevler, tsvetanovv, ulqiorra, ustas, waldenyan20, y1cunhui, yongskiws, yosuke
35.4779 USDC - $35.48
externalRegistry
is not validated in YearnAdapter.initializeyVault = VaultAPI(IYearnRegistry(externalRegistry).latestVault(_asset)); //@audit
When YearnAdapter
is initialized, yVault
is from externalRegistry
. But there is no endorsement check about externalRegistry
. So the creator of the adapter can send an invalid Yearn registry. So it is recommended to validate endorsement of externalRegistry
to prevent this.
MultiRewardEscrow.lock
can revert for small fee percentsuint256 feePerc = fees[token].feePerc; if (feePerc > 0) { uint256 fee = Math.mulDiv(amount, feePerc, 1e18); amount -= fee; token.safeTransfer(feeRecipient, fee); }
When the fee percent is very small, amount * feePerc
can be less than 1e18
and fee
will be 0. The transfer of zero amount will revert for some tokens and this will cause the revert of MultiRewardEscrow.lock
.
It is recommended to use another if conditional before the safeTransfer.
VaultRegistry.getSubmitter
is wrongVaultRegistry.getSubmitter
returns VaultMetadata
in the implementation.
function getSubmitter(address vault) external view returns (VaultMetadata memory) {
But it should return address as we can see in the interface.
function getSubmitter(address vault) external view returns (address);
I think the return type and the implementation of VaultRegistry.getSubmitter
are not correct. getSubmitter
is not used in the repository so I can not check anymore.
MultiRewardStaking.addRewardToken
will revert for decimals > 19
in the following line.
uint64 ONE = (10**IERC20Metadata(address(rewardToken)).decimals()).safeCastTo64();
So we can't use those tokens as reward tokens.
Mismatch is not correct here.
if (length1 != length2) revert ArrayLengthMissmatch();
There is an open TODO for repository for an audit.
#0 - c4-judge
2023-02-28T17:36:44Z
dmvt marked the issue as grade-b