From ec10c57272cdb1cc23e46a274734ba5864617bd2 Mon Sep 17 00:00:00 2001 From: mard Date: Sat, 24 Sep 2022 16:29:44 +0800 Subject: [PATCH 1/9] fix: impersonate function --- src/tests/utils/setupHelper.ts | 2 +- src/tests/utils/testHelper.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tests/utils/setupHelper.ts b/src/tests/utils/setupHelper.ts index 0350b263..04d7693d 100644 --- a/src/tests/utils/setupHelper.ts +++ b/src/tests/utils/setupHelper.ts @@ -254,7 +254,7 @@ export const getLpToken = async ( /** * @dev get want token from the whale using impersonating account feature - * @param want want token instance + * @param want_addr want token instance * @param amount token amount * @param to receive address * @param whaleAddr whale address to send tokens diff --git a/src/tests/utils/testHelper.ts b/src/tests/utils/testHelper.ts index 8c5f500c..8a964b45 100644 --- a/src/tests/utils/testHelper.ts +++ b/src/tests/utils/testHelper.ts @@ -2,6 +2,7 @@ import "@nomicfoundation/hardhat-toolbox"; import { BigNumber, BigNumberish, providers } from "ethers"; import { ethers, network } from "hardhat"; import * as chai from 'chai'; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; /** @@ -49,9 +50,8 @@ export const getContractAt = async (name: string, address: string) => { */ export const unlockAccount = async ( address: string -): Promise => { - await network.provider.send("hardhat_impersonateAccount", [address]); - return ethers.provider.getSigner(address); +): Promise => { + return ethers.getImpersonatedSigner(address); }; /** From 7e87bd1775782d32e43cd6d118f8be70a92025cb Mon Sep 17 00:00:00 2001 From: mard Date: Sat, 24 Sep 2022 16:31:23 +0800 Subject: [PATCH 2/9] add velo usdc-tusd strategy --- .../velodrome/strategy-velo-usdc-tusd-slp.sol | 42 +++++++++++++++++++ .../strategy-velo-usdc-tusd-slp.test.ts | 18 ++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/strategies/optimism/velodrome/strategy-velo-usdc-tusd-slp.sol create mode 100644 src/tests/strategies/optimism/velodrome/strategy-velo-usdc-tusd-slp.test.ts diff --git a/src/strategies/optimism/velodrome/strategy-velo-usdc-tusd-slp.sol b/src/strategies/optimism/velodrome/strategy-velo-usdc-tusd-slp.sol new file mode 100644 index 00000000..e628956e --- /dev/null +++ b/src/strategies/optimism/velodrome/strategy-velo-usdc-tusd-slp.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.6; + +import "./strategy-velo-base.sol"; + +contract StrategyVeloUsdcTusdSlp is StrategyVeloBase { + // Addresses + address private _lp = 0xA4549B89A39f76d9D28415474aeD7d06Ec9935fe; + address private _gauge = 0xc4eAB0D1d7616eA99c15698bb075C2Adb8D2fDc5; + address private constant usdc = 0x7F5c764cBc14f9669B88837ca1490cCa17c31607; + address private constant tusd = 0xcB59a0A753fDB7491d5F3D794316F1adE197B21E; + + constructor( + address _governance, + address _strategist, + address _controller, + address _timelock + ) StrategyVeloBase( + _lp, + _gauge, + _governance, + _strategist, + _controller, + _timelock + ) + { + isStablePool = true; + + // token0 route + nativeToTokenRoutes[usdc].push(ISolidlyRouter.route(native, usdc, false)); + + // token1 route + nativeToTokenRoutes[tusd].push(ISolidlyRouter.route(native, usdc, false)); + nativeToTokenRoutes[tusd].push(ISolidlyRouter.route(usdc, tusd, true)); + } + + // **** Views **** + + function getName() external override pure returns (string memory) { + return "StrategyVeloUsdcTusdSlp"; + } +} diff --git a/src/tests/strategies/optimism/velodrome/strategy-velo-usdc-tusd-slp.test.ts b/src/tests/strategies/optimism/velodrome/strategy-velo-usdc-tusd-slp.test.ts new file mode 100644 index 00000000..3f5e3682 --- /dev/null +++ b/src/tests/strategies/optimism/velodrome/strategy-velo-usdc-tusd-slp.test.ts @@ -0,0 +1,18 @@ +import "@nomicfoundation/hardhat-toolbox"; +import { getWantFromWhale } from "../../../utils/setupHelper"; +import { doTestBehaviorBase } from "./strategyVeloBase"; +import { ethers } from "hardhat"; + + +describe("StrategyVeloUsdcTusdSlp", () => { + const want_addr = "0xA4549B89A39f76d9D28415474aeD7d06Ec9935fe"; + const whale_addr = "0x4023ef3aaa0669FaAf3A712626F4D8cCc3eAF2e5"; + const reward_token_addr = "0x3c8B650257cFb5f272f799F5e2b4e65093a11a05"; + + before("Get want token", async () => { + const [alice] = await ethers.getSigners(); + await getWantFromWhale(want_addr, 2000000000000, alice, whale_addr); + }); + + doTestBehaviorBase("StrategyVeloUsdcTusdSlp", want_addr, reward_token_addr, 5); +}); From 5fb0e00749123962f1e265d14e42ff7a41a46515 Mon Sep 17 00:00:00 2001 From: mard Date: Sat, 24 Sep 2022 16:33:27 +0800 Subject: [PATCH 3/9] add prettier rules for typescript --- .prettierrc | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/.prettierrc b/.prettierrc index cb4c77cf..5061e21f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,26 +1,22 @@ { + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "singleQuote": false, + "bracketSpacing": false, + "explicitTypes": "always", "overrides": [ { "files": "*.sol", "options": { - "printWidth": 120, - "tabWidth": 4, - "useTabs": false, - "singleQuote": false, - "bracketSpacing": false, - "explicitTypes": "always" + "tabWidth": 4 } }, { - "files": "*.js", + "files": "*.ts", "options": { - "printWidth": 120, - "tabWidth": 2, - "useTabs": false, - "singleQuote": false, - "bracketSpacing": false, - "explicitTypes": "always" + "parser": "typescript" } } ] -} +} \ No newline at end of file From f80751a6b4373e934a318d907b297fe5e16fbec5 Mon Sep 17 00:00:00 2001 From: mard Date: Tue, 4 Oct 2022 10:47:35 +0800 Subject: [PATCH 4/9] optimism chainlink keeper --- src/optimism/chainlinkKeeper.sol | 126 +++++++ .../AutomationCompatibleInterface.sol | 41 +++ src/optimism/interfaces/strategyv2.sol | 32 ++ src/tests/optimism/chainlinkKeeper.test.ts | 333 ++++++++++++++++++ 4 files changed, 532 insertions(+) create mode 100644 src/optimism/chainlinkKeeper.sol create mode 100644 src/optimism/interfaces/chainlink/AutomationCompatibleInterface.sol create mode 100644 src/optimism/interfaces/strategyv2.sol create mode 100644 src/tests/optimism/chainlinkKeeper.test.ts diff --git a/src/optimism/chainlinkKeeper.sol b/src/optimism/chainlinkKeeper.sol new file mode 100644 index 00000000..e5913fac --- /dev/null +++ b/src/optimism/chainlinkKeeper.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import "./interfaces/chainlink/AutomationCompatibleInterface.sol"; +import "./interfaces/strategyv2.sol"; +import "./interfaces/univ3/pool/IUniswapV3PoolState.sol"; + + +contract PickleRebalancingKeeper is AutomationCompatibleInterface { + address[] public strategies; + address public keeperRegistry = 0x75c0530885F385721fddA23C539AF3701d6183D4; + int24 public threshold = 10; + + address public governance; + bool public disabled = false; + + modifier onlyGovernance() { + require(msg.sender == governance, "!Governance"); + _; + } + + modifier whenNotDisabled() { + require(!disabled, "Disabled"); + _; + } + + constructor(address _governance) { + governance = _governance; + } + + function setGovernance(address _governance) external onlyGovernance { + governance = _governance; + } + + function setKeeperRegistry(address _keeperRegistry) external onlyGovernance { + keeperRegistry = _keeperRegistry; + } + + function setThreshold(int24 _threshold) external onlyGovernance { + threshold = _threshold; + } + + function setDisabled(bool _disabled) external onlyGovernance { + disabled = _disabled; + } + + function addStrategy(address _address) external onlyGovernance { + require(!_search(_address), "Address Already Watched"); + strategies.push(_address); + } + + function removeStrategy(address _address) external onlyGovernance { + require(_search(_address), "Address Not Watched"); + + for (uint256 i = 0; i < strategies.length; i++) { + if (strategies[i] == _address) { + strategies[i] = strategies[strategies.length - 1]; + strategies.pop(); + break; + } + } + } + + function checkUpkeep(bytes calldata) + external + view + override + whenNotDisabled + returns (bool upkeepNeeded, bytes memory performData) + { + address[] memory _stratsToUpkeep = new address[](strategies.length); + + uint24 counter = 0; + for (uint256 i = 0; i < strategies.length; i++) { + bool shouldRebalance = _checkValidToCall(strategies[i]); + if (shouldRebalance == true) { + _stratsToUpkeep[counter] = strategies[i]; + upkeepNeeded = true; + counter++; + } + } + + if (upkeepNeeded == true) { + address[] memory stratsToUpkeep = new address[](counter); + for (uint i = 0; i < counter; i++) { + stratsToUpkeep[i] = _stratsToUpkeep[i]; + } + performData = abi.encode(stratsToUpkeep); + } + } + + function performUpkeep(bytes calldata performData) external override whenNotDisabled { + address[] memory stratsToUpkeep = abi.decode(performData, (address[])); + + for (uint24 i = 0; i < stratsToUpkeep.length; i++) { + require(_checkValidToCall(stratsToUpkeep[i]), "!Valid"); + IStrategyV2(stratsToUpkeep[i]).rebalance(); + } + } + + function _search(address _address) internal view returns (bool) { + for (uint256 i = 0; i < strategies.length; i++) { + if (strategies[i] == _address) { + return true; + } + } + return false; + } + + function _checkValidToCall(address _strategy) internal view returns (bool) { + require(_search(_strategy), "Address Not Watched"); + + int24 _lowerTick = IStrategyV2(_strategy).tick_lower(); + int24 _upperTick = IStrategyV2(_strategy).tick_upper(); + int24 _range = _upperTick - _lowerTick; + int24 _limitVar = _range / threshold; + int24 _lowerLimit = _lowerTick + _limitVar; + int24 _upperLimit = _upperTick - _limitVar; + + (, int24 _currentTick, , , , , ) = IUniswapV3PoolState(IStrategyV2(_strategy).pool()).slot0(); + if (_currentTick < _lowerLimit || _currentTick > _upperLimit) { + return true; + } + return false; + } +} diff --git a/src/optimism/interfaces/chainlink/AutomationCompatibleInterface.sol b/src/optimism/interfaces/chainlink/AutomationCompatibleInterface.sol new file mode 100644 index 00000000..e0899d42 --- /dev/null +++ b/src/optimism/interfaces/chainlink/AutomationCompatibleInterface.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface AutomationCompatibleInterface { + /** + * @notice method that is simulated by the keepers to see if any work actually + * needs to be performed. This method does does not actually need to be + * executable, and since it is only ever simulated it can consume lots of gas. + * @dev To ensure that it is never called, you may want to add the + * cannotExecute modifier from KeeperBase to your implementation of this + * method. + * @param checkData specified in the upkeep registration so it is always the + * same for a registered upkeep. This can easily be broken down into specific + * arguments using `abi.decode`, so multiple upkeeps can be registered on the + * same contract and easily differentiated by the contract. + * @return upkeepNeeded boolean to indicate whether the keeper should call + * performUpkeep or not. + * @return performData bytes that the keeper should call performUpkeep with, if + * upkeep is needed. If you would like to encode data to decode later, try + * `abi.encode`. + */ + function checkUpkeep(bytes calldata checkData) external returns (bool upkeepNeeded, bytes memory performData); + + /** + * @notice method that is actually executed by the keepers, via the registry. + * The data returned by the checkUpkeep simulation will be passed into + * this method to actually be executed. + * @dev The input to this method should not be trusted, and the caller of the + * method should not even be restricted to any single registry. Anyone should + * be able call it, and the input should be validated, there is no guarantee + * that the data passed in is the performData returned from checkUpkeep. This + * could happen due to malicious keepers, racing keepers, or simply a state + * change while the performUpkeep transaction is waiting for confirmation. + * Always validate the data passed in. + * @param performData is the data which was passed back from the checkData + * simulation. If it is encoded, it can easily be decoded into other types by + * calling `abi.decode`. This data should not be trusted, and should be + * validated against the contract's current state. + */ + function performUpkeep(bytes calldata performData) external; +} \ No newline at end of file diff --git a/src/optimism/interfaces/strategyv2.sol b/src/optimism/interfaces/strategyv2.sol new file mode 100644 index 00000000..d24098e0 --- /dev/null +++ b/src/optimism/interfaces/strategyv2.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.2; + +interface IStrategyV2 { + function tick_lower() external view returns (int24); + + function tick_upper() external view returns (int24); + + function balanceProportion(int24, int24) external; + + function pool() external view returns (address); + + function timelock() external view returns (address); + + function deposit() external; + + function withdraw(address) external; + + function withdraw(uint256) external returns (uint256, uint256); + + function withdrawAll() external returns (uint256, uint256); + + function liquidityOf() external view returns (uint256); + + function harvest() external; + + function rebalance() external; + + function setTimelock(address) external; + + function setController(address _controller) external; +} diff --git a/src/tests/optimism/chainlinkKeeper.test.ts b/src/tests/optimism/chainlinkKeeper.test.ts new file mode 100644 index 00000000..11933cad --- /dev/null +++ b/src/tests/optimism/chainlinkKeeper.test.ts @@ -0,0 +1,333 @@ +import "@nomicfoundation/hardhat-toolbox"; +import {ethers} from "hardhat"; +import {setBalance, loadFixture, mine} from "@nomicfoundation/hardhat-network-helpers"; +import {expect, getContractAt, deployContract, toWei, unlockAccount} from "../utils/testHelper"; +import {BigNumber, Contract} from "ethers"; +import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; + +describe(`PickleRebalancingKeeper`, () => { + const keeperContractName: string = "src/optimism/chainlinkKeeper.sol:PickleRebalancingKeeper"; + const uniV3StrategyContractName: string = + "src/strategies/optimism/uniswapv3/strategy-univ3-eth-usdc-lp.sol:StrategyEthUsdcUniV3Optimism"; + const uniV3Strategy1Address: string = "0x1570B5D17a0796112263F4E3FAeee53459B41A49"; + const uniV3Strategy2Address: string = "0x754ece9AC6b3FF9aCc311261EC82Bd1B69b8E00B"; + const wethAddress: string = "0x4200000000000000000000000000000000000006"; + const strat1WethAmount: BigNumber = toWei(5000); + const strat2WethAmount: BigNumber = toWei(9000); + const univ3RouterAddress: string = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"; + const poolAbi = [ + "function observe(uint32[]) view returns(int56[], uint160[])", + "function tickSpacing() view returns(int24)", + "function token0() view returns(address)", + "function token1() view returns(address)", + "function slot0() view returns(uint160 sqrtPriceX96, int24 tick, uint16 observationIndex, uint16 observationCardinality, uint16 observationCardinalityNext, uint8 feeProtocol, bool unlocked)", + "function fee() view returns(uint24)", + ]; + const routerAbi = [ + "function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) payable returns(uint256 amountOut)", + "function exactInput((bytes path,address recipient,uint256 amountIn,uint256 amountOutMinimum)) payable returns (uint256 amountOut)", + ]; + + const setupSingleStrategyFixture = async () => { + const [alice, governance] = await ethers.getSigners(); + + const weth = await getContractAt("src/lib/erc20.sol:ERC20", wethAddress); + + const strategy = await getContractAt(uniV3StrategyContractName, uniV3Strategy1Address); + const stratAddrEncoded = ethers.utils.defaultAbiCoder.encode(["address[]"], [[strategy.address]]); + + const poolAddress = await strategy.pool(); + const pool = await ethers.getContractAt(poolAbi, poolAddress); + + const token0 = await pool.token0(); + const token1 = await pool.token1(); + const strat1TokenOut: string = weth.address.toLowerCase() === token0.toLowerCase() ? token1 : token0; + + const router = await ethers.getContractAt(routerAbi, univ3RouterAddress); + + const keeper = await deployContract(keeperContractName, governance.address); + + // Add strategy to keeper watch-list + await keeper.connect(governance).addStrategy(strategy.address); + + // Set alice weth balance so it can push strategy out of range + await setBalance(alice.address, strat1WethAmount.add(ethers.utils.parseEther("1"))); + const wethDepositAbi = ["function deposit() payable"]; + const wethTmp = await ethers.getContractAt(wethDepositAbi, wethAddress); + await wethTmp.connect(alice).deposit({value: strat1WethAmount}); + + // Add keeper to the strategy harvesters list + const stratStrategistAddr = await strategy.governance(); + const stratStrategist = await unlockAccount(stratStrategistAddr); + await strategy.connect(stratStrategist).whitelistHarvesters([keeper.address]); + + return {alice, governance, weth, strategy, stratAddrEncoded, pool, strat1TokenOut, router, keeper}; + }; + + const setupDoubleStrategiesFixture = async () => { + const {alice, governance, weth, strategy, stratAddrEncoded, pool, strat1TokenOut, router, keeper} = + await loadFixture(setupSingleStrategyFixture); + + const strategy2 = await getContractAt(uniV3StrategyContractName, uniV3Strategy2Address); + + const poolAddress = await strategy2.pool(); + const strat2Pool = await ethers.getContractAt(poolAbi, poolAddress); + + const token0 = await strat2Pool.token0(); + const token1 = await strat2Pool.token1(); + const strat2TokenOut: string = weth.address.toLowerCase() === token0.toLowerCase() ? token1 : token0; + + // Add strategy to keeper watch-list + await keeper.connect(governance).addStrategy(strategy2.address); + + // Set alice weth balance so it can push both strategies out of range + await setBalance(alice.address, strat2WethAmount.add(ethers.utils.parseEther("1"))); + const wethDepositAbi = ["function deposit() payable"]; + const wethTmp = await ethers.getContractAt(wethDepositAbi, wethAddress); + await wethTmp.connect(alice).deposit({value: strat2WethAmount}); + + // Add keeper to strategy2 harvesters list + const stratStrategistAddr = await strategy.governance(); + const stratStrategist = await unlockAccount(stratStrategistAddr); + await strategy2.connect(stratStrategist).whitelistHarvesters([keeper.address]); + + return { + alice, + governance, + weth, + strategy1: strategy, + strategy2, + stratAddrEncoded, + pool, + strat2Pool, + strat1TokenOut, + strat2TokenOut, + router, + keeper, + }; + }; + + describe("Keeper Strategies' Watch-List Behaviours", () => { + let pass: boolean; + + it("Only governance can remove strategies", async () => { + pass = false; + const {keeper, alice, strategy} = await loadFixture(setupSingleStrategyFixture); + await keeper + .connect(alice) + .removeStrategy(strategy.address) + .catch(() => { + pass = true; + }); + expect(pass).to.be.eq(true, "Non-governance address can remove strategy"); + }); + + it("Only governance can add strategies", async () => { + pass = true; + const {keeper, alice} = await loadFixture(setupSingleStrategyFixture); + await keeper + .connect(alice) + .addStrategy(uniV3Strategy2Address) + .catch(() => { + pass = true; + }); + expect(pass).to.be.eq(true, "Non-governance address can add strategy"); + }); + + it("Should not add an already watched strategy", async () => { + pass = false; + const {keeper, governance, strategy} = await loadFixture(setupSingleStrategyFixture); + await keeper + .connect(governance) + .addStrategy(strategy.address) + .catch(() => { + pass = true; + }); + expect(pass).to.be.eq(true, "Duplicate strategies can be added to keeper's watch list"); + }); + + it("Should not remove a non-watched strategy", async () => { + pass = false; + const {keeper, governance} = await loadFixture(setupSingleStrategyFixture); + await keeper + .connect(governance) + .removeStrategy(uniV3Strategy2Address) + .catch(() => { + pass = true; + }); + expect(pass).to.be.eq(true, "Non-watched strategy can be removed!"); + }); + + it("Should add a new strategy correctly", async () => { + const {keeper, governance} = await loadFixture(setupSingleStrategyFixture); + await keeper.connect(governance).addStrategy(uniV3Strategy2Address).catch(); + const newStratAddress = await keeper.strategies(1); + expect(newStratAddress).to.be.eq(uniV3Strategy2Address, "Failed adding new strategy"); + }); + }); + + describe("checkUpkeep", () => { + it("Should return false when no rebalance needed", async () => { + const {keeper} = await loadFixture(setupSingleStrategyFixture); + const [shouldUpkeep] = await keeper.callStatic.checkUpkeep("0x"); + expect(shouldUpkeep).to.be.eq(false, "checkUpkeep logic broken"); + }); + + it("Should return true when a rebalance needed", async () => { + const {keeper, strategy, weth, strat1TokenOut, alice, router} = await loadFixture(setupSingleStrategyFixture); + + // Push strategy out of range + await pushOutOfRange(strategy, weth, strat1TokenOut, alice, router, strat1WethAmount); + + const [shouldUpkeep, data] = await keeper.callStatic.checkUpkeep("0x"); + expect(shouldUpkeep).to.be.eq(true, "checkUpkeep logic broken"); + + // In solidity, bytes variable is of type Uint8Array where each 32 bytes encodes a single value + // We need to convert it into a hex string so ethers abi coder can decode it + const hexString = ethers.utils.hexlify(data); + + const stratsToUpkeep = ethers.utils.defaultAbiCoder.decode( + ["address[]"], + ethers.utils.hexDataSlice(hexString, 0) + ) as [string[]]; + + expect(stratsToUpkeep[0].length).to.be.eq(1, "checkUpkeep thinks more than one strategy requires upkeeping"); + expect(stratsToUpkeep[0][0]).to.be.eq(strategy.address, "wrong strategy address returned"); + }); + }); + + describe("performUpkeep", () => { + let pass: boolean; + + it("Should prevent rebalance when within range", async () => { + const {keeper, governance, strategy, stratAddrEncoded} = await loadFixture(setupSingleStrategyFixture); + const upperTickBefore = await strategy.tick_upper(); + await keeper + .connect(governance) + .performUpkeep(stratAddrEncoded) + .catch(() => {}); + const upperTickAfter = await strategy.tick_upper(); + expect(upperTickBefore).to.be.eq(upperTickAfter, "Rebalance happened while within range"); + }); + + it("Should prevent rebalance when strategy not added to watchlist", async () => { + const {keeper, governance, strategy, stratAddrEncoded, weth, strat1TokenOut, alice, router} = await loadFixture( + setupSingleStrategyFixture + ); + // Push strategy out of range + await pushOutOfRange(strategy, weth, strat1TokenOut, alice, router, strat1WethAmount); + + pass = false; + await keeper.connect(governance).removeStrategy(strategy.address); + await keeper + .connect(governance) + .performUpkeep(stratAddrEncoded) + .catch(() => { + pass = true; + }); + expect(pass).to.be.eq(true, "Rebalanced a non-watched strategy"); + expect(await shouldRebalance(strategy.address)).to.be.eq(true, "Rebalanced a non-watched strategy"); + }); + + it("Should prevent rebalance when keeper is disabled", async () => { + pass = false; + const {keeper, governance, strategy, stratAddrEncoded, weth, strat1TokenOut, alice, router} = await loadFixture( + setupSingleStrategyFixture + ); + await pushOutOfRange(strategy, weth, strat1TokenOut, alice, router, strat1WethAmount); + await keeper.connect(governance).setDisabled(true); + expect(await keeper.disabled()).to.be.eq(true, "governance failed to disable the keeper"); + await keeper + .connect(governance) + .performUpkeep(stratAddrEncoded) + .catch(() => { + pass = true; + }); + expect(pass).to.be.eq(true, "Rebalanced while keeper is disabled"); + expect(await shouldRebalance(strategy.address)).to.be.eq(true, "Rebalanced while keeper is disabled"); + }); + + it("Should perform a rebalance successfully", async () => { + const {keeper, governance, strategy, weth, strat1TokenOut, alice, router} = await loadFixture( + setupSingleStrategyFixture + ); + await pushOutOfRange(strategy, weth, strat1TokenOut, alice, router, strat1WethAmount); + const [, data] = await keeper.checkUpkeep("0x"); + await keeper + .connect(governance) + .performUpkeep(data) + .catch(() => {}); + expect(await shouldRebalance(strategy.address)).to.be.eq(false, "Rebalance unsuccessful"); + }); + + it("Should rebalance multiple strategies successfully", async () => { + const {alice, governance, weth, strategy1, strategy2, strat1TokenOut, strat2TokenOut, router, keeper} = + await loadFixture(setupDoubleStrategiesFixture); + await pushOutOfRange(strategy1, weth, strat1TokenOut, alice, router, strat1WethAmount); + await pushOutOfRange(strategy2, weth, strat2TokenOut, alice, router, strat2WethAmount); + const [, data] = await keeper.checkUpkeep("0x"); + + await keeper + .connect(governance) + .performUpkeep(data) + .catch(() => {}); + expect(await shouldRebalance(strategy1.address)).to.be.eq(false, "Strategy1 rebalance unsuccessful"); + expect(await shouldRebalance(strategy2.address)).to.be.eq(false, "Strategy2 rebalance unsuccessful"); + }); + }); + + const shouldRebalance = async (stratAddr: string): Promise => { + const strategyContract = await getContractAt(uniV3StrategyContractName, stratAddr); + const poolContract = await ethers.getContractAt(poolAbi, await strategyContract.pool()); + + const upperTick = await strategyContract.tick_upper(); + const lowerTick = await strategyContract.tick_lower(); + const range = upperTick - lowerTick; + const limitVar = range / 10; + const lowerLimit = lowerTick + limitVar; + const upperLimit = upperTick - limitVar; + const [, currentTick] = await poolContract.slot0(); + + let shouldRebalance = false; + if (currentTick < lowerLimit || currentTick > upperLimit) shouldRebalance = true; + + return shouldRebalance; + }; + + const pushOutOfRange = async ( + strategy: Contract, + weth: Contract, + tokenOut: string, + alice: SignerWithAddress, + router: Contract, + wethAmount: BigNumber + ) => { + const poolContract = await ethers.getContractAt(poolAbi, await strategy.pool()); + + const fee = await poolContract.fee(); + const pathEncoded = ethers.utils.solidityPack(["address", "uint24", "address"], [weth.address, fee, tokenOut]); + const exactInputParams = [pathEncoded, alice.address, wethAmount, 0]; + + //console.log("Performing swap (be patient!)"); + const allowance = await weth.allowance(alice.address, router.address); + if (allowance.lt(wethAmount)) { + await weth.connect(alice).approve(router.address, 0); + await weth.connect(alice).approve(router.address, ethers.constants.MaxUint256); + } + await router.connect(alice).exactInput(exactInputParams); + //console.log("Swap successful"); + + // Forward blocks a bit so strategy.determineTicks() can adjust properly + await mine(1000); + + expect(await shouldRebalance(strategy.address)).to.be.eq( + true, + "Couldn't push strategy out of balance. Consider a larger trade size (Hint: increase wethAmount)" + ); + }; +}); + +process.on("unhandledRejection", (err) => { + console.log(err); + process.exit(1); +}); From 7eec3304d22d8cb1eef356e309cf596875dcb13b Mon Sep 17 00:00:00 2001 From: mard Date: Wed, 5 Oct 2022 22:14:33 +0800 Subject: [PATCH 5/9] add multiple strats to keeper watchlist at once better watchlist inspection --- src/optimism/chainlinkKeeper.sol | 17 +++++++++++------ src/tests/optimism/chainlinkKeeper.test.ts | 10 +++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/optimism/chainlinkKeeper.sol b/src/optimism/chainlinkKeeper.sol index e5913fac..ed1c9aaf 100644 --- a/src/optimism/chainlinkKeeper.sol +++ b/src/optimism/chainlinkKeeper.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.16; +pragma solidity ^0.8.0; import "./interfaces/chainlink/AutomationCompatibleInterface.sol"; import "./interfaces/strategyv2.sol"; import "./interfaces/univ3/pool/IUniswapV3PoolState.sol"; - contract PickleRebalancingKeeper is AutomationCompatibleInterface { address[] public strategies; address public keeperRegistry = 0x75c0530885F385721fddA23C539AF3701d6183D4; @@ -28,6 +27,10 @@ contract PickleRebalancingKeeper is AutomationCompatibleInterface { governance = _governance; } + function strategiesLength() external view returns(uint256 length) { + length = strategies.length; + } + function setGovernance(address _governance) external onlyGovernance { governance = _governance; } @@ -44,9 +47,11 @@ contract PickleRebalancingKeeper is AutomationCompatibleInterface { disabled = _disabled; } - function addStrategy(address _address) external onlyGovernance { - require(!_search(_address), "Address Already Watched"); - strategies.push(_address); + function addStrategies(address[] calldata _addresses) external onlyGovernance { + for (uint256 i = 0; i < _addresses.length; i++) { + require(!_search(_addresses[i]), "Address Already Watched"); + strategies.push(_addresses[i]); + } } function removeStrategy(address _address) external onlyGovernance { @@ -82,7 +87,7 @@ contract PickleRebalancingKeeper is AutomationCompatibleInterface { if (upkeepNeeded == true) { address[] memory stratsToUpkeep = new address[](counter); - for (uint i = 0; i < counter; i++) { + for (uint256 i = 0; i < counter; i++) { stratsToUpkeep[i] = _stratsToUpkeep[i]; } performData = abi.encode(stratsToUpkeep); diff --git a/src/tests/optimism/chainlinkKeeper.test.ts b/src/tests/optimism/chainlinkKeeper.test.ts index 11933cad..5425e952 100644 --- a/src/tests/optimism/chainlinkKeeper.test.ts +++ b/src/tests/optimism/chainlinkKeeper.test.ts @@ -48,7 +48,7 @@ describe(`PickleRebalancingKeeper`, () => { const keeper = await deployContract(keeperContractName, governance.address); // Add strategy to keeper watch-list - await keeper.connect(governance).addStrategy(strategy.address); + await keeper.connect(governance).addStrategies([strategy.address]); // Set alice weth balance so it can push strategy out of range await setBalance(alice.address, strat1WethAmount.add(ethers.utils.parseEther("1"))); @@ -78,7 +78,7 @@ describe(`PickleRebalancingKeeper`, () => { const strat2TokenOut: string = weth.address.toLowerCase() === token0.toLowerCase() ? token1 : token0; // Add strategy to keeper watch-list - await keeper.connect(governance).addStrategy(strategy2.address); + await keeper.connect(governance).addStrategies([strategy2.address]); // Set alice weth balance so it can push both strategies out of range await setBalance(alice.address, strat2WethAmount.add(ethers.utils.parseEther("1"))); @@ -127,7 +127,7 @@ describe(`PickleRebalancingKeeper`, () => { const {keeper, alice} = await loadFixture(setupSingleStrategyFixture); await keeper .connect(alice) - .addStrategy(uniV3Strategy2Address) + .addStrategies([uniV3Strategy2Address]) .catch(() => { pass = true; }); @@ -139,7 +139,7 @@ describe(`PickleRebalancingKeeper`, () => { const {keeper, governance, strategy} = await loadFixture(setupSingleStrategyFixture); await keeper .connect(governance) - .addStrategy(strategy.address) + .addStrategies([strategy.address]) .catch(() => { pass = true; }); @@ -160,7 +160,7 @@ describe(`PickleRebalancingKeeper`, () => { it("Should add a new strategy correctly", async () => { const {keeper, governance} = await loadFixture(setupSingleStrategyFixture); - await keeper.connect(governance).addStrategy(uniV3Strategy2Address).catch(); + await keeper.connect(governance).addStrategies([uniV3Strategy2Address]).catch(); const newStratAddress = await keeper.strategies(1); expect(newStratAddress).to.be.eq(uniV3Strategy2Address, "Failed adding new strategy"); }); From b7894cce0f7a969baf8d7c7fe856db0ce2c230f0 Mon Sep 17 00:00:00 2001 From: mard Date: Wed, 5 Oct 2022 22:49:35 +0800 Subject: [PATCH 6/9] add beetx ib-reth strategy --- .gitignore | 4 +- .../beethovenx/strategy-beetx-ib-reth.sol | 49 +++++ .../beethovenx/strategy-beetx-ib-reth.test.ts | 19 ++ .../optimism/beethovenx/strategyBeetxBase.ts | 205 ++++-------------- 4 files changed, 117 insertions(+), 160 deletions(-) create mode 100644 src/strategies/optimism/beethovenx/strategy-beetx-ib-reth.sol create mode 100644 src/tests/strategies/optimism/beethovenx/strategy-beetx-ib-reth.test.ts diff --git a/.gitignore b/.gitignore index 3b7ffb4d..c7090348 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,6 @@ out.sol .vscode out.sol -pickleJarOut.sol \ No newline at end of file +pickleJarOut.sol + +.vim \ No newline at end of file diff --git a/src/strategies/optimism/beethovenx/strategy-beetx-ib-reth.sol b/src/strategies/optimism/beethovenx/strategy-beetx-ib-reth.sol new file mode 100644 index 00000000..53049934 --- /dev/null +++ b/src/strategies/optimism/beethovenx/strategy-beetx-ib-reth.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.6; + +import "./strategy-beetx-base.sol"; + +contract StrategyBeetxIbRethLp is StrategyBeetxBase { + bytes32 private _vaultPoolId = 0x785f08fb77ec934c01736e30546f87b4daccbe50000200000000000000000041; + address private _lp = 0x785F08fB77ec934c01736E30546f87B4daccBe50; + address private _gauge = 0x1C438149E3e210233FCE91eeE1c097d34Fd655c2; + + constructor( + address _governance, + address _strategist, + address _controller, + address _timelock + ) StrategyBeetxBase(_vaultPoolId, _gauge, _lp, _governance, _strategist, _controller, _timelock) { + // Pool IDs + bytes32 ethReth = 0x4fd63966879300cafafbb35d157dc5229278ed2300020000000000000000002b; + bytes32 ethIb = 0xefb0d9f51efd52d7589a9083a6d0ca4de416c24900020000000000000000002c; + + // Tokens addresses + address ib = 0x00a35FD824c717879BF370E70AC6868b95870Dfb; + address reth = 0x9Bcef72be871e61ED4fBbc7630889beE758eb81D; + + // Rewards toNativeRoutes (Need a valid route for every reward token) // + // IB->ETH + // ETH-IB + bytes32[] memory _ibToNativePoolIds = new bytes32[](1); + _ibToNativePoolIds[0] = ethIb; + address[] memory _ibToNativeTokenPath = new address[](2); + _ibToNativeTokenPath[0] = ib; + _ibToNativeTokenPath[1] = native; + _addToNativeRoute(_ibToNativePoolIds, _ibToNativeTokenPath); + + // Pool tokens toTokenRoutes (Only need one token route) // + // ETH->RETH + // ETH-RETH + bytes32[] memory _toRethPoolIds = new bytes32[](1); + _toRethPoolIds[0] = ethReth; + address[] memory _toRethTokenPath = new address[](2); + _toRethTokenPath[0] = native; + _toRethTokenPath[1] = reth; + _addToTokenRoute(_toRethPoolIds, _toRethTokenPath); + } + + function getName() external pure override returns (string memory) { + return "StrategyBeetxIbRethLp"; + } +} diff --git a/src/tests/strategies/optimism/beethovenx/strategy-beetx-ib-reth.test.ts b/src/tests/strategies/optimism/beethovenx/strategy-beetx-ib-reth.test.ts new file mode 100644 index 00000000..12938fb4 --- /dev/null +++ b/src/tests/strategies/optimism/beethovenx/strategy-beetx-ib-reth.test.ts @@ -0,0 +1,19 @@ +import "@nomicfoundation/hardhat-toolbox"; +import { getWantFromWhale } from "../../../utils/setupHelper"; +import { doTestBehaviorBase } from "./strategyBeetxBase"; +import { ethers } from "hardhat"; +import { toWei } from "../../../utils/testHelper"; + + +describe("StrategyBeetxIbRethLp", () => { + const want_addr = "0x785F08fB77ec934c01736E30546f87B4daccBe50"; + const whale_addr = "0x4fbe899d37fb7514adf2f41B0630E018Ec275a0C"; + const reward_token_addr = "0xFE8B128bA8C78aabC59d4c64cEE7fF28e9379921"; + + before("Get want token", async () => { + const [alice] = await ethers.getSigners(); + await getWantFromWhale(want_addr, toWei(11, 16), alice, whale_addr); + }); + + doTestBehaviorBase("StrategyBeetxIbRethLp", want_addr, reward_token_addr, 5); +}); diff --git a/src/tests/strategies/optimism/beethovenx/strategyBeetxBase.ts b/src/tests/strategies/optimism/beethovenx/strategyBeetxBase.ts index 1c7f2c90..e14a0618 100644 --- a/src/tests/strategies/optimism/beethovenx/strategyBeetxBase.ts +++ b/src/tests/strategies/optimism/beethovenx/strategyBeetxBase.ts @@ -1,15 +1,10 @@ import "@nomicfoundation/hardhat-toolbox"; -import { ethers, network } from "hardhat"; -import { - expect, - increaseTime, - getContractAt, - increaseBlock, -} from "../../../utils/testHelper"; -import { setup } from "../../../utils/setupHelper"; -import { NULL_ADDRESS } from "../../../utils/constants"; -import { BigNumber, Contract } from "ethers"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import {ethers, network} from "hardhat"; +import {expect, increaseTime, getContractAt, increaseBlock} from "../../../utils/testHelper"; +import {setup} from "../../../utils/setupHelper"; +import {NULL_ADDRESS} from "../../../utils/constants"; +import {BigNumber, Contract} from "ethers"; +import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; export const doTestBehaviorBase = ( strategyName: string, @@ -20,9 +15,7 @@ export const doTestBehaviorBase = ( isPolygon = false, blocktime = 5 ) => { - let alice: SignerWithAddress, - want: Contract, - native: Contract; + let alice: SignerWithAddress, want: Contract, native: Contract; let strategy: Contract, pickleJar: Contract, controller: Contract; let governance: SignerWithAddress, strategist: SignerWithAddress, @@ -56,25 +49,16 @@ export const doTestBehaviorBase = ( }); it("Should set the timelock correctly", async () => { - expect(await strategy.timelock()).to.be.eq( - timelock.address, - "timelock is incorrect" - ); + expect(await strategy.timelock()).to.be.eq(timelock.address, "timelock is incorrect"); await strategy.setTimelock(NULL_ADDRESS); - expect(await strategy.timelock()).to.be.eq( - NULL_ADDRESS, - "timelock is incorrect" - ); + expect(await strategy.timelock()).to.be.eq(NULL_ADDRESS, "timelock is incorrect"); }); it("Should withdraw correctly", async () => { const _want: BigNumber = await want.balanceOf(alice.address); await want.approve(pickleJar.address, _want); await pickleJar.deposit(_want); - console.log( - "Alice pTokenBalance after deposit: %s\n", - (await pickleJar.balanceOf(alice.address)).toString() - ); + console.log("Alice pTokenBalance after deposit: %s\n", (await pickleJar.balanceOf(alice.address)).toString()); await pickleJar.earn(); await increaseTime(60 * 60 * 24 * days); //travel days into the future @@ -82,47 +66,29 @@ export const doTestBehaviorBase = ( await increaseBlock((60 * 60 * 24 * days) / blocktime); //roughly days } - console.log( - "\nRatio before harvest: ", - (await pickleJar.getRatio()).toString() - ); + console.log("\nRatio before harvest: ", (await pickleJar.getRatio()).toString()); await strategy.harvest(); - console.log( - "Ratio after harvest: %s", - (await pickleJar.getRatio()).toString() - ); + console.log("Ratio after harvest: %s", (await pickleJar.getRatio()).toString()); let _before: BigNumber = await want.balanceOf(pickleJar.address); - console.log( - "\nPicklejar balance before controller withdrawal: ", - _before.toString() - ); + console.log("\nPicklejar balance before controller withdrawal: ", _before.toString()); await controller.withdrawAll(want.address); let _after: BigNumber = await want.balanceOf(pickleJar.address); - console.log( - "Picklejar balance after controller withdrawal: ", - _after.toString() - ); + console.log("Picklejar balance after controller withdrawal: ", _after.toString()); expect(_after).to.be.gt(_before, "controller withdrawAll failed"); _before = await want.balanceOf(alice.address); - console.log( - "\nAlice balance before picklejar withdrawal: ", - _before.toString() - ); + console.log("\nAlice balance before picklejar withdrawal: ", _before.toString()); await pickleJar.withdrawAll(); _after = await want.balanceOf(alice.address); - console.log( - "Alice balance after picklejar withdrawal: ", - _after.toString() - ); + console.log("Alice balance after picklejar withdrawal: ", _after.toString()); expect(_after).to.be.gt(_before, "picklejar withdrawAll failed"); expect(_after).to.be.gt(_want, "no interest earned"); @@ -132,18 +98,14 @@ export const doTestBehaviorBase = ( const _want: BigNumber = await want.balanceOf(alice.address); await want.approve(pickleJar.address, _want); await pickleJar.deposit(_want); - console.log( - "Alice pTokenBalance after deposit: %s\n", - (await pickleJar.balanceOf(alice.address)).toString() - ); + console.log("Alice pTokenBalance after deposit: %s\n", (await pickleJar.balanceOf(alice.address)).toString()); await pickleJar.earn(); await increaseTime(60 * 60 * 24 * days); //travel days into the future if (bIncreaseBlock) { await increaseBlock((60 * 60 * 24 * days) / blocktime); //roughly days } - const pendingRewards: [string[], BigNumber[]] = - await strategy.getHarvestable(); + const pendingRewards: [string[], BigNumber[]] = await strategy.getHarvestable(); const _before: BigNumber = await pickleJar.balance(); let _treasuryBefore: BigNumber = await native.balanceOf(treasury.address); @@ -152,62 +114,35 @@ export const doTestBehaviorBase = ( console.log(`\t${addr}: ${pendingRewards[1][i].toString()}`); }); console.log("Picklejar balance before harvest: ", _before.toString()); - console.log( - "šŸ’ø Treasury reward token balance before harvest: ", - _treasuryBefore.toString() - ); - console.log( - "\nRatio before harvest: ", - (await pickleJar.getRatio()).toString() - ); + console.log("šŸ’ø Treasury reward token balance before harvest: ", _treasuryBefore.toString()); + console.log("\nRatio before harvest: ", (await pickleJar.getRatio()).toString()); await strategy.harvest(); const _after: BigNumber = await pickleJar.balance(); let _treasuryAfter: BigNumber = await native.balanceOf(treasury.address); - console.log( - "Ratio after harvest: ", - (await pickleJar.getRatio()).toString() - ); + console.log("Ratio after harvest: ", (await pickleJar.getRatio()).toString()); console.log("\nPicklejar balance after harvest: ", _after.toString()); - console.log( - "šŸ’ø Treasury reward token balance after harvest: ", - _treasuryAfter.toString() - ); + console.log("šŸ’ø Treasury reward token balance after harvest: ", _treasuryAfter.toString()); //performance fee is given const rewardsEarned = _treasuryAfter.sub(_treasuryBefore); - console.log( - "\nPerformance fee earned by treasury: ", - rewardsEarned.toString() - ); + console.log("\nPerformance fee earned by treasury: ", rewardsEarned.toString()); expect(rewardsEarned).to.be.gt(0, "no performance fee taken"); //withdraw const _devBefore: BigNumber = await want.balanceOf(devfund.address); _treasuryBefore = await want.balanceOf(treasury.address); - console.log( - "\nšŸ‘Øā€šŸŒ¾ Dev balance before picklejar withdrawal: ", - _devBefore.toString() - ); - console.log( - "šŸ’ø Treasury balance before picklejar withdrawal: ", - _treasuryBefore.toString() - ); + console.log("\nšŸ‘Øā€šŸŒ¾ Dev balance before picklejar withdrawal: ", _devBefore.toString()); + console.log("šŸ’ø Treasury balance before picklejar withdrawal: ", _treasuryBefore.toString()); await pickleJar.withdrawAll(); const _devAfter: BigNumber = await want.balanceOf(devfund.address); _treasuryAfter = await want.balanceOf(treasury.address); - console.log( - "\nšŸ‘Øā€šŸŒ¾ Dev balance after picklejar withdrawal: ", - _devAfter.toString() - ); - console.log( - "šŸ’ø Treasury balance after picklejar withdrawal: ", - _treasuryAfter.toString() - ); + console.log("\nšŸ‘Øā€šŸŒ¾ Dev balance after picklejar withdrawal: ", _devAfter.toString()); + console.log("šŸ’ø Treasury balance after picklejar withdrawal: ", _treasuryAfter.toString()); //0% goes to dev const _devFund = _devAfter.sub(_devBefore); @@ -219,9 +154,7 @@ export const doTestBehaviorBase = ( }); it("Should perform multiple deposits and withdrawals correctly", async () => { - const _wantHalved: BigNumber = (await want.balanceOf(alice.address)).div( - 2 - ); + const _wantHalved: BigNumber = (await want.balanceOf(alice.address)).div(2); await want.connect(alice).transfer(strategist.address, _wantHalved); await want.connect(alice).approve(pickleJar.address, _wantHalved); await want.connect(strategist).approve(pickleJar.address, _wantHalved); @@ -248,51 +181,26 @@ export const doTestBehaviorBase = ( await pickleJar.connect(strategist).withdrawAll(); let _aliceBalanceAfter: BigNumber = await want.balanceOf(alice.address); - let _strategistBalanceAfter: BigNumber = await want.balanceOf( - strategist.address - ); - console.log( - "\nAlice balance after half withdrawal: %s\n", - _aliceBalanceAfter.toString() - ); - console.log( - "\nStrategist balance after half withdrawal: %s\n", - _strategistBalanceAfter.toString() - ); + let _strategistBalanceAfter: BigNumber = await want.balanceOf(strategist.address); + console.log("\nAlice balance after half withdrawal: %s\n", _aliceBalanceAfter.toString()); + console.log("\nStrategist balance after half withdrawal: %s\n", _strategistBalanceAfter.toString()); - expect(_aliceBalanceAfter).to.be.approximately( - _wantHalved.div(2), - 1, - "Alice withdrawal amount incorrect" - ); + expect(_aliceBalanceAfter).to.be.approximately(_wantHalved.div(2), 1, "Alice withdrawal amount incorrect"); - expect(_strategistBalanceAfter).to.be.approximately( - _wantHalved, - 1, - "Strategist withdrawal amount incorrect" - ); + expect(_strategistBalanceAfter).to.be.approximately(_wantHalved, 1, "Strategist withdrawal amount incorrect"); // Alice withdraws remainder await pickleJar.connect(alice).withdrawAll(); _aliceBalanceAfter = await want.balanceOf(alice.address); - console.log( - "\nAlice balance after full withdrawal: %s\n", - _aliceBalanceAfter.toString() - ); - expect(_aliceBalanceAfter).to.be.approximately( - _wantHalved, - 1, - "Alice withdrawal amount incorrect" - ); + console.log("\nAlice balance after full withdrawal: %s\n", _aliceBalanceAfter.toString()); + expect(_aliceBalanceAfter).to.be.approximately(_wantHalved, 1, "Alice withdrawal amount incorrect"); }); it("should add and remove rewards correctly", async () => { - const beetsBalOp = - "0xd6e5824b54f64ce6f1161210bc17eebffc77e031000100000000000000000006"; - const ethOpUsdc = - "0x39965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003"; - + const beetsBalOp = "0xd6e5824b54f64ce6f1161210bc17eebffc77e031000100000000000000000006"; + const ethOpUsdc = "0x39965c9dab5448482cf7e002f583c812ceb53046000100000000000000000003"; + // Addresses and pool IDs const op = "0x4200000000000000000000000000000000000042"; const usdc = "0x7F5c764cBc14f9669B88837ca1490cCa17c31607"; @@ -304,31 +212,20 @@ export const doTestBehaviorBase = ( [beetsBalOp, ethOpUsdc], [reward_addr, op, native.address], ]; - const invalidToNativeRoute = [ - [ethOpUsdc], - [native.address, notRewardToken], - ]; + const invalidToNativeRoute = [[ethOpUsdc], [native.address, notRewardToken]]; // Arbitrary new reward route const arbNewRoute = [[ethOpUsdc], [notRewardToken, native.address]]; // Add reward tokens - const rewardsBeforeAdd: string[] = - await strategy.getActiveRewardsTokens(); + const rewardsBeforeAdd: string[] = await strategy.getActiveRewardsTokens(); await strategy.connect(alice).addToNativeRoute(...validToNativeRoute); await strategy.connect(alice).addToNativeRoute(...arbNewRoute); const rewardsAfterAdd: string[] = await strategy.getActiveRewardsTokens(); - expect(rewardsAfterAdd.length).to.be.eq( - rewardsBeforeAdd.length + 1, - "Adding reward failed" - ); - expect( - rewardsAfterAdd.filter( - (z) => z.toLowerCase() === reward_addr.toLowerCase() - ).length - ).to.be.eq( + expect(rewardsAfterAdd.length).to.be.eq(rewardsBeforeAdd.length + 1, "Adding reward failed"); + expect(rewardsAfterAdd.filter((z) => z.toLowerCase() === reward_addr.toLowerCase()).length).to.be.eq( 1, "Updating reward path results in redundance in activeRewardsTokens" ); @@ -341,10 +238,7 @@ export const doTestBehaviorBase = ( } catch (e) { callReverted = true; } - expect(callReverted).to.be.eq( - true, - "Strategy accepts adding reward with wrong route" - ); + expect(callReverted).to.be.eq(true, "Strategy accepts adding reward with wrong route"); // Perform a harvest and ensure it is successful const _want: BigNumber = await want.balanceOf(alice.address); @@ -361,20 +255,13 @@ export const doTestBehaviorBase = ( await strategy.harvest(); const ratioAfter = await pickleJar.getRatio(); - expect(ratioAfter).to.be.gt( - ratioBefore, - "Harvest failed after setting new toNative route" - ); + expect(ratioAfter).to.be.gt(ratioBefore, "Harvest failed after setting new toNative route"); // Remove a reward token await strategy.connect(alice).deactivateReward(reward_addr); - const rewardsAfterRemove: string[] = - await strategy.getActiveRewardsTokens(); + const rewardsAfterRemove: string[] = await strategy.getActiveRewardsTokens(); - expect(rewardsAfterRemove.length).to.be.eq( - rewardsAfterAdd.length - 1, - "Deactivating reward failed" - ); + expect(rewardsAfterRemove.length).to.be.eq(rewardsAfterAdd.length - 1, "Deactivating reward failed"); }); beforeEach(async () => { From 62a724104d1bfe0a3bd6a1348c5dcecd9f9b5a42 Mon Sep 17 00:00:00 2001 From: mard Date: Sat, 29 Oct 2022 19:01:09 +0800 Subject: [PATCH 7/9] fix: fix strategist address for v3 keeper test --- src/tests/optimism/chainlinkKeeper.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/optimism/chainlinkKeeper.test.ts b/src/tests/optimism/chainlinkKeeper.test.ts index 5425e952..dfdd01db 100644 --- a/src/tests/optimism/chainlinkKeeper.test.ts +++ b/src/tests/optimism/chainlinkKeeper.test.ts @@ -87,7 +87,7 @@ describe(`PickleRebalancingKeeper`, () => { await wethTmp.connect(alice).deposit({value: strat2WethAmount}); // Add keeper to strategy2 harvesters list - const stratStrategistAddr = await strategy.governance(); + const stratStrategistAddr = await strategy2.governance(); const stratStrategist = await unlockAccount(stratStrategistAddr); await strategy2.connect(stratStrategist).whitelistHarvesters([keeper.address]); From 5d7b3875e82775b98aa0fb772534635bdaac1ff8 Mon Sep 17 00:00:00 2001 From: mard Date: Wed, 9 Nov 2022 01:58:40 +0800 Subject: [PATCH 8/9] fix: chainlinkKeeper test typo --- src/tests/optimism/chainlinkKeeper.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/optimism/chainlinkKeeper.test.ts b/src/tests/optimism/chainlinkKeeper.test.ts index dfdd01db..3a69aa6a 100644 --- a/src/tests/optimism/chainlinkKeeper.test.ts +++ b/src/tests/optimism/chainlinkKeeper.test.ts @@ -57,7 +57,7 @@ describe(`PickleRebalancingKeeper`, () => { await wethTmp.connect(alice).deposit({value: strat1WethAmount}); // Add keeper to the strategy harvesters list - const stratStrategistAddr = await strategy.governance(); + const stratStrategistAddr = await strategy.strategist(); const stratStrategist = await unlockAccount(stratStrategistAddr); await strategy.connect(stratStrategist).whitelistHarvesters([keeper.address]); @@ -87,7 +87,7 @@ describe(`PickleRebalancingKeeper`, () => { await wethTmp.connect(alice).deposit({value: strat2WethAmount}); // Add keeper to strategy2 harvesters list - const stratStrategistAddr = await strategy2.governance(); + const stratStrategistAddr = await strategy2.strategist(); const stratStrategist = await unlockAccount(stratStrategistAddr); await strategy2.connect(stratStrategist).whitelistHarvesters([keeper.address]); From 9a9ae174fd82ab0dfccce3a39d7d4406afe1a847 Mon Sep 17 00:00:00 2001 From: mard Date: Wed, 9 Nov 2022 02:02:01 +0800 Subject: [PATCH 9/9] Minichef proxy contract --- src/optimism/minichefProxy.sol | 187 +++++++++++++++++++++++ src/tests/optimism/minichefProxy.test.ts | 144 +++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 src/optimism/minichefProxy.sol create mode 100644 src/tests/optimism/minichefProxy.test.ts diff --git a/src/optimism/minichefProxy.sol b/src/optimism/minichefProxy.sol new file mode 100644 index 00000000..4a577a01 --- /dev/null +++ b/src/optimism/minichefProxy.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +struct PoolInfo { + uint128 accPicklePerShare; + uint64 lastRewardTime; + uint64 allocPoint; +} + +interface IMiniChef { + function add( + uint256 allocPoint, + address _lpToken, + address _rewarder + ) external; + + function set( + uint256 _pid, + uint256 _allocPoint, + address _rewarder, + bool overwrite + ) external; + + function setPicklePerSecond(uint256 _picklePerSecond) external; + + function updatePool(uint256 pid) external returns (PoolInfo memory pool); + + function massUpdatePools(uint256[] calldata pids) external; + + function poolLength() external view returns (uint256 pools); + + function transferOwnership( + address newOwner, + bool direct, + bool renounce + ) external; + + function claimOwnership() external; +} + +interface IRewarder { + function setRewardPerSecond(uint256 _rewardPerSecond) external; + + function add(uint256 allocPoint, uint256 _pid) external; + + function set(uint256 _pid, uint256 _allocPoint) external; + + function updatePool(uint256 pid) external returns (PoolInfo memory pool); + + function massUpdatePools(uint256[] calldata pids) external; + + function transferOwnership( + address newOwner, + bool direct, + bool renounce + ) external; + + function claimOwnership() external; +} + +contract ChefProxy { + address public governance; + address public pendingGovernance; + + address[] public strategists; + mapping(address => bool) public isStrategist; + + IMiniChef public MINICHEF; + IRewarder public REWARDER; + + modifier onlyGovernance() { + require(msg.sender == governance, "!Governance"); + _; + } + + modifier onlyStrategist() { + require(isStrategist[msg.sender], "!Strategist"); + _; + } + + constructor(IMiniChef _minichef, IRewarder _rewarder) { + governance = msg.sender; + MINICHEF = _minichef; + REWARDER = _rewarder; + } + + function setPendingGovernance(address _newGovernance) external onlyGovernance { + pendingGovernance = _newGovernance; + } + + function claimGovernance() external { + require(msg.sender == pendingGovernance, "!pendingGovernance"); + governance = pendingGovernance; + pendingGovernance = address(0); + } + + function addStrategist(address _newStrategist) external onlyGovernance { + require(!isStrategist[msg.sender], "Already a strategist"); + + strategists.push(_newStrategist); + isStrategist[_newStrategist] = true; + } + + function removeStrategist(address _strategist) external onlyGovernance { + require(isStrategist[_strategist], "!Strategist"); + + for (uint256 i = 0; i < strategists.length; i++) { + if (strategists[i] == _strategist) { + strategists[i] = strategists[strategists.length - 1]; + strategists.pop(); + break; + } + } + isStrategist[_strategist] = false; + } + + function setMinichef(IMiniChef _newMinichef) external onlyGovernance { + MINICHEF = _newMinichef; + } + + function setRewarder(IRewarder _newRewarder) external onlyGovernance { + REWARDER = _newRewarder; + } + + ///@notice set an address as pendingOwner on the minichef + function transferMinichefOwnership(address _newOwner) external onlyGovernance { + MINICHEF.transferOwnership(_newOwner, false, false); + } + + ///@notice claims ownership of the minichef + function claimMinichefOwnership() external onlyGovernance { + MINICHEF.claimOwnership(); + } + + ///@notice set an address as pendingOwner on the rewarder + function transferRewarderOwnership(address _newOwner) external onlyGovernance { + REWARDER.transferOwnership(_newOwner, false, false); + } + + ///@notice claims ownership of the rewarder + function claimRewarderOwnership() external onlyGovernance { + REWARDER.claimOwnership(); + } + + function setPicklePerSecond(uint256 _picklePerSecond) external onlyStrategist { + MINICHEF.setPicklePerSecond(_picklePerSecond); + } + + function setRewardPerSecond(uint256 _rewardPerSecond) external onlyStrategist { + REWARDER.setRewardPerSecond(_rewardPerSecond); + } + + ///@notice Add multiple LPs to minichef and rewarder + function add(address[] calldata _lpTokens, uint256[] calldata _allocPoints) external onlyStrategist { + require(_lpTokens.length == _allocPoints.length, "!match"); + + uint256 poolLength = MINICHEF.poolLength(); + + for (uint256 i = 0; i < _lpTokens.length; i++) { + MINICHEF.add(_allocPoints[i], _lpTokens[i], address(REWARDER)); + REWARDER.add(_allocPoints[i], poolLength + i); + } + } + + ///@notice Update the allocPoints for multiple pools on minichef and rewarder + function set(uint256[] calldata _pids, uint256[] calldata _allocPoints) external onlyStrategist { + require(_pids.length == _allocPoints.length, "!match"); + + for (uint256 i = 0; i < _pids.length; i++) { + MINICHEF.set(_pids[i], _allocPoints[i], address(REWARDER), false); + REWARDER.set(_pids[i], _allocPoints[i]); + } + } + + ///@notice An emergency function for the governance to execute calls that are not supported by this contract (e.g, renounce chef ownership to address(0)) + function execute( + address target, + string calldata signature, + bytes calldata data + ) external onlyGovernance returns (bytes memory returnData) { + bytes memory callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + + bool success; + (success, returnData) = target.call(callData); + require(success, "execute failed"); + } +} diff --git a/src/tests/optimism/minichefProxy.test.ts b/src/tests/optimism/minichefProxy.test.ts new file mode 100644 index 00000000..2bdb886f --- /dev/null +++ b/src/tests/optimism/minichefProxy.test.ts @@ -0,0 +1,144 @@ +import "@nomicfoundation/hardhat-toolbox"; +import {ethers} from "hardhat"; +import {setBalance, loadFixture, mine} from "@nomicfoundation/hardhat-network-helpers"; +import {expect, getContractAt, deployContract, toWei, unlockAccount} from "../utils/testHelper"; +import {BigNumber, Contract} from "ethers"; +import {SignerWithAddress} from "@nomiclabs/hardhat-ethers/signers"; + +describe("MiniChefController", () => { + const setupFixture = async () => { + const [alice, governance, strategist] = await ethers.getSigners(); + + const pickle = await deployContract("src/yield-farming/pickle-token.sol:PickleToken"); + const reward = await deployContract("src/yield-farming/pickle-token.sol:PickleToken"); + await pickle.mint(governance.address, toWei(10_000)); + await reward.mint(governance.address, toWei(10_000)); + + const minichef = await deployContract("src/optimism/minichefv2.sol:MiniChefV2", pickle.address); + const rewarder = await deployContract( + "src/optimism/PickleRewarder.sol:PickleRewarder", + reward.address, + 0, + minichef.address + ); + + const jar1 = await deployContract("src/yield-farming/pickle-token.sol:PickleToken"); + const jar2 = await deployContract("src/yield-farming/pickle-token.sol:PickleToken"); + await jar1.mint(alice.address, toWei(10_000)); + await jar2.mint(alice.address, toWei(10_000)); + + // Deploy the controller and transfer governance + const miniChefController = await deployContract( + "src/optimism/minichefProxy.sol:ChefProxy", + minichef.address, + rewarder.address + ); + await miniChefController.setPendingGovernance(governance.address); + await miniChefController.connect(governance).claimGovernance(); + + // Transfer minichef & rewarder ownership to the controller + await minichef.transferOwnership(miniChefController.address, true, false); + await rewarder.transferOwnership(miniChefController.address, true, false); + + // Add strategist on the controller + await miniChefController.connect(governance).addStrategist(strategist.address); + + return {alice, governance, strategist, pickle, reward, minichef, rewarder, miniChefController, jar1, jar2}; + }; + + describe("Controller behaviour", () => { + it("Should add new strategists", async () => { + const {alice, governance, miniChefController} = await loadFixture(setupFixture); + + await miniChefController.connect(governance).addStrategist(alice.address); + expect(await miniChefController.isStrategist(alice.address)).to.be.eq(true, "Strategist was not added"); + }); + + it("Should remove strategists", async () => { + const {strategist, governance, miniChefController} = await loadFixture(setupFixture); + + await miniChefController.connect(governance).removeStrategist(strategist.address); + expect(await miniChefController.isStrategist(strategist.address)).to.be.eq(false, "Strategist was not removed"); + }); + + it("Should transfer governance correctly", async () => { + const {alice, governance, miniChefController} = await loadFixture(setupFixture); + + await miniChefController.connect(governance).setPendingGovernance(alice.address); + await miniChefController.connect(alice).claimGovernance(); + expect(await miniChefController.governance()).to.be.eq(alice.address, "Governance was not transferred"); + }); + + it("Should change chef/rewarder addresses correctly", async () => { + const {alice, governance, miniChefController} = await loadFixture(setupFixture); + + await miniChefController.connect(governance).setMinichef(alice.address); + expect(await miniChefController.MINICHEF()).to.be.eq(alice.address, "Minichef address was not changed"); + + await miniChefController.connect(governance).setRewarder(alice.address); + expect(await miniChefController.REWARDER()).to.be.eq(alice.address, "Rewarder address was not changed"); + }); + + it("Only governance can change chef owner", async () => { + const {alice, governance, miniChefController, minichef, rewarder} = await loadFixture(setupFixture); + + await miniChefController + .connect(alice) + .transferMinichefOwnership(alice.address) + .catch(() => {}); + expect(await minichef.owner()).to.be.eq(miniChefController.address, "Non-governance changed minichef owner"); + + await miniChefController + .connect(alice) + .transferRewarderOwnership(alice.address) + .catch(() => {}); + expect(await rewarder.owner()).to.be.eq(miniChefController.address, "Non-governance changed rewarder owner"); + + await miniChefController.connect(governance).transferMinichefOwnership(alice.address); + await minichef.connect(alice).claimOwnership(); + expect(await minichef.owner()).to.be.eq(alice.address, "Governance failed to change minichef owner"); + + await miniChefController.connect(governance).transferRewarderOwnership(alice.address); + await rewarder.connect(alice).claimOwnership(); + expect(await rewarder.owner()).to.be.eq(alice.address, "Governance failed to change rewarder owner"); + }); + + it("Should add multiple pools properly", async () => { + const {strategist, miniChefController, minichef, rewarder, jar1, jar2} = await loadFixture(setupFixture); + + await miniChefController.connect(strategist).add([jar1.address, jar2.address], [10, 20]); + expect(await minichef.lpToken(1)).to.be.eq(jar2.address, "LPs were not added properly on the minichef"); + expect((await minichef.poolInfo(1)).allocPoint).to.be.eq(20, "AllocPoints were not set properly on the minichef"); + expect(await rewarder.poolIds(1)).to.be.eq(1, "LPs were not added properly on the rewarder"); + expect((await rewarder.poolInfo(1)).allocPoint).to.be.eq(20, "AllocPoints were not set properly on the rewarder"); + }); + + it("Should set emission rates correctly", async () => { + const {strategist, miniChefController, minichef, rewarder} = await loadFixture(setupFixture); + + await miniChefController.connect(strategist).setPicklePerSecond(10001); + expect(await minichef.picklePerSecond()).to.be.eq( + BigNumber.from(10001), + "Emission rate was not set properly on the minichef" + ); + await miniChefController.connect(strategist).setRewardPerSecond(10002); + expect(await rewarder.rewardPerSecond()).to.be.eq( + BigNumber.from(10002), + "Emission rate was not set properly on the rewarder" + ); + }); + + it("Should execute emergency properly", async () => { + const {governance, miniChefController, minichef, rewarder, alice} = await loadFixture(setupFixture); + + // Renounce minichef ownership + const signature = "transferOwnership(address,bool,bool)"; + const data = ethers.utils.defaultAbiCoder.encode( + ["address", "bool", "bool"], + [ethers.constants.AddressZero, true, true] + ); + await miniChefController.connect(governance).execute(minichef.address, signature, data); + expect(await minichef.owner()).to.be.eq(ethers.constants.AddressZero, "Emergency execute failed"); + }); + }); +});