Popcorn contest - csanuragjain'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: 61/169

Findings: 3

Award: $161.60

QA:
grade-b

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: Lirios

Also found by: KIntern_NA, csanuragjain, hansfriese, ladboy233, thecatking

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sponsor confirmed
edited-by-warden
duplicate-515

Awards

114.1988 USDC - $114.20

External Links

Lines of code

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

Vulnerability details

Impact

On changing adapter address, all funds are first withdrawn from old adapter and then reinvested in new adaptor. In case of some adaptor like BeefyAdapter, an extra fees (strat.withdrawalFee()) is also deducted on withdrawal. It was observed that proposedAdapter is never reset meaning user can call changeAdapter multiple times causing protocol to loss funds as withdrawal fees

Proof of Concept

  1. Assume owner has proposed a new BeefyAdapter for Vault V using the proposeAdapter function
function proposeAdapter(IERC4626 newAdapter) external onlyOwner {
        if (newAdapter.asset() != address(asset))
            revert VaultAssetMismatchNewAdapterAsset();

        proposedAdapter = newAdapter;
        proposedAdapterTime = block.timestamp;

        emit NewAdapterProposed(newAdapter, block.timestamp);
    }
  1. Post proposedAdapterTime owner calls changeAdapter which changes adapter to BeefyAdapter
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));
    }
  1. Attacker A calls changeAdapter function again

  2. Since proposedAdapter and proposedAdapterTime are not reset, this works


//Pass since adapterTime was not reset
if (block.timestamp < proposedAdapterTime + quitPeriod)
            revert NotPassedQuitPeriod(quitPeriod);
  1. Now this redeems BeefyAdapter funds which also deducts a Beefy strategy withdrawal fees (due to bug described in separate issue, fees is not deducted currently but once fixed this will occur)
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;
    }
  1. Due to withdrawal fees, not full amount is redeposited and the fees is then distributed to network users. Even though there was no change in adapter

  2. Repeating these steps will cause growing losses

Revise the changeAdapter function to change adaptor only if provided is different from existing adapter

function changeAdapter() external takeFees {
...
require(proposedAdapter!=adapter, "Same adapter");
...
}

#0 - c4-judge

2023-02-16T07:17:32Z

dmvt marked the issue as primary issue

#1 - c4-sponsor

2023-02-17T10:11:40Z

RedVeil marked the issue as sponsor confirmed

#2 - c4-judge

2023-02-25T21:03:23Z

dmvt changed the severity to 2 (Med Risk)

#3 - c4-judge

2023-02-25T21:08:28Z

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

#4 - c4-judge

2023-02-28T17:31:48Z

dmvt marked the issue as satisfactory

Awards

11.9167 USDC - $11.92

Labels

bug
2 (Med Risk)
primary issue
selected for report
sponsor confirmed
M-14

External Links

Lines of code

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

Vulnerability details

Impact

If you are making a Lock fund for escrow using a fee on transfer token then contract will receive less amount (X-fees) but will record full amount (X). This becomes a problem as when claim is made then call will fail due to lack of funds. Worse, one user will unknowingly take the missing fees part from another user deposited escrow fund

Proof of Concept

  1. User locks token X as escrow which take fee on transfer
  2. For same, he uses lock function which transfer funds from user to contract
function lock( IERC20 token, address account, uint256 amount, uint32 duration, uint32 offset ) external { ... token.safeTransferFrom(msg.sender, address(this), amount); ... escrows[id] = Escrow({ token: token, start: start, end: start + duration, lastUpdateTime: start, initialBalance: amount, balance: amount, account: account }); ... }
  1. Since token has fee on transfer so contract receives only amount-fees but the escrow object is created for full amount

  2. Lets say escrow duration is over and claim is made using claimRewards function

function claimRewards(bytes32[] memory escrowIds) external { ... uint256 claimable = _getClaimableAmount(escrow); ... escrow.token.safeTransfer(escrow.account, claimable); ... }
  1. Since full duration is over so claimable amount is amount. But this fails on transfer to account since contract has only amount-fees

Compute the balance before and after transfer and subtract them to get the real amount. Also use nonReentrant while using this to prevent from reentrancy in ERC777 tokens

#0 - c4-judge

2023-02-16T07:07:17Z

dmvt marked the issue as duplicate of #44

#1 - c4-sponsor

2023-02-18T11:48:12Z

RedVeil marked the issue as sponsor confirmed

#2 - c4-judge

2023-02-23T01:21:07Z

dmvt marked the issue as selected for report

Owner can accidentally add duplicate Clone

Contract: https://github.com/code-423n4/2023-01-popcorn//blob/main/src/vault/CloneRegistry.sol#L41

Issue: On adding a duplicate entry via addClone function, multiple clone will be written on clones and allClones object.

Impact: If any future function relies directly on number of clones, or iterating allClones then it will encounter duplicate entry. if its not operated correctly then could result in incorrect outcome

Recommendation: Add below condition:

require(!cloneExists[clone], "Already exists");

#0 - c4-judge

2023-02-28T15:03:06Z

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