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: 1/169
Findings: 15
Award: $8,268.35
π Selected for report: 5
π Solo Findings: 1
π Selected for report: bin2chen
Also found by: 0xTraub, Ch_301, rvierdiiev
704.9309 USDC - $704.93
Malicious beefy booster can be provided to BeefyAdapter that can steal funds, as there is no check that it's endorsed in registry.
When user deploys BeefyAdapter
he can provide beefyVault and beefyBooster addresses that will be used by contract.
Later beefyVault is checked by template registry in order if it's endorsed. This is done to prevent providing malicious contract that can steal funds.
However, beefyBooster is not checked and this allows user to deploy adapter with malicious contract as beefyBooster which can steal user funds. And once beefyBooster is provided then all moo token that are controlled by adapter after deposit to vault will be send to malicious beefyBooster contract.
VsCode
Add check for the beefyBooster contract if it's endorsed.
#0 - c4-judge
2023-02-16T08:14:07Z
dmvt marked the issue as duplicate of #89
#1 - c4-sponsor
2023-02-18T12:17:40Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T01:19:41Z
dmvt marked the issue as unsatisfactory: Insufficient quality
#3 - rvierdiyev
2023-03-02T15:23:49Z
@dmvt Could you please explain which quality improvements you would like to see here?
This is super simple bug and it's explained in this way. beefyBooster
param is input that is controlled by caller(deployer of adapter). This param(address) is not checked to be endorsed by system. Because of that attacker can provide malicious address and steal depositors funds. Also i have described impact of the bug.
it explains same things as #552
i would like you to look again on this one
also, pls note, that this issue was disscussed with sponsor and he confirmed this finding.
#4 - c4-judge
2023-03-03T20:24:56Z
dmvt marked the issue as satisfactory
#5 - dmvt
2023-03-03T20:27:44Z
Agreed after second review. This report could be written better, and it would be nice if you had included a walkthrough of a real world situation in the proof of concept section, but the simplicity of the bug and its dramatic impact is clear. I've marked both this one and #89 satisfactory.
#6 - rvierdiyev
2023-03-03T20:33:03Z
thanks @dmvt
π 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
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L170-L188 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L413-L432
User can drain all erc777 token balance of MultiRewardStaking using reentrancy in claimRewards function.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L170-L188
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; } }
This function allow any user to receive collected rewards. It first call accrueRewards
modifier, which will update user's reward amount.
Then it's looping by each reward token and sends amount to the caller in case when no escrow is used.
The problem is that accruedRewards[user][_rewardTokens[i]]
amount is reset only in the end of loop.
That allows user to drain all balance of reward token by reentrancy attack in case if erc777 token is used as reward token.
Example.
1.Reward token is erc777 token.
2.User calls claimRewards
from the contract which has hook.
3.In that hook user calls claimRewards
again and again while MultiRewardStaking contract transfers all balance to attacker.
4.As result attacker received not his portion of rewards only, but all balance of MultiRewardStaking contract.
VsCode
Update rewards amount at the beginning of function.
#0 - c4-judge
2023-02-16T07:38:23Z
dmvt marked the issue as duplicate of #54
#1 - c4-sponsor
2023-02-18T12:10:46Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:53:13Z
dmvt marked the issue as partial-50
#3 - c4-judge
2023-02-23T01:10:38Z
dmvt changed the severity to 3 (High Risk)
#4 - c4-judge
2023-03-01T00:29:17Z
dmvt marked the issue as full credit
#5 - c4-judge
2023-03-01T00:54:03Z
dmvt marked the issue as satisfactory
π Selected for report: rvierdiiev
Also found by: bin2chen
2262.7411 USDC - $2,262.74
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L605-L615 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L575
Anyone who uses same adapter have ability to pause it. As result you have ability to pause any vault by creating your vault with same adapter.
When user creates vault he has ability to deploy new adapter or reuse already created adapter.
VaultController gives ability to pause adapter. https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L605-L615
function pauseAdapters(address[] calldata vaults) external { uint8 len = uint8(vaults.length); for (uint256 i = 0; i < len; i++) { _verifyCreatorOrOwner(vaults[i]); (bool success, bytes memory returnData) = adminProxy.execute( IVault(vaults[i]).adapter(), abi.encodeWithSelector(IPausable.pause.selector) ); if (!success) revert UnderlyingError(returnData); } }
As you can see _verifyCreatorOrOwner
is used to determine if msg.sender can pause adapter.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L667-L670
function _verifyCreatorOrOwner(address vault) internal returns (VaultMetadata memory metadata) { metadata = vaultRegistry.getVault(vault); if (msg.sender != metadata.creator || msg.sender != owner) revert NotSubmitterNorOwner(msg.sender); }
So in case if you are creator of vault that uses adaptor that you want to pause, then you are able to pause it.
This is how it can be used in order to stop the vault, i don't like. 1.Someone created vault that uses adapterA. 2.Attacker creates own vault and set adapterA as well. 3.Now attacker is able to pause adapterA and as result it's not possible to deposit anymore. Also vault is not earning fees now, as pausing withdraws all from strategy. 4.And it can pause it as many times as he wants(in case if someone else will try to unpause it).
So this attack allows to stop all vaults that use same adapter from earning yields.
VsCode
I think that it's better to create a clone of adapter for the vault, so each vault has separate adaptor.
#0 - c4-judge
2023-02-16T06:06:18Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2023-02-17T13:29:20Z
RedVeil marked the issue as sponsor acknowledged
#2 - c4-sponsor
2023-02-18T12:04:48Z
RedVeil marked the issue as disagree with severity
#3 - c4-judge
2023-02-23T21:42:41Z
dmvt marked the issue as selected for report
π Selected for report: rvierdiiev
Also found by: peakbolt
2262.7411 USDC - $2,262.74
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L138 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L215 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L480-L499 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L447-L460
Protocol loses fees because highWaterMark is updated every time someone deposit, withdraw, mint.
This bug is related to the fees accruing design. It was disscussed with the sponsor in order to understand how it should work.
Protocol has such thing as performance fee. Actually this is fee from accrued yields. If user deposited X assets and after some time he can withdraw X+Y assets for that minted amount of shares, that means that startegy has earned some Y amount of yields. Then protocol is able to get some part of that Y amount as a performance fee.
takeFees
modifier is responsible for taking fees.
It calls accruedPerformanceFee
function to calculate fees amount.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L447-L460
function accruedPerformanceFee() public view returns (uint256) { uint256 highWaterMark_ = highWaterMark; uint256 shareValue = convertToAssets(1e18); uint256 performanceFee = fees.performance; return performanceFee > 0 && shareValue > highWaterMark ? performanceFee.mulDiv( (shareValue - highWaterMark) * totalSupply(), 1e36, Math.Rounding.Down ) : 0; }
As you can see, protocol has such variable as highWaterMark
. This variable actually should store convertToAssets(1e18)
amount at the time when last fee were accrued or after first deposit.
Then after some time when strategy earned some yields, convertToAssets(1e18)
will return more assets than highWaterMark
, so protocol will take fees.
But currently updating of highWaterMark
is done incorrectly.
Deposit, mint, withdraw function use syncFeeCheckpoint
modifier.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L496-L499
modifier syncFeeCheckpoint() { _; highWaterMark = convertToAssets(1e18); }
This modifier will update highWaterMark
to current assets amount that you can receive for 1e18 of shares.
That means that every time when deposit, mint, withdraw is called, highWaterMark
is updated to the new state, so protocol doesn't track yield progress anymore.
In case if protocol accrued some performance fees, which can be possible if noone called deposit, withdraw, mint for a long time, then anyone can frontrun takeFees
and deposit small amount of assets in order to update highWaterMark
, so protocol will not get any fees.
VsCode
I believe that you need to store highWaterMark = convertToAssets(1e18)
at the time of first deposit, or when totalShares is 0,
this will be the value that protocol started with
and then at time, when takefee was called you can calculate current convertToAssets(1e18)
in case if it's bigger, than previous stored, then you can mint fees for protocol and update highWaterMark to current value.
#0 - c4-judge
2023-02-16T06:11:08Z
dmvt marked the issue as duplicate of #30
#1 - c4-judge
2023-02-16T06:18:52Z
dmvt marked the issue as not a duplicate
#2 - c4-judge
2023-02-16T06:19:05Z
dmvt marked the issue as duplicate of #118
#3 - c4-sponsor
2023-02-18T11:52:10Z
RedVeil marked the issue as sponsor confirmed
#4 - c4-judge
2023-02-23T11:18:31Z
dmvt marked the issue as not a duplicate
#5 - c4-judge
2023-02-23T11:18:36Z
dmvt marked the issue as primary issue
#6 - c4-judge
2023-02-23T11:23:47Z
dmvt marked issue #166 as primary and marked this issue as a duplicate of 166
#7 - c4-judge
2023-02-23T11:24:08Z
dmvt marked the issue as selected for report
π Selected for report: immeas
Also found by: KIntern_NA, bin2chen, minhtrng, rbserver, rvierdiiev, ustas, yellowBirdy
69.3758 USDC - $69.38
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L429-L439 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L473-L494
Management fees should be accrued on any user action with Vault. Currently some users can avoid management fee payment, while another can pay fee for what they didn't use.
Protocol takes management fee for using of Vault. https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L429-L439
function accruedManagementFee() public view returns (uint256) { uint256 managementFee = fees.management; return managementFee > 0 ? managementFee.mulDiv( totalAssets() * (block.timestamp - feesUpdatedAt), SECONDS_PER_YEAR, Math.Rounding.Down ) / 1e18 : 0; }
This fee is calculated as percent from total assets multiplied by time elapsed from last fee colecting.
Currently to collect fee, someone can call takeManagementAndPerformanceFees
function.
This is incorrect design as it can make such situations.
1.Fee was taken last time at T1, so feesUpdatedAt = T1
. At T100 user deposits big amount of assets. Then at T101 takeManagementAndPerformanceFees
function is called, which sliced some amount of fees from depositor, however he didn't use that vault yet, he just deposited.
2.Now similar situation. Fee was taken last time at T1, so feesUpdatedAt = T1
. At T2 user deposits big amount of assets. At T100, user withdraws that amount and pays no fee. At T101 takeManagementAndPerformanceFees
function is called and collects fee, but user already left pool without payment.
VsCode
I think that takeFees
modifier should be set to deposit, withdraw, mint, redeem functions, so you collect management fee according to the user's usage of vault.
#0 - c4-judge
2023-02-16T06:11:12Z
dmvt marked the issue as duplicate of #30
#1 - c4-sponsor
2023-02-18T12:05:20Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:14:20Z
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
7.466 USDC - $7.47
AdapterBase.lastHarvest is not updated inside harvest function.
harvest
function allows strategy to collect yields. It's designed by protocol to be called at most 1 time is a harvestCooldown
period.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L438-L450
function harvest() public takeFees { if ( address(strategy) != address(0) && ((lastHarvest + harvestCooldown) < block.timestamp) ) { // solhint-disable address(strategy).delegatecall( abi.encodeWithSignature("harvest()") ); } emit Harvested(); }
But because, lastHarvest
is never updated in the contract, this function will allow strategy to harvest anytime after first harvestCooldown
is passed.
VsCode
Add lastHarvest = block.timestamp
at the end of function.
#0 - c4-judge
2023-02-16T04:25:45Z
dmvt marked the issue as duplicate of #199
#1 - c4-sponsor
2023-02-18T12:00:54Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T22:53:05Z
dmvt marked the issue as partial-50
π Selected for report: ast3ros
Also found by: rvierdiiev
522.171 USDC - $522.17
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L429-L439 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L87
Vault.accruedManagementFee starts accruing interests even when no assets in the vault. User pay more fees because of that.
accruedManagementFee
function calculates amount of fees for management.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L429-L439
function accruedManagementFee() public view returns (uint256) { uint256 managementFee = fees.management; return managementFee > 0 ? managementFee.mulDiv( totalAssets() * (block.timestamp - feesUpdatedAt), SECONDS_PER_YEAR, Math.Rounding.Down ) / 1e18 : 0; }
As you can see it uses feesUpdatedAt
variable in order to calculate amount of seconds that has elapsed, since last update.
When Vault is initialized, this variable is set to block.timestamp.
That actually means that Vault starts accruing management fee at the deploying time, when actually noone using it yet.
Because of that such situation is possible. 1.Vault is created. 2.No one use it for very long time(let's say half of year, just as example) 3.User deposits some big amount of funds. 4.Someone calls takeManagementAndPerformanceFees which takes huge part as fee, so depositor actually lost half of his funds
VsCode
Initialize feesUpdatedAt
when first deposit was made.
#0 - c4-judge
2023-02-16T08:03:01Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2023-02-17T11:05:06Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:24:58Z
dmvt marked the issue as satisfactory
#3 - c4-judge
2023-02-23T01:23:24Z
dmvt marked issue #499 as primary and marked this issue as a duplicate of 499
44.9555 USDC - $44.96
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L218 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L294-L301
Vault.withdraw doesn't rounds up burnt shares amount and burns less shares of user, then it should.
When user wants to withdraw some amount of assets, he calls Vault.withdraw
function.
According to the amount of assets, that user wants to receive that function calculates shares amount that need to be burnt from user balance.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L294-L301
function convertToShares(uint256 assets) public view returns (uint256) { uint256 supply = totalSupply(); // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? assets : assets.mulDiv(supply, totalAssets(), Math.Rounding.Down); }
It uses convertToShares
function which rounds down shares amount, however it should round up in this case.
VsCode
You need to round up shares amount.
#0 - c4-judge
2023-02-16T07:56:40Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2023-02-17T11:02:35Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:23:08Z
dmvt marked the issue as satisfactory
#3 - c4-judge
2023-02-23T01:23:10Z
dmvt marked issue #471 as primary and marked this issue as a duplicate of 471
π Selected for report: rvierdiiev
1508.4941 USDC - $1,508.49
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L158 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L456-L472
Strategy can't earn yields for user as underlyingBalance is not updated when strategy deposits.
When someone deposits/withdraws from adapter, then underlyingBalance
variable is updated with deposited/withdrawn to the vault shares amount.
Only when user deposits or withdraws, then AdapterBase
changes totalSupply(it mints or burns shares).
This is how shares of users are calculated inside BeffyAdapter https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/beefy/BeefyAdapter.sol#L122-L133
function convertToUnderlyingShares(uint256, uint256 shares) public view override returns (uint256) { uint256 supply = totalSupply(); return supply == 0 ? shares : shares.mulDiv(underlyingBalance, supply, Math.Rounding.Up); }
As you can see, when user provides amount of shares that he wants to withdraw, then these shares are recalculated in order to receive shares amount inside vault. This depends on underlyingBalance and totalSupply.
Each adapter can have a strategy that can withdraw harvest
and then redeposit it inside the vault. In this case users should earn new shares.
When adapter wants to deposit to vault it should call strategyDeposit
function.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L456-L461
function strategyDeposit(uint256 amount, uint256 shares) public onlyStrategy { _protocolDeposit(amount, shares); }
This function is just sending all amount to the vault.
But actually it should also increase underlyingBalance
with shares amount that it will receive by depositing.
Because this is not happening, underlyingBalance
always equal to totalSupply and users do not earn any yields using strategy as convertToUnderlyingShares
function will just return same value as provided shares.
So currently users can't earn any yields using strategy.
Note: this was discussed with protocol developer and he explained me how it should work.
VsCode
Increase underlyingBalance
with shares amount that it will receive by depositing. But do not mint shares)
#0 - c4-sponsor
2023-02-17T13:35:15Z
RedVeil marked the issue as sponsor acknowledged
#1 - c4-judge
2023-02-28T23:30:02Z
dmvt changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-02-28T23:30:13Z
dmvt marked the issue as satisfactory
#3 - rvierdiyev
2023-03-03T08:55:58Z
hey @dmvt I would like to say that this should be high severity bug, because users who deposited to strategy lose their yields. The main goal of adapter is to take user's money and earn more using them. In this case users lose all their rewards(rewards are earned, but not distributed among them). Let's say users deposited 10 million tokens to adapter and during some long time they have earned 100K as yields. When strategy deposits this 100K to adapter, then users receive nothing, which is equal to losing 100K for them. This will be done constantly, all earned funds will be lost after depositing by strategy. This is why i believe this is high severity issue and losses can be very big, depending on deposits amount.
#4 - c4-judge
2023-03-03T21:04:41Z
dmvt marked the issue as unsatisfactory: Insufficient quality
#5 - dmvt
2023-03-03T22:37:36Z
I'm bringing this back as a medium risk with the following additional context provided by the warden:
<img width="551" alt="222837195-1dfcb7a5-5170-42de-b5b2-60df570dda45" src="https://user-images.githubusercontent.com/1116695/222844670-bc5b3a56-723c-49da-a020-0d5988beb247.png"> <img width="524" alt="222837387-61f02cc2-68ae-4c9b-a1bd-79b11a3fffc2" src="https://user-images.githubusercontent.com/1116695/222844672-b2b7d45c-e532-43cd-a68f-b562526b66c8.png">I believe the screenshots above make the issue clear enough to include in the report.
#6 - c4-judge
2023-03-03T22:37:47Z
dmvt marked the issue as satisfactory
#7 - c4-judge
2023-03-03T22:37:52Z
dmvt marked the issue as selected for report
π Selected for report: rvierdiiev
Also found by: bin2chen
678.8223 USDC - $678.82
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L438-L450 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L162 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L232
AdpaterBase.harvest should be called before deposit and withdraw.
Function harvest
is called in order to receive yields for the adapter. It calls strategy, which handles that process. Depending on strategy it can call strategyDeposit
function in order to deposit earned amount through the adaptor.
That actually means that in case if totalAssets was X before harvest
call, then after it becomes X+Y, in case if Y yields were earned by adapter and strategy deposited it. So for the same amount of shares, user can receive bigger amount of assets.
When user deposits or withdraw then harvest
function is called, but it's called after shares amount calculation.
Because of that in case of deposit, all previous depositors lose some part of yields as they share it with new depositor.
And in case of withdraw, user loses his yields.
VsCode
Call harvest
before shares amount calculation.
#0 - c4-judge
2023-02-16T06:25:39Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2023-02-17T09:40:56Z
RedVeil marked the issue as sponsor acknowledged
#2 - c4-judge
2023-02-23T21:45:28Z
dmvt marked the issue as selected for report
π Selected for report: gjaldon
Also found by: 0x52, Aymen0909, KIntern_NA, hansfriese, rvierdiiev
28.5497 USDC - $28.55
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L170-L202
MultiRewardStaking.claimRewards will fail when reward token doesn't allow 0 tranfers and escrow percentage is 100%.
MultiRewardStaking.claimRewards will transfer rewards for toker to the sender. In case if escrow is used, then some percentage of rewards will be locked.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L191-L202
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); escrow.lock(rewardToken, user, escrowed, escrowInfo.escrowDuration, escrowInfo.offset); }
In case if escrowPercentage
is 100%, that means that payout
will be 0.
And if rewardToken
is not supporting 0 value transfers, then function will revert and user will not be able to claim his rewards and send them to the escrow.
VsCode
Add check that payout
is bigger than 0.
#0 - c4-judge
2023-02-16T05:48:08Z
dmvt marked the issue as duplicate of #251
#1 - c4-sponsor
2023-02-18T12:03:01Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T21:04:10Z
dmvt changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-02-23T23:20:33Z
This previously downgraded issue has been upgraded by captainmangoC4
#4 - c4-judge
2023-02-25T15:37:07Z
dmvt marked the issue as partial-25
55.5006 USDC - $55.50
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L296-L315 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L351-L360
MultiRewardStaking.changeRewardSpeed calculates rewardsEndTimestamp incorrectly. Because of that contract supposes that it has more balance for rewarding, so some users will not receive rewards.
MultiRewardStaking.changeRewardSpeed allows owner to update speed of rewarding. https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L296-L315
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)); uint256 remainder = rewardToken.balanceOf(address(this)); uint32 prevEndTime = rewards.rewardsEndTimestamp; uint32 rewardsEndTimestamp = _calcRewardsEnd( prevEndTime > block.timestamp ? prevEndTime : block.timestamp.safeCastTo32(), rewardsPerSecond, remainder ); rewardInfos[rewardToken].rewardsPerSecond = rewardsPerSecond; rewardInfos[rewardToken].rewardsEndTimestamp = rewardsEndTimestamp; }
The most interesting for us is this part.
uint256 remainder = rewardToken.balanceOf(address(this)); uint32 prevEndTime = rewards.rewardsEndTimestamp; uint32 rewardsEndTimestamp = _calcRewardsEnd( prevEndTime > block.timestamp ? prevEndTime : block.timestamp.safeCastTo32(), rewardsPerSecond, remainder );
Here function takes full balance of contract as remainder
, previous time when rewards should be ended and new rewardsPerSecond
.
Then it passes it to _calcRewardsEnd
function in order to calculate the amount of time, when this funds will be rewarded.
This amount is calculated not correct.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L351-L360
function _calcRewardsEnd( uint32 rewardsEndTimestamp, uint160 rewardsPerSecond, uint256 amount ) internal returns (uint32) { if (rewardsEndTimestamp > block.timestamp) amount += uint256(rewardsPerSecond) * (rewardsEndTimestamp - block.timestamp); return (block.timestamp + (amount / uint256(rewardsPerSecond))).safeCastTo32(); }
This function is working like that. In case if rewardsEndTimestamp > block.timestamp
it calculates amount that will be distributed during the remaining time. Then it adds this amount to amount provided as parameter.
This is not correct in our case for the reason that param amount
is full balance of reward token. And there is no need to calculate additional amount. Such approach is used inside fundReward
function, but is not working in this case.
As result contract adds additional amount to the full balance of contract and calculates time for distribution. Because this amount is bigger than contract's balance, time will be also bigger. But that time is not backed by reward tokens, so some users will not receive rewards, when they will be claiming them.
Scenario.
1.Owner adds new reward with some amount.
2.After some time (when rewarding is still in progress) he wants to change speed of rewards for the token.
3.After update rewardsEndTimestamp
is bigger than amount of rewards that it should pay for that time.
VsCode
In this case calculate rewardsEndTimestamp
like this:
uint32 rewardsEndTimestamp = _calcRewardsEnd( 0, rewardsPerSecond, remainder );
#0 - c4-judge
2023-02-16T03:56:07Z
dmvt marked the issue as duplicate of #156
#1 - c4-sponsor
2023-02-18T11:58:56Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T16:05:16Z
dmvt changed the severity to QA (Quality Assurance)
#3 - Simon-Busch
2023-02-23T16:15:01Z
Revert back to M as requested by @dmvt
#4 - c4-judge
2023-02-25T15:06:55Z
dmvt marked the issue as satisfactory
π Selected for report: gjaldon
Also found by: 0xMirce, Kenshin, Kumpa, chaduke, jasonxiale, joestakey, rvierdiiev
34.6879 USDC - $34.69
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L371-L384
MultiRewardStaking will stop working when rewardTokens list will be huge enough because of out of gas.
When user deposits, withdraws, mints, redeems in MultiRewardStaking contract, then accrueRewards
modifier is used which calculates rewards for user.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L371-L384
modifier accrueRewards(address _caller, address _receiver) { IERC20[] memory _rewardTokens = rewardTokens; for (uint8 i; i < _rewardTokens.length; i++) { IERC20 rewardToken = _rewardTokens[i]; RewardInfo memory rewards = rewardInfos[rewardToken]; if (rewards.rewardsPerSecond > 0) _accrueRewards(rewardToken, _accrueStatic(rewards)); _accrueUser(_receiver, rewardToken); // If a deposit/withdraw operation gets called for another user we should accrue for both of them to avoid potential issues like in the Convex-Vulnerability if (_receiver != _caller) _accrueUser(_caller, rewardToken); } _; }
Pls, note that this loop uses uint8 i
value in order to go through _rewardTokens
array.
The loop will end when i == _rewardTokens.length
. Because i is uint8, then _rewardTokens.length
will be also casted to uint8 and the max value will be 2^8. That means that once array of reward tokens will be bigger, than 2^8, then this new tokens will not be handled by contract and user will not receive rewards for them.
Also it will be not possible for owner to withdraw that funds, so they will actually stuck inside contract.
2^8 = 256, which is not small value, but still it's possible that more reward tokens will be used. And there is no ability to remove reward token from list.
VsCode
If you want to restrict that amount to 2^8, then do not allow to add next reward tokens, once that size is filled.
#0 - c4-judge
2023-02-16T03:45:46Z
dmvt marked the issue as duplicate of #151
#1 - c4-sponsor
2023-02-18T11:55:26Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T22:50:52Z
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
47.8163 USDC - $47.82
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L540-L546
Anyone can reset fees to 0 value when Vault is deployed. As result protocol will stop collecting fees.
Anyone can call changeFees
function in order to change fees
variable to proposedFees
.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L540-L546
function changeFees() external { if (block.timestamp < proposedFeeTime + quitPeriod) revert NotPassedQuitPeriod(quitPeriod); emit ChangedFees(fees, proposedFees); fees = proposedFees; }
There is a check that should not allow anyone to call function before quitPeriod
has passed after fees changing was proposed.
However function doesn't check that proposedFeeTime
is not 0, so that means that after Vault has deployed, anyone can call this function the check will pass.
That means that fees
will be set to the proposedFees
, which is 0.
As result protocol will stop collecting fees.
Use this test inside Vault.t.sol. Here you can see that noone proposed fee changing, but it was changed and set fees to 0.
function test__changeFees() public { VaultFees memory newVaultFees = VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }); //noone proposed //vault.proposeFees(newVaultFees); vm.warp(block.timestamp + 3 days); vm.expectEmit(false, false, false, true, address(vault)); emit ChangedFees(VaultFees({ deposit: 0, withdrawal: 0, management: 0, performance: 0 }), newVaultFees); vault.changeFees(); (uint256 deposit, uint256 withdrawal, uint256 management, uint256 performance) = vault.fees(); assertEq(deposit, 0); assertEq(withdrawal, 0); assertEq(management, 0); assertEq(performance, 0); }
VsCode
function changeFees() external { if (proposedFeeTime == 0 || block.timestamp < proposedFeeTime + quitPeriod) revert NotPassedQuitPeriod(quitPeriod); emit ChangedFees(fees, proposedFees); fees = proposedFees; }
#0 - c4-judge
2023-02-16T08:07:41Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2023-02-17T11:10:20Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-sponsor
2023-02-18T12:16:29Z
RedVeil marked the issue as disagree with severity
#3 - c4-judge
2023-02-23T01:16:31Z
dmvt changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-02-23T01:23:52Z
dmvt marked the issue as selected for report