diff --git a/contracts/src/BeefyHarvestLens.sol b/contracts/src/BeefyHarvestLens.sol index 6977345..9524d7c 100644 --- a/contracts/src/BeefyHarvestLens.sol +++ b/contracts/src/BeefyHarvestLens.sol @@ -15,6 +15,10 @@ struct LensResult { bytes harvestResult; } +error CallRewardTooLow(uint256 callReward, uint256 minCallReward); +error StrategyPaused(); +error MinCallRewardTooLow(uint256 minCallReward); + // Simulate a harvest while recieving a call reward. Return callReward amount and whether or not it was a success. contract BeefyHarvestLens { using SafeERC20 for IERC20; @@ -22,7 +26,7 @@ contract BeefyHarvestLens { // Simulate harvest calling callStatic/simulateContract for return results. // this method will hide any harvest errors and it is not recommended to use it to do the harvesting // only the simulation using callStatic/simulateContract is recommended - function harvest(IStrategyV7 _strategy, IERC20 _rewardToken) external returns (LensResult memory res) { + function harvestSimulation(IStrategyV7 _strategy, IERC20 _rewardToken) external returns (LensResult memory res) { res.blockNumber = block.number; res.paused = _strategy.paused(); @@ -51,4 +55,30 @@ contract BeefyHarvestLens { } } } + + function safeHarvest(IStrategyV7 _strategy, IERC20 _rewardToken, uint256 _minCallReward) + external + returns (uint256) + { + if (_strategy.paused()) { + revert StrategyPaused(); + } + + if (_minCallReward == 0) { + revert MinCallRewardTooLow({minCallReward: _minCallReward}); + } + + uint256 rewardsBefore = IERC20(_rewardToken).balanceOf(address(this)); + IStrategyV7(_strategy).harvest(address(this)); + + // ensure we are not getting sandwiched by a flash loan + uint256 callReward = IERC20(_rewardToken).balanceOf(address(this)) - rewardsBefore; + if (callReward < _minCallReward) { + revert CallRewardTooLow({callReward: callReward, minCallReward: _minCallReward}); + } + + _rewardToken.safeTransfer(msg.sender, callReward); + + return callReward; + } } diff --git a/contracts/test/BeefyHarvestLens.t.sol b/contracts/test/BeefyHarvestLens.t.sol index 8486bec..5440b6c 100644 --- a/contracts/test/BeefyHarvestLens.t.sol +++ b/contracts/test/BeefyHarvestLens.t.sol @@ -49,11 +49,11 @@ contract BeefyHarvestLensTest is Test { lens = new BeefyHarvestLens(); } - function test_lens_do_not_throw_when_harvest_reverts() public { + function test_lens_simulation_do_not_throw_when_harvest_reverts() public { revertOnHarvest = true; (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); - LensResult memory res = lens.harvest(strat, rewardToken); + LensResult memory res = lens.harvestSimulation(strat, rewardToken); assertEq(res.callReward, 0); assertEq(res.success, false); @@ -65,9 +65,9 @@ contract BeefyHarvestLensTest is Test { assertEq(rewardToken.balanceOf(address(this)), 0); } - function test_normal_harvest() public { + function test_lens_simulation_can_simulate_harvest() public { (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); - LensResult memory res = lens.harvest(strat, rewardToken); + LensResult memory res = lens.harvestSimulation(strat, rewardToken); assertEq(res.callReward, 987654); assertEq(res.success, true); @@ -80,11 +80,11 @@ contract BeefyHarvestLensTest is Test { assertEq(rewardToken.balanceOf(address(this)), 987654); } - function test_lens_returns_call_rewards() public { + function test_lens_simulation_returns_call_rewards() public { harvestRewards = 1 ether; (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); - LensResult memory res = lens.harvest(strat, rewardToken); + LensResult memory res = lens.harvestSimulation(strat, rewardToken); assertEq(res.callReward, 1 ether); assertEq(res.success, true); @@ -97,11 +97,11 @@ contract BeefyHarvestLensTest is Test { assertEq(rewardToken.balanceOf(address(this)), 1 ether); } - function test_lens_returns_paused() public { + function test_lens_simulation_returns_paused() public { pausedMock = true; (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); - LensResult memory res = lens.harvest(strat, rewardToken); + LensResult memory res = lens.harvestSimulation(strat, rewardToken); assertEq(res.callReward, 0); assertEq(res.success, false); @@ -113,11 +113,11 @@ contract BeefyHarvestLensTest is Test { assertEq(rewardToken.balanceOf(address(this)), 0); } - function test_lens_returns_last_harvest() public { + function test_lens_simulation_returns_last_harvest() public { lastHarvestMock = 98765; (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); - LensResult memory res = lens.harvest(strat, rewardToken); + LensResult memory res = lens.harvestSimulation(strat, rewardToken); assertEq(res.callReward, 987654); assertEq(res.success, true); @@ -130,11 +130,11 @@ contract BeefyHarvestLensTest is Test { assertEq(rewardToken.balanceOf(address(this)), 987654); } - function test_lens_success_when_call_reward_is_zero() public { + function test_lens_simulation_success_when_call_reward_is_zero() public { harvestRewards = 0; (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); - LensResult memory res = lens.harvest(strat, rewardToken); + LensResult memory res = lens.harvestSimulation(strat, rewardToken); assertEq(res.callReward, 0); assertEq(res.success, true); @@ -147,11 +147,11 @@ contract BeefyHarvestLensTest is Test { assertEq(rewardToken.balanceOf(address(this)), 0); } - function test_lens_do_not_crash_when_last_harvest_isnt_defined() public { + function test_lens_simulation_do_not_crash_when_last_harvest_isnt_defined() public { revertOnLastHarvest = true; (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); - LensResult memory res = lens.harvest(strat, rewardToken); + LensResult memory res = lens.harvestSimulation(strat, rewardToken); assertEq(res.callReward, 987654); assertEq(res.success, true); @@ -163,4 +163,50 @@ contract BeefyHarvestLensTest is Test { assertEq(res.harvestResult.length, 0); assertEq(rewardToken.balanceOf(address(this)), 987654); } + + function test_lens_safe_transfer_do_throw_when_harvest_reverts() public { + revertOnHarvest = true; + + (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); + + vm.expectRevert("revertOnHarvest"); + + lens.safeHarvest(strat, rewardToken, 1000); + } + + function test_lens_safe_transfer_can_simulate_harvest() public { + (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); + uint256 callReward = lens.safeHarvest(strat, rewardToken, 1000); + + assertEq(callReward, 987654); + assertEq(rewardToken.balanceOf(address(this)), 987654); + } + + function test_lens_safe_transfer_returns_call_rewards() public { + harvestRewards = 1 ether; + + (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); + uint256 callReward = lens.safeHarvest(strat, rewardToken, 1000); + + assertEq(callReward, 1 ether); + assertEq(rewardToken.balanceOf(address(this)), 1 ether); + } + + function test_lens_safe_harvest_fails_when_expected_call_rewards_is_zero() public { + harvestRewards = 0; + + (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); + + vm.expectRevert(abi.encodeWithSelector(MinCallRewardTooLow.selector, 0)); + + lens.safeHarvest(strat, rewardToken, 0); + } + + function test_lens_safe_harvest_reverts_when_call_reward_is_too_low() public { + (IStrategyV7 strat, BeefyHarvestLens lens) = _helper_create_contracts(); + + vm.expectRevert(abi.encodeWithSelector(CallRewardTooLow.selector, 987654, 987655)); + + lens.safeHarvest(strat, rewardToken, 987655); + } }