code-423n4 / 2023-01-popcorn-findings

0 stars 0 forks source link

HarvestCooldown can be bypassed because lastHarvest is never updated, leading to potential risk of funds depending on the strategy attached. #773

Closed code423n4 closed 1 year ago

code423n4 commented 1 year ago

Lines of code

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

Vulnerability details

Impact

The variable uint256 public lastHarvest`` is initialized by__AdapterBaseinittoblock.timestamp` but it is never updated afterward. [Initialisation of lastHarvest in \_AdapterBase_init() GitHub Link](https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/adapter/abstracts/AdapterBase.sol#L42-L87)

Therefore, once the condition lastHarvest + harvestCooldown < block.timestamp is true, it will remain true (unless the AdapterBase is reinitialized which won't happen). Making harvestCooldown useless.

The harvest function is used in:

These two internal functions are used by these four external functions:

Once block.timestamp > lastHarvest + harvestCooldown, where harvestCooldown < 1 days, anyone calling one of these four functions will trigger strategy.harvest() which will result in an unexpected waste of gas (as big as the strategy needs). Depending on what the strategy does, modifying the vault/adapter's token balances could lead to an unexpected loss of tokens. (referring to the README.md::Grieving Attacks list)

Moreover, function harvest() public takeFees is public, with no access control, which means that anyone can call it at any time.

Depending on the strategy and the implementation of strategy.harvest(), an attacker could exploit the fact that harvest() can be called arbitrarily. (e.g strategy includes a UniswapV2 Pool which is a constant-product AMM: it could be subject to a price manipulation attack (powered by a flashloan) in favor of the attacker).

This attack surface is amplified by the fact that the delegated call to strategy.harvest() can silently fail.

Proof of Concept

test__harvest__cooldown() source code

I used the BeefyAdapter for this PoC, but it would work the same way with any adapter that inherits AdapterBase.

https://i.imgur.com/FYD9AVP.png

Tools Used

Manual review

Recommended Mitigation Steps

Update the lastHarvest sotrage variable to block.timestamp in the harvest() function (following the Check-Effect-Interaction pattern):

/**
* @notice Execute Strategy and take fees. Updates lastHarvest
* @dev Delegatecall to strategy's harvest() function. All necessary data is passed via `strategyConfig`.
* @dev Delegatecall is used to in case any logic requires the adapters address as a msg.sender. (e.g. Synthetix staking)
*/

function harvest() public takeFees {
    if (
        address(strategy) != address(0) &&
        ((lastHarvest + harvestCooldown) < block.timestamp)
    ) {
        lastHarvest = block.timestamp;

        // solhint-disable
        //Delegated call no return value should also be done
        address(strategy).delegatecall(
            abi.encodeWithSignature("harvest()")
        );
    }

    emit Harvested();
}
c4-judge commented 1 year ago

dmvt marked the issue as duplicate of #199

c4-sponsor commented 1 year ago

RedVeil marked the issue as sponsor confirmed

c4-judge commented 1 year ago

dmvt marked the issue as satisfactory