-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
a559741
commit a29405f
Showing
4 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | ||
}, | ||
], | ||
}; | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
200 changes: 200 additions & 0 deletions
200
contracts/test/harvest/simple-harvester.mainnet.fork-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |