Skip to content

Commit

Permalink
Simple harvester for OETH (#2333)
Browse files Browse the repository at this point in the history
* feat: Implement simple harvester.

* feat: support multiple strategies.

* fix: adjust error message.

* feat: add test for simple harvester.

* feat: WIP add deployment script for simple harvester.

* fix: adjust deployment for simple harvester.

* doc: add comment on deployment file.

* prettier deployment file.

* add prettier-ignore.

* feat: remove operator role.

* fix: `setStrategyStatus` can be called by strategist.

* fix: change wording for supporting strategies.

* feat: prevent supporting address 0.

* fix: change event for changing strategist.

* prettier.

* fix tests.

* fix: change event name.

* feat: set strategist in internal method.

* fix: use `changeGov` instead of `setGov` in constructor.

* feat: use `Strategizable` instead of `Governable`.

* feat: add rescue token.

* test: add more tests.

* feat: add strategy address in event.

* fix: use correct address for strategist at deployment.

* fix: remove console.log
  • Loading branch information
clement-ux authored Jan 8, 2025
1 parent a559741 commit a29405f
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 0 deletions.
83 changes: 83 additions & 0 deletions contracts/contracts/harvest/OETHHarvesterSimple.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { Strategizable } from "../governance/Strategizable.sol";
import { IStrategy } from "../interfaces/IStrategy.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract OETHHarvesterSimple is Strategizable {
using SafeERC20 for IERC20;

////////////////////////////////////////////////////
/// --- STORAGE
////////////////////////////////////////////////////
mapping(address => bool) public supportedStrategies;

////////////////////////////////////////////////////
/// --- EVENTS
////////////////////////////////////////////////////
event Harvested(address strategy, address token, uint256 amount);
event SupportedStrategyUpdated(address strategy, bool status);

////////////////////////////////////////////////////
/// --- CONSTRUCTOR
////////////////////////////////////////////////////
constructor(address _governor, address _strategist) {
_setStrategistAddr(_strategist);
_changeGovernor(_governor);
}

////////////////////////////////////////////////////
/// --- MUTATIVE FUNCTIONS
////////////////////////////////////////////////////
function harvestAndTransfer(address _strategy) external {
_harvestAndTransfer(_strategy);
}

function harvestAndTransfer(address[] calldata _strategies) external {
for (uint256 i = 0; i < _strategies.length; i++) {
_harvestAndTransfer(_strategies[i]);
}
}

function _harvestAndTransfer(address _strategy) internal {
// Ensure strategy is supported
require(supportedStrategies[_strategy], "Strategy not supported");

// Harvest rewards
IStrategy(_strategy).collectRewardTokens();

// Cache reward tokens
address[] memory rewardTokens = IStrategy(_strategy)
.getRewardTokenAddresses();
for (uint256 i = 0; i < rewardTokens.length; i++) {
// Cache balance
uint256 balance = IERC20(rewardTokens[i]).balanceOf(address(this));
if (balance > 0) {
// Transfer to strategist
IERC20(rewardTokens[i]).safeTransfer(strategistAddr, balance);
emit Harvested(_strategy, rewardTokens[i], balance);
}
}
}

////////////////////////////////////////////////////
/// --- GOVERNANCE
////////////////////////////////////////////////////
function setSupportedStrategy(address _strategy, bool _isSupported)
external
onlyGovernorOrStrategist
{
require(_strategy != address(0), "Invalid strategy");
supportedStrategies[_strategy] = _isSupported;
emit SupportedStrategyUpdated(_strategy, _isSupported);
}

function transferToken(address _asset, uint256 _amount)
external
onlyGovernorOrStrategist
{
IERC20(_asset).safeTransfer(strategistAddr, _amount);
}
}
47 changes: 47 additions & 0 deletions contracts/deploy/mainnet/114_simple_harvester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const addresses = require("../../utils/addresses");
const { deploymentWithGovernanceProposal } = require("../../utils/deploy");

