Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safer harvest #8

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion contracts/src/BeefyHarvestLens.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@ 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;

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

Expand Down Expand Up @@ -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;
}
}
74 changes: 60 additions & 14 deletions contracts/test/BeefyHarvestLens.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
}
}