Popcorn contest - fs0c'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: 21/169

Findings: 4

Award: $1,142.87

QA:
grade-b

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

4.6115 USDC - $4.61

Labels

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

External Links

Lines of code

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

Vulnerability details

Impact

In MultiRewardStaking the function claimRewards doesn’t have nonReentrant which makes it possible to re-enter the function. If one of the reward tokens in ERC-777 token, it is possible to re-enter and claim the reward again and again until the contract is drained out of those tokens.

When the function would be re-entered, it would call accrueRewards again which would call _accrueUser for _receiver and _caller, it would update the value of accruedRewards again as accruedRewards[_user][_rewardToken] += supplierDelta; The second time supplierDelta would be zero, but the accruedRewards would remain the same. This value is used later in the claimRewards function as rewardAmount and sent to the user.

POC

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

In the above function the accruedRewards[user][_rewardTokens[i]] is updated after the tokens are transfered to the user. This makes it possible to steal the rewards multiple times in case of ERC-777 (extension of ERC-20) tokens, which have a callback.

Recommendation

Add nonReentrant modifier.

#0 - c4-judge

2023-02-16T07:40:21Z

dmvt marked the issue as duplicate of #54

#1 - c4-sponsor

2023-02-18T12:11:19Z

RedVeil marked the issue as sponsor confirmed

#2 - c4-judge

2023-02-23T00:47:20Z

dmvt marked the issue as partial-50

#3 - c4-judge

2023-03-01T00:41:34Z

dmvt marked the issue as satisfactory

Findings Information

🌟 Selected for report: 7siech

Also found by: fs0c, imare

Labels

bug
3 (High Risk)
satisfactory
sponsor acknowledged
upgraded by judge
duplicate-388

Awards

1044.3421 USDC - $1,044.34

External Links

Lines of code

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

Vulnerability details

Impact

As the strategy implementation is currently unknown and a user can define it on their own, it is possible for strategy to have some storage variables, and for the harvest function to work upon those storage variables.

The function harvest() in AdapterBase.sol calls harvest in strategy using a delegate call. If in some case, the delegate call modifies storage variables. Those variables would be modified in adapter and might change some important variables in the adapter contract.

Currently we can’t tell the complete effect this has on adapter contract as the strategy contract can have different implementations, but this design can surely lead to some storage collision.

POC

function harvest() public takeFees {
        if (
            address(strategy) != address(0) &&
            ((lastHarvest + harvestCooldown) < block.timestamp)
        ) {
            // solhint-disable
            address(strategy).delegatecall(
                abi.encodeWithSignature("harvest()")
            );
        }

        emit Harvested();
    }

Recommendation

If the delegatecall is just used to forward the msg.sender (as written in the comments) , use a different approach rather than a delegatecall.

#0 - c4-judge

2023-02-16T06:53:34Z

dmvt marked the issue as duplicate of #388

#1 - c4-sponsor

2023-02-18T12:07:02Z

RedVeil marked the issue as sponsor acknowledged

#2 - c4-judge

2023-02-25T16:21:40Z

dmvt changed the severity to 3 (High Risk)

#3 - c4-judge

2023-02-28T17:29:56Z

dmvt marked the issue as satisfactory

#4 - c4-judge

2023-03-01T23:28:21Z

dmvt marked the issue as selected for report

#5 - c4-judge

2023-03-01T23:29:22Z

dmvt marked issue #388 as primary and marked this issue as a duplicate of 388

Findings Information

🌟 Selected for report: fs0c

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

Labels

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

Awards

58.4422 USDC - $58.44

External Links

Lines of code

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

Vulnerability details

Impact

Malicious users can drain the assets of the vault.

POC

The withdraw function users convertToShares to convert the assets to the amount of shares. These shares are burned from the users account and the assets are returned to the user.

The function withdraw is shown below:

function withdraw(
        uint256 assets,
        address receiver,
        address owner
    ) public nonReentrant syncFeeCheckpoint returns (uint256 shares) {
        if (receiver == address(0)) revert InvalidReceiver();

        shares = convertToShares(assets);
/// .... [skipped the code]

The function convertToShares is shown below:

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 Math.Rounding.Down , but it should use Math.Rounding.Up

Assume that the vault with the following state:

  • Total Asset = 1000 WETH
  • Total Supply = 10 shares

Assume that Alice wants to withdraw 99 WETH from the vault. Thus, she calls the Vault.withdraw(99 WETH) function.

The calculation would go like this:

assets = 99
return value = assets * supply / totalAssets()
return value = 99 * 10 / 1000
return value = 0

The value would be rounded round to zero. This will be the amount of shares burned from users account, which is zero.

Hence user can drain the assets from the vault without burning their shares.

Note : A similar issue also exists in mint functionality where Math.Rounding.Down is used and Math.Rounding.Up should be used.

Recommendation

Use Math.Rounding.Up instead of Math.Rounding.Down.

As per OZ implementation here is the rounding method that should be used:

deposit : convertToShares → Math.Rounding.Down

mint : converttoAssets → Math.Rounding.Up

withdraw : convertToShares → Math.Rounding.Up

redeem : convertToAssets → Math.Rounding.Down

#0 - c4-judge

2023-02-16T07:57:50Z

dmvt marked the issue as duplicate of #71

#1 - c4-sponsor

2023-02-18T12:13:29Z

RedVeil marked the issue as disagree with severity

#2 - c4-sponsor

2023-02-18T12:13:32Z

RedVeil marked the issue as sponsor confirmed

#3 - c4-judge

2023-02-23T01:13:35Z

dmvt changed the severity to 2 (Med Risk)

#4 - c4-judge

2023-02-23T01:23:13Z

dmvt marked the issue as selected for report

[QA-01] changeAdapter should only be called by owner.

Impact

Altough there is no immediate threat due to this, but this function should only be called by owner.

POC

function changeAdapter() external takeFees {
        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));
    }

Recommendation

Add onlyOwner modfier to the function.

[QA-02] Remove the unnecessary variable assetsCheckpoint and change the natspec comments regarding that.

The unnecessary variable assetsCheckpoint can be removed and the natspec comment of changeAdapter has to be modified to remove the variable.

* @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 {

[QA-03] Function state mutability can be restricted to view

src/utils/MultiRewardStaking.sol:351:3:

function _calcRewardsEnd(
    uint32 rewardsEndTimestamp,
    uint160 rewardsPerSecond,
    uint256 amount
  ) internal returns (uint32) {

src/vault/VaultController.sol:242:3:

function _encodeAdapterData(DeploymentArgs memory adapterData, bytes memory baseAdapterData)
    internal
    returns (bytes memory)

src/vault/VaultController.sol:667:3:

function _verifyCreatorOrOwner(address vault) internal returns (VaultMetadata memory metadata) {

#0 - c4-judge

2023-02-28T15:02:50Z

dmvt marked the issue as grade-b

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