module.exports = deploymentWithGovernanceProposal(
{
deployName: "114_simple_harvester",
forceDeploy: false,
// forceSkip: true,
reduceQueueTime: true,
deployerIsProposer: false,
// proposalId: "",
},
async ({ deployWithConfirmation }) => {
const { strategistAddr } = await getNamedAccounts();

// 1. Deploy contract
const dOETHHarvesterSimple = await deployWithConfirmation(
"OETHHarvesterSimple",
[addresses.mainnet.Timelock, strategistAddr]
);

console.log("strategistAddr: ", strategistAddr);
const cOETHHarvesterSimple = await ethers.getContractAt(
"OETHHarvesterSimple",
dOETHHarvesterSimple.address
);

// Get AMO contract
const cAMO = await ethers.getContractAt(
"ConvexEthMetaStrategy",
addresses.mainnet.ConvexOETHAMOStrategy
);

// Governance Actions
// ----------------
return {
name: "Change harvester in OETH AMO",
actions: [
{
contract: cAMO,
signature: "setHarvesterAddress(address)",
args: [cOETHHarvesterSimple.address],
},
],
};
}
);
5 changes: 5 additions & 0 deletions contracts/test/_fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@ const defaultFixture = deployments.createFixture(async () => {
morphoGauntletPrimeUSDTStrategyProxy.address
);

const simpleOETHHarvester = isFork
? await ethers.getContract("OETHHarvesterSimple")
: undefined;

let usdt,
dai,
tusd,
Expand Down Expand Up @@ -805,6 +809,7 @@ const defaultFixture = deployments.createFixture(async () => {
morphoGauntletPrimeUSDCVault,
morphoGauntletPrimeUSDTStrategy,
morphoGauntletPrimeUSDTVault,
simpleOETHHarvester,

// Flux strategy
fluxStrategy,
Expand Down
200 changes: 200 additions & 0 deletions contracts/test/harvest/simple-harvester.mainnet.fork-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
const { expect } = require("chai");

const addresses = require("../../utils/addresses");
const { isCI } = require("../helpers");
const { setERC20TokenBalance } = require("../_fund");

const { loadDefaultFixture } = require("../_fixture");

describe("ForkTest: CurvePoolBooster", function () {
this.timeout(0);

// Retry up to 3 times on CI
this.retries(isCI ? 3 : 0);

let fixture;
beforeEach(async () => {
fixture = await loadDefaultFixture();
});

it("Should have correct parameters", async () => {
const { simpleOETHHarvester } = fixture;
const { strategistAddr } = await getNamedAccounts();

expect(await simpleOETHHarvester.governor()).to.be.equal(
addresses.mainnet.Timelock
);

expect(await simpleOETHHarvester.strategistAddr()).to.be.equal(
strategistAddr
);
});

it("Should support Strategy as governor", async () => {
const { simpleOETHHarvester } = fixture;
const timelock = await ethers.provider.getSigner(
addresses.mainnet.Timelock
);

expect(
await simpleOETHHarvester.supportedStrategies(
addresses.mainnet.ConvexOETHAMOStrategy
)
).to.be.equal(false);
await simpleOETHHarvester
.connect(timelock)
.setSupportedStrategy(addresses.mainnet.ConvexOETHAMOStrategy, true);
expect(
await simpleOETHHarvester.supportedStrategies(
addresses.mainnet.ConvexOETHAMOStrategy
)
).to.be.equal(true);
});

it("Should support Strategy as strategist", async () => {
const { simpleOETHHarvester } = fixture;
const strategist = await ethers.provider.getSigner(
await simpleOETHHarvester.strategistAddr()
);

expect(
await simpleOETHHarvester.supportedStrategies(
addresses.mainnet.ConvexOETHAMOStrategy
)
).to.be.equal(false);
await simpleOETHHarvester
.connect(strategist)
.setSupportedStrategy(addresses.mainnet.ConvexOETHAMOStrategy, true);
expect(
await simpleOETHHarvester.supportedStrategies(
addresses.mainnet.ConvexOETHAMOStrategy
)
).to.be.equal(true);
});

it("Should revert if support strategy is not governor or strategist", async () => {
const { simpleOETHHarvester, josh } = fixture;

await expect(
// prettier-ignore
simpleOETHHarvester
.connect(josh)
.setSupportedStrategy(addresses.mainnet.ConvexOETHAMOStrategy, true)
).to.be.revertedWith("Caller is not the Strategist or Governor");
});

it("Should revert if when setting strategist is not governor", async () => {
const { simpleOETHHarvester, josh } = fixture;

await expect(
// prettier-ignore
simpleOETHHarvester
.connect(josh)
.setStrategistAddr(josh.address)
).to.be.revertedWith("Caller is not the Governor");
});

it("Should Set strategist", async () => {
const { simpleOETHHarvester, josh } = fixture;
const timelock = await ethers.provider.getSigner(
addresses.mainnet.Timelock
);

expect(await simpleOETHHarvester.strategistAddr()).not.to.equal(
josh.address
);
await simpleOETHHarvester.connect(timelock).setStrategistAddr(josh.address);
expect(await simpleOETHHarvester.strategistAddr()).to.equal(josh.address);
});

it("Should Harvest and transfer rewards as strategist", async () => {
const { simpleOETHHarvester, convexEthMetaStrategy, crv } = fixture;
const strategistAddress = await simpleOETHHarvester.strategistAddr();
const strategist = await ethers.provider.getSigner(strategistAddress);

const balanceBeforeCRV = await crv.balanceOf(strategistAddress);
await simpleOETHHarvester
.connect(strategist)
.setSupportedStrategy(convexEthMetaStrategy.address, true);
// prettier-ignore
await simpleOETHHarvester
.connect(strategist)["harvestAndTransfer(address)"](convexEthMetaStrategy.address);

const balanceAfterCRV = await crv.balanceOf(strategistAddress);
expect(balanceAfterCRV).to.be.gt(balanceBeforeCRV);
});

it("Should Harvest and transfer rewards as governor", async () => {
const { simpleOETHHarvester, convexEthMetaStrategy, crv } = fixture;
const timelock = await ethers.provider.getSigner(
addresses.mainnet.Timelock
);
const strategist = await simpleOETHHarvester.strategistAddr();

const balanceBeforeCRV = await crv.balanceOf(strategist);
await simpleOETHHarvester
.connect(timelock)
.setSupportedStrategy(convexEthMetaStrategy.address, true);
// prettier-ignore
await simpleOETHHarvester
.connect(timelock)["harvestAndTransfer(address)"](convexEthMetaStrategy.address);

const balanceAfterCRV = await crv.balanceOf(strategist);
expect(balanceAfterCRV).to.be.gt(balanceBeforeCRV);
});

it("Should revert if strategy is not authorized", async () => {
const { simpleOETHHarvester, convexEthMetaStrategy } = fixture;
const timelock = await ethers.provider.getSigner(
addresses.mainnet.Timelock
);

await expect(
// prettier-ignore
simpleOETHHarvester
.connect(timelock)["harvestAndTransfer(address)"](convexEthMetaStrategy.address)
).to.be.revertedWith("Strategy not supported");
});

it("Should revert if strategy is address 0", async () => {
const { simpleOETHHarvester } = fixture;
const timelock = await ethers.provider.getSigner(
addresses.mainnet.Timelock
);

await expect(
// prettier-ignore
simpleOETHHarvester
.connect(timelock).setSupportedStrategy(addresses.zero, true)
).to.be.revertedWith("Invalid strategy");
});

it("Should test to rescue tokens as governor", async () => {
const { simpleOETHHarvester, crv } = fixture;
const timelock = await ethers.provider.getSigner(
addresses.mainnet.Timelock
);

await setERC20TokenBalance(simpleOETHHarvester.address, crv, "1000");
const balanceBeforeCRV = await crv.balanceOf(simpleOETHHarvester.address);
await simpleOETHHarvester
.connect(timelock)
.transferToken(crv.address, "1000");
const balanceAfterCRV = await crv.balanceOf(simpleOETHHarvester.address);
expect(balanceAfterCRV).to.be.lt(balanceBeforeCRV);
});

it("Should test to rescue tokens as strategist", async () => {
const { simpleOETHHarvester, crv } = fixture;
const strategistAddress = await simpleOETHHarvester.strategistAddr();
const strategist = await ethers.provider.getSigner(strategistAddress);

await setERC20TokenBalance(simpleOETHHarvester.address, crv, "1000");
const balanceBeforeCRV = await crv.balanceOf(simpleOETHHarvester.address);
await simpleOETHHarvester
.connect(strategist)
.transferToken(crv.address, "1000");
const balanceAfterCRV = await crv.balanceOf(simpleOETHHarvester.address);
expect(balanceAfterCRV).to.be.lt(balanceBeforeCRV);
});
});

0 comments on commit a29405f

Please sign in to comment.