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

Simple harvester for OETH #2333

Merged
merged 27 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8fe5bee
feat: Implement simple harvester.
clement-ux Dec 20, 2024
e14774a
feat: support multiple strategies.
clement-ux Dec 23, 2024
16513f4
fix: adjust error message.
clement-ux Dec 23, 2024
128c288
feat: add test for simple harvester.
clement-ux Dec 23, 2024
cb78145
feat: WIP add deployment script for simple harvester.
clement-ux Dec 23, 2024
0208ddd
fix: adjust deployment for simple harvester.
clement-ux Dec 23, 2024
fe7596c
doc: add comment on deployment file.
clement-ux Dec 23, 2024
20bab54
Merge branch 'master' of https://github.com/OriginProtocol/origin-dol…
clement-ux Dec 30, 2024
8142811
prettier deployment file.
clement-ux Dec 30, 2024
0f22981
add prettier-ignore.
clement-ux Dec 30, 2024
500b6d3
feat: remove operator role.
clement-ux Dec 30, 2024
23a885f
fix: `setStrategyStatus` can be called by strategist.
clement-ux Dec 30, 2024
5446bde
fix: change wording for supporting strategies.
clement-ux Dec 30, 2024
b79578b
feat: prevent supporting address 0.
clement-ux Dec 30, 2024
00515f3
fix: change event for changing strategist.
clement-ux Dec 30, 2024
8427f26
prettier.
clement-ux Dec 30, 2024
23a5a50
fix tests.
clement-ux Dec 30, 2024
9e968fd
fix: change event name.
clement-ux Dec 31, 2024
00766a9
feat: set strategist in internal method.
clement-ux Jan 2, 2025
534ce82
fix: use `changeGov` instead of `setGov` in constructor.
clement-ux Jan 2, 2025
639e7ae
feat: use `Strategizable` instead of `Governable`.
clement-ux Jan 4, 2025
afaf9e5
feat: add rescue token.
clement-ux Jan 4, 2025
9eefde2
test: add more tests.
clement-ux Jan 4, 2025
0e8ab27
feat: add strategy address in event.
clement-ux Jan 5, 2025
8d2de44
Merge branch 'master' of https://github.com/OriginProtocol/origin-dol…
clement-ux Jan 6, 2025
543378d
fix: use correct address for strategist at deployment.
clement-ux Jan 6, 2025
f0559ad
fix: remove console.log
clement-ux Jan 6, 2025
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
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);

Check warning on line 28 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L26-L28

Added lines #L26 - L28 were not covered by tests
}

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

Check warning on line 35 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L34-L35

Added lines #L34 - L35 were not covered by tests
}

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

Check warning on line 40 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L38-L40

Added lines #L38 - L40 were not covered by tests
}
}

function _harvestAndTransfer(address _strategy) internal {

Check warning on line 44 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L44

Added line #L44 was not covered by tests
// Ensure strategy is supported
DanielVF marked this conversation as resolved.
Show resolved Hide resolved
require(supportedStrategies[_strategy], "Strategy not supported");

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

Check warning on line 49 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L49

Added line #L49 was not covered by tests

// Cache reward tokens
address[] memory rewardTokens = IStrategy(_strategy)

Check warning on line 52 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L52

Added line #L52 was not covered by tests
.getRewardTokenAddresses();
for (uint256 i = 0; i < rewardTokens.length; i++) {

Check warning on line 54 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L54

Added line #L54 was not covered by tests
// Cache balance
uint256 balance = IERC20(rewardTokens[i]).balanceOf(address(this));

Check warning on line 56 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L56

Added line #L56 was not covered by tests
if (balance > 0) {
// Transfer to strategist
IERC20(rewardTokens[i]).safeTransfer(strategistAddr, balance);
emit Harvested(_strategy, rewardTokens[i], balance);

Check warning on line 60 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L59-L60

Added lines #L59 - L60 were not covered by tests
}
}
}

////////////////////////////////////////////////////
/// --- GOVERNANCE
////////////////////////////////////////////////////
function setSupportedStrategy(address _strategy, bool _isSupported)

Check warning on line 68 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L68

Added line #L68 was not covered by tests
external
onlyGovernorOrStrategist
{
DanielVF marked this conversation as resolved.
Show resolved Hide resolved
require(_strategy != address(0), "Invalid strategy");
supportedStrategies[_strategy] = _isSupported;
emit SupportedStrategyUpdated(_strategy, _isSupported);

Check warning on line 74 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L73-L74

Added lines #L73 - L74 were not covered by tests
}

function transferToken(address _asset, uint256 _amount)

Check warning on line 77 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L77

Added line #L77 was not covered by tests
external
onlyGovernorOrStrategist
{
IERC20(_asset).safeTransfer(strategistAddr, _amount);

Check warning on line 81 in contracts/contracts/harvest/OETHHarvesterSimple.sol

View check run for this annotation

Codecov / codecov/patch

contracts/contracts/harvest/OETHHarvesterSimple.sol#L81

Added line #L81 was not covered by tests
}
}
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also have a test for the strategist to set a supported strategy

.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);
});
});
Loading