Popcorn contest - rvierdiiev's results

A multi-chain regenerative yield-optimizing protocol.

General Information

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

Popcorn

Findings Distribution

Researcher Performance

Rank: 1/169

Findings: 15

Award: $8,268.35

🌟 Selected for report: 5

πŸš€ Solo Findings: 1

Findings Information

🌟 Selected for report: bin2chen

Also found by: 0xTraub, Ch_301, rvierdiiev

Labels

bug
3 (High Risk)
satisfactory
sponsor confirmed
duplicate-552

Awards

704.9309 USDC - $704.93

External Links

Lines of code

https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/beefy/BeefyAdapter.sol#L46-L84

Vulnerability details

Impact

Malicious beefy booster can be provided to BeefyAdapter that can steal funds, as there is no check that it's endorsed in registry.

Proof of Concept

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.

Tools Used

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

Awards

4.6115 USDC - $4.61

Labels

bug
3 (High Risk)
satisfactory
sponsor confirmed
upgraded by judge
duplicate-402

External Links

Lines of code

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

Vulnerability details

Impact

User can drain all erc777 token balance of MultiRewardStaking using reentrancy in claimRewards function.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: rvierdiiev

Also found by: bin2chen

Labels

bug
3 (High Risk)
disagree with severity
primary issue
selected for report
sponsor acknowledged
H-07

Awards

2262.7411 USDC - $2,262.74

External Links

Lines of code

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

Vulnerability details

Impact

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.

Tools Used

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

Findings Information

🌟 Selected for report: rvierdiiev

Also found by: peakbolt

Labels

bug
3 (High Risk)
primary issue
selected for report
sponsor confirmed
H-11

Awards

2262.7411 USDC - $2,262.74

External Links

Lines of code

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

Vulnerability details

Impact

Protocol loses fees because highWaterMark is updated every time someone deposit, withdraw, mint.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: immeas

Also found by: KIntern_NA, bin2chen, minhtrng, rbserver, rvierdiiev, ustas, yellowBirdy

Labels

bug
2 (Med Risk)
satisfactory
sponsor confirmed
duplicate-780

Awards

69.3758 USDC - $69.38

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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.

Tools Used

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

Findings Information

Awards

7.466 USDC - $7.47

Labels

bug
2 (Med Risk)
partial-50
sponsor confirmed
duplicate-558

External Links

Lines of code

https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/adapter/abstracts/AdapterBase.sol#L438-L450

Vulnerability details

Impact

AdapterBase.lastHarvest is not updated inside harvest function.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: ast3ros

Also found by: rvierdiiev

Labels

bug
2 (Med Risk)
satisfactory
sponsor confirmed
duplicate-499

Awards

522.171 USDC - $522.17

External Links

Lines of code

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

Vulnerability details

Impact

Vault.accruedManagementFee starts accruing interests even when no assets in the vault. User pay more fees because of that.

Proof of Concept

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

Tools Used

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

Findings Information

🌟 Selected for report: fs0c

Also found by: 0xmuxyz, DadeKuma, Kumpa, bin2chen, koxuan, ladboy233, nadin, rvi0x, rvierdiiev

Labels

bug
2 (Med Risk)
satisfactory
sponsor confirmed
duplicate-471

Awards

44.9555 USDC - $44.96

External Links

Lines of code

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

Vulnerability details

Impact

Vault.withdraw doesn't rounds up burnt shares amount and burns less shares of user, then it should.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: rvierdiiev

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
selected for report
sponsor acknowledged
M-18

Awards

1508.4941 USDC - $1,508.49

External Links

Lines of code

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

Vulnerability details

Impact

Strategy can't earn yields for user as underlyingBalance is not updated when strategy deposits.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: rvierdiiev

Also found by: bin2chen

Labels

bug
2 (Med Risk)
primary issue
selected for report
sponsor acknowledged
M-25

Awards

678.8223 USDC - $678.82

External Links

Lines of code

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

Vulnerability details

Impact

AdpaterBase.harvest should be called before deposit and withdraw.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: gjaldon

Also found by: 0x52, Aymen0909, KIntern_NA, hansfriese, rvierdiiev

Labels

bug
2 (Med Risk)
partial-25
sponsor confirmed
duplicate-251

Awards

28.5497 USDC - $28.55

External Links

Lines of code

https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L170-L202

Vulnerability details

Impact

MultiRewardStaking.claimRewards will fail when reward token doesn't allow 0 tranfers and escrow percentage is 100%.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: Ruhum

Also found by: 0xMirce, 0xRobocop, cccz, chaduke, gjaldon, minhtrng, rvierdiiev, ulqiorra

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sponsor confirmed
duplicate-190

Awards

55.5006 USDC - $55.50

External Links

Lines of code

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

Vulnerability details

Impact

MultiRewardStaking.changeRewardSpeed calculates rewardsEndTimestamp incorrectly. Because of that contract supposes that it has more balance for rewarding, so some users will not receive rewards.

Proof of Concept

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.

Tools Used

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

Findings Information

🌟 Selected for report: gjaldon

Also found by: 0xMirce, Kenshin, Kumpa, chaduke, jasonxiale, joestakey, rvierdiiev

Labels

bug
2 (Med Risk)
partial-50
sponsor confirmed
duplicate-165

Awards

34.6879 USDC - $34.69

External Links

Lines of code

https://github.com/code-423n4/2023-01-popcorn/blob/main/src/utils/MultiRewardStaking.sol#L371-L384

Vulnerability details

Impact

MultiRewardStaking will stop working when rewardTokens list will be huge enough because of out of gas.

Proof of Concept

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.

Tools Used

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

Findings Information

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
primary issue
selected for report
sponsor confirmed
M-34

Awards

47.8163 USDC - $47.82

External Links

Lines of code

https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L540-L546

Vulnerability details

Impact

Anyone can reset fees to 0 value when Vault is deployed. As result protocol will stop collecting fees.

Proof of Concept

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);
  }

Tools Used

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax Β© 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter