Popcorn contest - Malatrax'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: 95/169

Findings: 2

Award: $50.41

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Awards

14.932 USDC - $14.93

Labels

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

External Links

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 __AdapterBase_inittoblock.timestamp` but it is never updated afterward. Initialisation of lastHarvest in __AdapterBase_init() GitHub Link

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.

  • The strategy used is the MockStrategy.sol, its harvest function emits the event "StrategyExecuted()" when called.
  • The PoC relies on checking if the current timestamp is in the expected interval and on the expected emission of StrategyExecuted(). uint256 public lastHarvest from AdapterBase.sol has no getter function in IAdapter.sol, which is why initialLastHarvest & updatedLastHarvest are used to assert the interval in the test test__harvest_cooldown().
  • The test set harvestCooldown as 1 hour (3600 seconds) adapter.harvest() is then called at different times: - When block.timestamp < lastHarvest + harvestCooldown, which works as intended, emitting Harvested() but not StrategyExecuted() - When lastHarvest + harvestCooldown < block.timestamp, which works as intended, emitting Harvested() and StrategyExecuted(). - When updatedLastHarvest + harvestCooldown > block.timestamp updatedLastHarvest is the value that should've taken lastHarvest on the past call of adapter.harvest(). We shouldn't have an emission of StrategyExecuted() but we do when checking the logs:
https://i.imgur.com/FYD9AVP.png

Tools Used

Manual review

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

#0 - c4-judge

2023-02-16T04:26:38Z

dmvt marked the issue as duplicate of #199

#1 - c4-sponsor

2023-02-18T12:01:11Z

RedVeil marked the issue as sponsor confirmed

#2 - c4-judge

2023-02-23T22:29:07Z

dmvt marked the issue as satisfactory

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