From 56488064cd2f9592aeca7794008da8c4a85af63e Mon Sep 17 00:00:00 2001 From: Abiencode Date: Thu, 18 Mar 2021 20:03:18 -0700 Subject: [PATCH 01/19] Write sushi-eth-alcx strategy Adding unit test --- .gitignore | 7 +- package.json | 7 +- src/interfaces/alcx-farm.sol | 28 ++++ .../alchemix/strategy-sushi-eth-alcx-lp.sol | 88 +++++++++++++ src/strategies/strategy-alcx-farm-base.sol | 74 +++++++++++ .../lib/test-strategy-alcx-farm-base.sol | 121 ++++++++++++++++++ 6 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 src/interfaces/alcx-farm.sol create mode 100644 src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol create mode 100644 src/strategies/strategy-alcx-farm-base.sol create mode 100644 src/tests/lib/test-strategy-alcx-farm-base.sol diff --git a/.gitignore b/.gitignore index 53f0b44f5..c409919a1 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,9 @@ dist solc-input.json build -artifacts \ No newline at end of file +cache +artifacts +hardhat.config.js +yarn.lock +hardhat +package-lock.json \ No newline at end of file diff --git a/package.json b/package.json index 5f5d31030..7f22f4899 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,12 @@ "flatten": "npx sol-merger \"./src/**/*.sol\" ./build" }, "devDependencies": { - "eslint": "^7.10.0" + "@nomiclabs/hardhat-ethers": "^2.0.2", + "@nomiclabs/hardhat-etherscan": "^2.1.1", + "@nomiclabs/hardhat-truffle5": "^2.0.0", + "@nomiclabs/hardhat-web3": "^2.0.0", + "eslint": "^7.10.0", + "hardhat": "^2.1.1" }, "eslintConfig": { "extends": "eslint:recommended", diff --git a/src/interfaces/alcx-farm.sol b/src/interfaces/alcx-farm.sol new file mode 100644 index 000000000..ec3094a6f --- /dev/null +++ b/src/interfaces/alcx-farm.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +// interface for Alchemix farm contract +import "../lib/erc20.sol"; + +interface IStakingPools { + function createPool( + IERC20 _token + ) external returns (uint256); + function setRewardWeights(uint256[] calldata _rewardWeights) external; + function acceptGovernance() external; + function setPendingGovernance(address _pendingGovernance) external; + function setRewardRate(uint256 _rewardRate) external; + function deposit(uint256 _poolId, uint256 _depositAmount) external; + function withdraw(uint256 _poolId, uint256 _withdrawAmount) external; + function claim(uint256 _poolId) external; + function exit(uint256 _poolId) external; + function rewardRate() external view returns (uint256); + function totalRewardWeight() external view returns (uint256); + function poolCount() external view returns (uint256); + function getPoolToken(uint256 _poolId) external view returns (IERC20); + function getPoolTotalDeposited(uint256 _poolId) external view returns (uint256); + function getPoolRewardWeight(uint256 _poolId) external view returns (uint256); + function getPoolRewardRate(uint256 _poolId) external view returns (uint256); + function getStakeTotalDeposited(address _account, uint256 _poolId) external view returns (uint256); + function getStakeTotalUnclaimed(address _account, uint256 _poolId) external view returns (uint256); +} diff --git a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol new file mode 100644 index 000000000..75a781134 --- /dev/null +++ b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +import "../strategy-alcx-farm-base.sol"; + +contract StrategySushiEthAlcxLp is StrategyAlchemixFarmBase { + + uint256 public sushi_alcx_poolId = 2; + + address public sushi_eth_alcx_lp = 0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8; + + constructor( + address _governance, + address _strategist, + address _controller, + address _timelock + ) + public + StrategyAlchemixFarmBase( + sushi_alcx_poolId, + sushi_eth_alcx_lp, + _governance, + _strategist, + _controller, + _timelock + ) + {} + + // **** Views **** + + function getName() external override pure returns (string memory) { + return "StrategySushiEthAlcxLp"; + } + + // **** State Mutations **** + + function harvest() public override onlyBenevolent { + // Collects Alcx tokens + IStakingPools(stakingPool).claim(poolId); + uint256 _alcx = IERC20(alcx).balanceOf(address(this)); + if (_alcx > 0) { + // 10% is locked up for future gov + uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); + IERC20(alcx).safeTransfer( + IController(controller).treasury(), + _keepAlcx + ); + uint256 _amount = _alcx.sub(_keepAlcx); + _swapSushiswap(alcx, weth, _amount.div(2)); + } + + // Adds in liquidity for WETH/ALCX + uint256 _weth = IERC20(weth).balanceOf(address(this)); + + _alcx = IERC20(alcx).balanceOf(address(this)); + + if (_weth > 0 && _alcx > 0) { + IERC20(weth).safeApprove(sushiRouter, 0); + IERC20(weth).safeApprove(sushiRouter, _weth); + + IERC20(alcx).safeApprove(sushiRouter, 0); + IERC20(alcx).safeApprove(sushiRouter, _alcx); + + UniswapRouterV2(sushiRouter).addLiquidity( + weth, + alcx, + _weth, + _alcx, + 0, + 0, + address(this), + now + 60 + ); + + // Donates DUST + IERC20(weth).transfer( + IController(controller).treasury(), + IERC20(weth).balanceOf(address(this)) + ); + IERC20(alcx).safeTransfer( + IController(controller).treasury(), + IERC20(alcx).balanceOf(address(this)) + ); + } + + _distributePerformanceFeesAndDeposit(); + } +} diff --git a/src/strategies/strategy-alcx-farm-base.sol b/src/strategies/strategy-alcx-farm-base.sol new file mode 100644 index 000000000..7662a3aef --- /dev/null +++ b/src/strategies/strategy-alcx-farm-base.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +import "./strategy-base.sol"; +import "../interfaces/alcx-farm.sol"; + +abstract contract StrategyAlchemixFarmBase is StrategyBase { + // Token addresses + address public constant alcx = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; + address public constant stakingPool = 0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa; + + // How much Alcx tokens to keep? + uint256 public keepAlcx = 0; + uint256 public constant keepAlcxMax = 10000; + + uint256 public poolId; + + constructor( + uint256 _poolId, + address _token, + address _governance, + address _strategist, + address _controller, + address _timelock + ) + public + StrategyBase( + _token, + _governance, + _strategist, + _controller, + _timelock + ) + { + poolId = _poolId; + } + + function balanceOfPool() public override view returns (uint256) { + uint256 amount = IStakingPools(stakingPool).getStakeTotalDeposited(address(this), poolId); + return amount; + } + + function getHarvestable() external view returns (uint256) { + return IStakingPools(stakingPool).getStakeTotalUnclaimed(address(this), poolId); + } + + // **** Setters **** + + function deposit() public override { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(stakingPool, 0); + IERC20(want).safeApprove(stakingPool, _want); + IStakingPools(stakingPool).deposit(poolId, _want); + } + } + + function _withdrawSome(uint256 _amount) + internal + override + returns (uint256) + { + IStakingPools(stakingPool).withdraw(poolId, _amount); + return _amount; + } + + // **** Setters **** + + function setKeepAlcx(uint256 _keepAlcx) external { + require(msg.sender == timelock, "!timelock"); + keepAlcx = _keepAlcx; + } + // can't have harvest function here +} diff --git a/src/tests/lib/test-strategy-alcx-farm-base.sol b/src/tests/lib/test-strategy-alcx-farm-base.sol new file mode 100644 index 000000000..5b4406b3c --- /dev/null +++ b/src/tests/lib/test-strategy-alcx-farm-base.sol @@ -0,0 +1,121 @@ +pragma solidity ^0.6.7; + +import "../lib/hevm.sol"; +import "../lib/user.sol"; +import "../lib/test-approx.sol"; +import "../lib/test-defi-base.sol"; + +import "../../interfaces/strategy.sol"; +import "../../interfaces/curve.sol"; +import "../../interfaces/uniswapv2.sol"; + +import "../../pickle-jar.sol"; +import "../../controller-v4.sol"; + +contract StrategyAlcxFarmTestBase is DSTestDefiBase { + address want; + address token1; + + address governance; + address strategist; + address timelock; + + address devfund; + address treasury; + + PickleJar pickleJar; + ControllerV4 controller; + IStrategy strategy; + + function _getWant(uint256 ethAmount, uint256 amount) internal { + _getERC20(token1, amount); + + uint256 _token1 = IERC20(token1).balanceOf(address(this)); + + IERC20(token1).safeApprove(address(sushiRouter), 0); + IERC20(token1).safeApprove(address(sushiRouter), _token1); + + sushiRouter.addLiquidityETH{value: ethAmount}( + token1, + _token1, + 0, + 0, + address(this), + now + 60 + ); + } + + // **** Tests **** + + function _test_timelock() internal { + assertTrue(strategy.timelock() == timelock); + strategy.setTimelock(address(1)); + assertTrue(strategy.timelock() == address(1)); + } + + function _test_withdraw_release() internal { + uint256 decimals = ERC20(token1).decimals(); + _getWant(10 ether, 400 * (10**decimals)); + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(address(pickleJar), 0); + IERC20(want).safeApprove(address(pickleJar), _want); + pickleJar.deposit(_want); + pickleJar.earn(); + hevm.roll(block.number + 1000); + strategy.harvest(); + + // Checking withdraw + uint256 _before = IERC20(want).balanceOf(address(pickleJar)); + controller.withdrawAll(want); + uint256 _after = IERC20(want).balanceOf(address(pickleJar)); + assertTrue(_after > _before); + _before = IERC20(want).balanceOf(address(this)); + pickleJar.withdrawAll(); + _after = IERC20(want).balanceOf(address(this)); + assertTrue(_after > _before); + + // Check if we gained interest + assertTrue(_after > _want); + } + + function _test_get_earn_harvest_rewards() internal { + uint256 decimals = ERC20(token1).decimals(); + _getWant(10 ether, 400 * (10**decimals)); + uint256 _want = IERC20(want).balanceOf(address(this)); + IERC20(want).safeApprove(address(pickleJar), 0); + IERC20(want).safeApprove(address(pickleJar), _want); + pickleJar.deposit(_want); + pickleJar.earn(); + hevm.roll(block.number + 1000); + + // Call the harvest function + uint256 _before = pickleJar.balance(); + uint256 _treasuryBefore = IERC20(want).balanceOf(treasury); + strategy.harvest(); + uint256 _after = pickleJar.balance(); + uint256 _treasuryAfter = IERC20(want).balanceOf(treasury); + + uint256 earned = _after.sub(_before).mul(1000).div(800); + uint256 earnedRewards = earned.mul(200).div(1000); // 20% + uint256 actualRewardsEarned = _treasuryAfter.sub(_treasuryBefore); + + // 20% performance fee is given + assertEqApprox(earnedRewards, actualRewardsEarned); + + // Withdraw + uint256 _devBefore = IERC20(want).balanceOf(devfund); + _treasuryBefore = IERC20(want).balanceOf(treasury); + uint256 _stratBal = strategy.balanceOf(); + pickleJar.withdrawAll(); + uint256 _devAfter = IERC20(want).balanceOf(devfund); + _treasuryAfter = IERC20(want).balanceOf(treasury); + + // 0% goes to dev + uint256 _devFund = _devAfter.sub(_devBefore); + assertEq(_devFund, 0); + + // 0% goes to treasury + uint256 _treasuryFund = _treasuryAfter.sub(_treasuryBefore); + assertEq(_treasuryFund, 0); + } +} From bba8fafafaff9195c3c4b5e975d508a3b5435d1b Mon Sep 17 00:00:00 2001 From: Abiencode Date: Fri, 19 Mar 2021 07:01:51 -0700 Subject: [PATCH 02/19] Finish Sushi ETH/ALCX strategy and unit test --- src/strategies/strategy-alcx-farm-base.sol | 1 + .../strategy-sushi-eth-alcx-lp.test.sol} | 76 ++++++++++++++----- 2 files changed, 59 insertions(+), 18 deletions(-) rename src/tests/{lib/test-strategy-alcx-farm-base.sol => strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol} (64%) diff --git a/src/strategies/strategy-alcx-farm-base.sol b/src/strategies/strategy-alcx-farm-base.sol index 7662a3aef..08975d162 100644 --- a/src/strategies/strategy-alcx-farm-base.sol +++ b/src/strategies/strategy-alcx-farm-base.sol @@ -33,6 +33,7 @@ abstract contract StrategyAlchemixFarmBase is StrategyBase { ) { poolId = _poolId; + IERC20(alcx).approve(sushiRouter, uint(-1)); } function balanceOfPool() public override view returns (uint256) { diff --git a/src/tests/lib/test-strategy-alcx-farm-base.sol b/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol similarity index 64% rename from src/tests/lib/test-strategy-alcx-farm-base.sol rename to src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol index 5b4406b3c..4531e6d31 100644 --- a/src/tests/lib/test-strategy-alcx-farm-base.sol +++ b/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol @@ -1,18 +1,15 @@ pragma solidity ^0.6.7; -import "../lib/hevm.sol"; -import "../lib/user.sol"; -import "../lib/test-approx.sol"; -import "../lib/test-defi-base.sol"; +import "../../../interfaces/strategy.sol"; +import "../../../interfaces/curve.sol"; +import "../../../interfaces/uniswapv2.sol"; -import "../../interfaces/strategy.sol"; -import "../../interfaces/curve.sol"; -import "../../interfaces/uniswapv2.sol"; +import "../../../pickle-jar.sol"; +import "../../../controller-v4.sol"; +import "../../lib/test-sushi-base.sol"; +import "../../../strategies/alchemix/strategy-sushi-eth-alcx-lp.sol"; -import "../../pickle-jar.sol"; -import "../../controller-v4.sol"; - -contract StrategyAlcxFarmTestBase is DSTestDefiBase { +contract StrategySushiEthAlcxLpTest is DSTestSushiBase { address want; address token1; @@ -26,6 +23,53 @@ contract StrategyAlcxFarmTestBase is DSTestDefiBase { PickleJar pickleJar; ControllerV4 controller; IStrategy strategy; + + function setUp() public { + want = 0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8; + token1 = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; + + governance = address(this); + strategist = address(this); + devfund = address(new User()); + treasury = address(new User()); + timelock = address(this); + + controller = new ControllerV4( + governance, + strategist, + timelock, + devfund, + treasury + ); + + strategy = IStrategy( + address( + new StrategySushiEthAlcxLp( + governance, + strategist, + address(controller), + timelock + ) + ) + ); + + pickleJar = new PickleJar( + strategy.want(), + governance, + timelock, + address(controller) + ); + + controller.setJar(strategy.want(), address(pickleJar)); + controller.approveStrategy(strategy.want(), address(strategy)); + controller.setStrategy(strategy.want(), address(strategy)); + + // Set time + hevm.warp(startTime); + + uint256 decimals = ERC20(token1).decimals(); + _getWant(100 ether, 100 * (10**decimals)); + } function _getWant(uint256 ethAmount, uint256 amount) internal { _getERC20(token1, amount); @@ -47,15 +91,13 @@ contract StrategyAlcxFarmTestBase is DSTestDefiBase { // **** Tests **** - function _test_timelock() internal { + function test_ethalcxv1_timelock() public { assertTrue(strategy.timelock() == timelock); strategy.setTimelock(address(1)); assertTrue(strategy.timelock() == address(1)); } - function _test_withdraw_release() internal { - uint256 decimals = ERC20(token1).decimals(); - _getWant(10 ether, 400 * (10**decimals)); + function test_ethalcxv1_withdraw_release() public { uint256 _want = IERC20(want).balanceOf(address(this)); IERC20(want).safeApprove(address(pickleJar), 0); IERC20(want).safeApprove(address(pickleJar), _want); @@ -78,9 +120,7 @@ contract StrategyAlcxFarmTestBase is DSTestDefiBase { assertTrue(_after > _want); } - function _test_get_earn_harvest_rewards() internal { - uint256 decimals = ERC20(token1).decimals(); - _getWant(10 ether, 400 * (10**decimals)); + function test_ethalcxv1_get_earn_harvest_rewards() public { uint256 _want = IERC20(want).balanceOf(address(this)); IERC20(want).safeApprove(address(pickleJar), 0); IERC20(want).safeApprove(address(pickleJar), _want); From c4c8e3d46d01482485e6ebbc479512902831b95c Mon Sep 17 00:00:00 2001 From: Abiencode Date: Mon, 22 Mar 2021 07:51:49 -0700 Subject: [PATCH 03/19] Write hardhat unit test for alusd-3crv pool, finish alcx farm --- package.json | 4 + src/interfaces/controller.sol | 2 + src/interfaces/jar.sol | 4 + src/interfaces/strategy.sol | 4 +- src/pickle-jar-alusd-3crv.sol | 130 +++++++ .../alchemix/strategy-alusd-3crv.sol | 127 +++++++ .../alchemix/strategy-sushi-eth-alcx-lp.sol | 15 +- src/strategies/strategy-alcx-farm-base.sol | 14 +- .../strategy-sushi-eth-alcx-lp.test.sol | 5 +- test/StrategyAlusd3crv.test.js | 194 ++++++++++ test/abi/ERC20.json | 356 ++++++++++++++++++ 11 files changed, 839 insertions(+), 16 deletions(-) create mode 100644 src/pickle-jar-alusd-3crv.sol create mode 100644 src/strategies/alchemix/strategy-alusd-3crv.sol create mode 100644 test/StrategyAlusd3crv.test.js create mode 100644 test/abi/ERC20.json diff --git a/package.json b/package.json index 7f22f4899..b05c4ceba 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,10 @@ "@nomiclabs/hardhat-etherscan": "^2.1.1", "@nomiclabs/hardhat-truffle5": "^2.0.0", "@nomiclabs/hardhat-web3": "^2.0.0", + "@openzeppelin/test-helpers": "^0.5.10", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", + "dotenv": "^8.2.0", "eslint": "^7.10.0", "hardhat": "^2.1.1" }, diff --git a/src/interfaces/controller.sol b/src/interfaces/controller.sol index 6bbf8ddec..446b16260 100644 --- a/src/interfaces/controller.sol +++ b/src/interfaces/controller.sol @@ -16,4 +16,6 @@ interface IController { function withdraw(address, uint256) external; function earn(address, uint256) external; + + function strategies(address) external view returns (address); } diff --git a/src/interfaces/jar.sol b/src/interfaces/jar.sol index df0bd77df..40e301c60 100644 --- a/src/interfaces/jar.sol +++ b/src/interfaces/jar.sol @@ -5,12 +5,16 @@ import "../lib/erc20.sol"; interface IJar is IERC20 { function token() external view returns (address); + + function reward() external view returns (address); function claimInsurance() external; // NOTE: Only yDelegatedVault implements this function getRatio() external view returns (uint256); function depositAll() external; + + function balance() external view returns (uint256); function deposit(uint256) external; diff --git a/src/interfaces/strategy.sol b/src/interfaces/strategy.sol index 9426a4cde..22d650bb9 100644 --- a/src/interfaces/strategy.sol +++ b/src/interfaces/strategy.sol @@ -15,7 +15,9 @@ interface IStrategy { function withdrawForSwap(uint256) external returns (uint256); function withdraw(address) external; - + + function getRedeemableReward() external view returns (uint256); + function withdraw(uint256) external; function skim() external; diff --git a/src/pickle-jar-alusd-3crv.sol b/src/pickle-jar-alusd-3crv.sol new file mode 100644 index 000000000..94676ea75 --- /dev/null +++ b/src/pickle-jar-alusd-3crv.sol @@ -0,0 +1,130 @@ +// https://github.com/iearn-finance/vaults/blob/master/contracts/vaults/yVault.sol + +pragma solidity ^0.6.7; + +import "./interfaces/controller.sol"; + +import "./lib/erc20.sol"; +import "./interfaces/strategy.sol"; +import "./lib/safe-math.sol"; +contract PickleJarAlusd3Crv is ERC20 { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + IERC20 public token; + IERC20 public reward; + + uint256 public min = 9500; + uint256 public constant max = 10000; + + address public governance; + address public timelock; + address public controller; + + constructor(address _token, address _reward, address _governance, address _timelock, address _controller) + public + ERC20( + string(abi.encodePacked("pickling ", ERC20(_token).name())), + string(abi.encodePacked("p", ERC20(_token).symbol())) + ) + { + _setupDecimals(ERC20(_token).decimals()); + token = IERC20(_token); + reward = IERC20(_reward); + governance = _governance; + timelock = _timelock; + controller = _controller; + } + + function balance() public view returns (uint256) { + return + token.balanceOf(address(this)).add( + IController(controller).balanceOf(address(token)) + ); + } + + function setMin(uint256 _min) external { + require(msg.sender == governance, "!governance"); + require(_min <= max, "numerator cannot be greater than denominator"); + min = _min; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setTimelock(address _timelock) public { + require(msg.sender == timelock, "!timelock"); + timelock = _timelock; + } + + function setController(address _controller) public { + require(msg.sender == timelock, "!timelock"); + controller = _controller; + } + + // Custom logic in here for how much the jars allows to be borrowed + // Sets minimum required on-hand to keep small withdrawals cheap + function available() public view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + function earn() public { + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + function depositAll() external { + deposit(token.balanceOf(msg.sender)); + } + + function deposit(uint256 _amount) public { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + earn(); //to prevent the exploit + } + + function withdrawAll() external { + withdraw(balanceOf(msg.sender)); + } + + // Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + function harvest(address reserve, uint256 amount) external { + require(msg.sender == controller, "!controller"); + require(reserve != address(token), "token"); + IERC20(reserve).safeTransfer(controller, amount); + } + + // No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public { + _burn(msg.sender, _shares); + + uint256 _redeemable_reward = IStrategy(IController(controller).strategies(address(token))).getRedeemableReward(); + uint256 _rewardAmount = (_redeemable_reward.mul(_shares)).div(totalSupply().add(_shares)); //add missing shares + + IController(controller).withdraw(address(token), _shares); + uint256 _reward_balance = reward.balanceOf(address(this)); + + if (_reward_balance < _rewardAmount) _rewardAmount = _reward_balance; + + token.safeTransfer(msg.sender, _shares); + reward.safeTransfer(msg.sender, _rewardAmount); + } + + function getRatio() public view returns (uint256) { + return balance().mul(1e18).div(totalSupply()); + } +} diff --git a/src/strategies/alchemix/strategy-alusd-3crv.sol b/src/strategies/alchemix/strategy-alusd-3crv.sol new file mode 100644 index 000000000..c68eb723f --- /dev/null +++ b/src/strategies/alchemix/strategy-alusd-3crv.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +import "../strategy-alcx-farm-base.sol"; + +contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { + + uint256 public alusd_3crv_poolId = 4; + + address public alusd_3crv = 0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c; + + constructor( + address _governance, + address _strategist, + address _controller, + address _timelock + ) + public + StrategyAlchemixFarmBase( + alusd_3crv_poolId, + alusd_3crv, + _governance, + _strategist, + _controller, + _timelock + ) + { + IERC20(alcx).approve(stakingPool, uint(-1)); + } + + // **** Views **** + + function getName() external override pure returns (string memory) { + return "StrategyCurveAlusd3Crv"; + } + + function getAlcxFarmHarvestable() public view returns (uint256) { + return IStakingPools(stakingPool).getStakeTotalUnclaimed(address(this), alcxPoolId); + } + + // **** State Mutations **** + + function harvest() public override onlyBenevolent { + // Collects Alcx tokens + uint256 _alcxHarvestable = getAlcxFarmHarvestable(); + if (_alcxHarvestable > 0) IStakingPools(stakingPool).claim(alcxPoolId); //claim from alcx staking pool + + uint256 _harvestable = getHarvestable(); + if (_harvestable > 0) IStakingPools(stakingPool).claim(poolId); //claim from alusd_3crv staking pool + + uint256 _alcx = IERC20(alcx).balanceOf(address(this)); + if (_alcx > 0) { + // 10% is locked up for future gov + uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); + IERC20(alcx).safeTransfer( + IController(controller).treasury(), + _keepAlcx + ); + uint256 _amount = _alcx.sub(_keepAlcx); + IStakingPools(stakingPool).deposit(alcxPoolId, _amount); //stake to alcx farm + } + } + + function _withdrawToJar(uint256 _amount) internal returns(uint256) { + address _jar = getJarAddress(); + address reward_token = IJar(_jar).reward(); + require (reward_token != address(0), "Reward token is not set in the pickle jar"); + + uint256 _rewardAmount = (getRedeemableReward().mul(_amount)) + .div(IJar(_jar).totalSupply().add(_amount)); //add missing shares + _withdrawSomeReward(_rewardAmount); + uint256 _reward_balance = IERC20(reward_token).balanceOf(address(this)); + + if (_reward_balance < _rewardAmount) _rewardAmount = _reward_balance; + + uint256 _feeDev = _rewardAmount.mul(withdrawalDevFundFee).div( + withdrawalDevFundMax + ); + IERC20(reward_token).safeTransfer(IController(controller).devfund(), _feeDev); + + uint256 _feeTreasury = _rewardAmount.mul(withdrawalTreasuryFee).div( + withdrawalTreasuryMax + ); + + IERC20(reward_token).safeTransfer( + IController(controller).treasury(), + _feeTreasury + ); + + uint256 _send_amount = _rewardAmount.sub(_feeDev).sub(_feeTreasury); + + IERC20(reward_token).safeTransfer(_jar, _send_amount); + return _send_amount; + } + function _withdrawSome(uint256 _amount) + internal + override + returns (uint256) + { + _withdrawToJar(_amount); + address _jar = getJarAddress(); + uint256 r = (IJar(_jar).balance().mul(_amount)).div(IJar(_jar).totalSupply().add(_amount)); //add missing amount because it is already burned + uint256 _want_amount = r.sub(IERC20(want).balanceOf(_jar)); //sub existing want balance + IStakingPools(stakingPool).withdraw(poolId, _want_amount); + + return _want_amount; + } + + function getJarAddress() public view returns (address) { + address _jar = IController(controller).jars(address(want)); + require(_jar != address(0), "!jar"); // additional protection so we don't burn the funds + + return _jar; + } + + function _withdrawSomeReward(uint256 _amount) + internal + returns (uint256) + { + IStakingPools(stakingPool).withdraw(alcxPoolId, _amount); + return _amount; + } + + function getRedeemableReward() public view returns (uint256) { + return IStakingPools(stakingPool).getStakeTotalDeposited(address(this), alcxPoolId); + } +} diff --git a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol index 75a781134..b05352f7b 100644 --- a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol +++ b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol @@ -24,7 +24,9 @@ contract StrategySushiEthAlcxLp is StrategyAlchemixFarmBase { _controller, _timelock ) - {} + { + IERC20(alcx).approve(sushiRouter, uint(-1)); + } // **** Views **** @@ -82,7 +84,16 @@ contract StrategySushiEthAlcxLp is StrategyAlchemixFarmBase { IERC20(alcx).balanceOf(address(this)) ); } - + _distributePerformanceFeesAndDeposit(); } + + function _withdrawSome(uint256 _amount) + internal + override + returns (uint256) + { + IStakingPools(stakingPool).withdraw(poolId, _amount); + return _amount; + } } diff --git a/src/strategies/strategy-alcx-farm-base.sol b/src/strategies/strategy-alcx-farm-base.sol index 08975d162..213847754 100644 --- a/src/strategies/strategy-alcx-farm-base.sol +++ b/src/strategies/strategy-alcx-farm-base.sol @@ -12,6 +12,8 @@ abstract contract StrategyAlchemixFarmBase is StrategyBase { // How much Alcx tokens to keep? uint256 public keepAlcx = 0; uint256 public constant keepAlcxMax = 10000; + + uint256 public alcxPoolId = 1; uint256 public poolId; @@ -33,7 +35,6 @@ abstract contract StrategyAlchemixFarmBase is StrategyBase { ) { poolId = _poolId; - IERC20(alcx).approve(sushiRouter, uint(-1)); } function balanceOfPool() public override view returns (uint256) { @@ -41,7 +42,7 @@ abstract contract StrategyAlchemixFarmBase is StrategyBase { return amount; } - function getHarvestable() external view returns (uint256) { + function getHarvestable() public view returns (uint256) { return IStakingPools(stakingPool).getStakeTotalUnclaimed(address(this), poolId); } @@ -56,15 +57,6 @@ abstract contract StrategyAlchemixFarmBase is StrategyBase { } } - function _withdrawSome(uint256 _amount) - internal - override - returns (uint256) - { - IStakingPools(stakingPool).withdraw(poolId, _amount); - return _amount; - } - // **** Setters **** function setKeepAlcx(uint256 _keepAlcx) external { diff --git a/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol b/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol index 4531e6d31..21797f80d 100644 --- a/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol +++ b/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol @@ -6,6 +6,7 @@ import "../../../interfaces/uniswapv2.sol"; import "../../../pickle-jar.sol"; import "../../../controller-v4.sol"; +import "../../../lib/erc20.sol"; import "../../lib/test-sushi-base.sol"; import "../../../strategies/alchemix/strategy-sushi-eth-alcx-lp.sol"; @@ -23,7 +24,7 @@ contract StrategySushiEthAlcxLpTest is DSTestSushiBase { PickleJar pickleJar; ControllerV4 controller; IStrategy strategy; - + function setUp() public { want = 0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8; token1 = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; @@ -68,7 +69,7 @@ contract StrategySushiEthAlcxLpTest is DSTestSushiBase { hevm.warp(startTime); uint256 decimals = ERC20(token1).decimals(); - _getWant(100 ether, 100 * (10**decimals)); + _getWant(100 ether, 50 * (10**decimals)); } function _getWant(uint256 ethAmount, uint256 amount) internal { diff --git a/test/StrategyAlusd3crv.test.js b/test/StrategyAlusd3crv.test.js new file mode 100644 index 000000000..388eb583e --- /dev/null +++ b/test/StrategyAlusd3crv.test.js @@ -0,0 +1,194 @@ +const hre = require("hardhat"); +var chaiAsPromised = require("chai-as-promised"); +const StrategyCurveAlusd3Crv = hre.artifacts.require("StrategyCurveAlusd3Crv"); +const PickleJarAlusd3Crv = hre.artifacts.require("PickleJarAlusd3Crv"); +const ControllerV4 = hre.artifacts.require("ControllerV4"); + +const { assert } = require("chai").use(chaiAsPromised); +const { time } = require("@openzeppelin/test-helpers"); + +const ERC20_ABI = require("./abi/ERC20.json"); + +const unlockAccount = async (address) => { + await hre.network.provider.send("hardhat_impersonateAccount", [address]); + return hre.ethers.provider.getSigner(address); +}; + +const toWei = (ethAmount) => { + return hre.ethers.constants.WeiPerEther.mul(hre.ethers.BigNumber.from(ethAmount)); +}; + +describe("StrategyCurveAlusd3Crv Unit test", () => { + let strategy, pickleJar, controller; + const want = "0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c"; + let alusd_3crv, alusd_3crv_whale; + let deployer, alice, bob; + let alcx, + alcx_addr = "0xdbdb4d16eda451d0503b854cf79d55697f90c8df"; + let governance, strategist, devfund, treasury, timelock; + + before("Deploy contracts", async () => { + [governance, devfund, treasury] = await web3.eth.getAccounts(); + const signers = await hre.ethers.getSigners(); + deployer = signers[0]; + alice = signers[3]; + bob = signers[4]; + + strategist = governance; + timelock = governance; + + controller = await ControllerV4.new(governance, strategist, timelock, devfund, treasury); + console.log("controller is deployed at =====> ", controller.address); + + strategy = await StrategyCurveAlusd3Crv.new(governance, strategist, controller.address, timelock); + console.log("Strategy is deployed at =====> ", strategy.address); + + pickleJar = await PickleJarAlusd3Crv.new(want, alcx_addr, governance, timelock, controller.address); + console.log("pickleJar is deployed at =====> ", pickleJar.address); + + await controller.setJar(want, pickleJar.address, { from: governance }); + await controller.approveStrategy(want, strategy.address, { from: governance }); + await controller.setStrategy(want, strategy.address, { from: governance }); + + await strategy.setKeepAlcx("2000", { from: governance }); + + alusd_3crv_whale = await unlockAccount("0xBAF18722C137E725327F1376329d3c99F26f6A60"); + alusd_3crv = await hre.ethers.getContractAt(ERC20_ABI, want); + alcx = await hre.ethers.getContractAt(ERC20_ABI, alcx_addr); + + alusd_3crv.connect(alusd_3crv_whale).transfer(alice.address, toWei(5000)); + assert.equal((await alusd_3crv.balanceOf(alice.address)).toString(), toWei(5000).toString()); + alusd_3crv.connect(alusd_3crv_whale).transfer(bob.address, toWei(3000)); + assert.equal((await alusd_3crv.balanceOf(bob.address)).toString(), toWei(3000).toString()); + }); + + it("Should harvest the reward correctly", async () => { + console.log("\n---------------------------Alice deposit---------------------------------------\n"); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(5000)); + await pickleJar.deposit(toWei(5000), { from: alice.address }); + console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); + await pickleJar.earn({ from: alice.address }); + + await harvest(); + + console.log("\n---------------------------Bob deposit---------------------------------------\n"); + await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(3000)); + await pickleJar.deposit(toWei(3000), { from: bob.address }); + console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(bob.address)).toString()); + await pickleJar.earn({ from: bob.address }); + await harvest(); + + console.log("\n---------------------------Alice withdraw---------------------------------------\n"); + console.log("Redeemable Reward of Strategy =====> ", (await strategy.getRedeemableReward()).toString()); + + const _devBefore = await alusd_3crv.balanceOf(devfund); + let _treasuryBefore = await alcx.balanceOf(treasury); + + console.log("Dev fund balance before ===> ", _devBefore.toString()); + console.log("Treasury balance before ===> ", _treasuryBefore.toString()); + + let _alcx_before = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance before =====> ", _alcx_before.toString()); + + await pickleJar.withdrawAll({ from: alice.address }); + + let _alcx_after = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance after =====> ", _alcx_after.toString()); + + assert.equal(_alcx_after.gt(_alcx_before), true); + + const _devAfter = await alusd_3crv.balanceOf(devfund); + let _treasuryAfter = await alcx.balanceOf(treasury); + + console.log("Dev fund balance after ===> ", _devAfter.toString()); + console.log("Treasury balance after ===> ", _treasuryAfter.toString()); + + // 0% goes to dev when withdraw + assert.equal(_devAfter.eq(_devBefore), true); + + // 0% goes to treasury when withdraw + assert.equal(_treasuryAfter.eq(_treasuryBefore), true); + + console.log("\n---------------------------Bob withdraw---------------------------------------\n"); + + console.log("Redeemable Reward of Strategy =====> ", (await strategy.getRedeemableReward()).toString()); + _alcx_before = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance before =====> ", _alcx_before.toString()); + + await pickleJar.withdrawAll({ from: bob.address }); + + _alcx_after = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); + assert.equal(_alcx_after.gt(_alcx_before), true); + }); + + it("Should withdraw the want correctly", async () => { + console.log("\n---------------------------Alice deposit---------------------------------------\n"); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(5000)); + await pickleJar.deposit(toWei(5000), { from: alice.address }); + console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); + await pickleJar.earn({ from: alice.address }); + + await harvest(); + + console.log("\n---------------------------Bob deposit---------------------------------------\n"); + await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(3000)); + await pickleJar.deposit(toWei(3000), { from: bob.address }); + console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(bob.address)).toString()); + await pickleJar.earn({ from: bob.address }); + await harvest(); + + console.log("\n---------------------------Alice withdraw---------------------------------------\n"); + + let _jar_before = await alusd_3crv.balanceOf(pickleJar.address); + await controller.withdrawAll(alusd_3crv.address, { from: governance }); + let _jar_after = await alusd_3crv.balanceOf(pickleJar.address); + + let _alcx_before = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance before =====> ", _alcx_before.toString()); + + await pickleJar.withdrawAll({ from: alice.address }); + + let _alcx_after = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance after =====> ", _alcx_after.toString()); + + console.log("\n---------------------------Bob withdraw---------------------------------------\n"); + + _alcx_before = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance before =====> ", _alcx_before.toString()); + + _jar_before = await alusd_3crv.balanceOf(pickleJar.address); + + await controller.withdrawAll(alusd_3crv.address, { from: governance }); + + _jar_after = await alusd_3crv.balanceOf(pickleJar.address); + + await pickleJar.withdrawAll({ from: bob.address }); + + _alcx_after = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); + assert.equal(_alcx_after.gt(_alcx_before), true); + }); + + const harvest = async () => { + await time.increase(60 * 60 * 24 * 15); //15 days + const _balance = await strategy.balanceOfPool(); + console.log("Deposited amount of strategy ===> ", _balance.toString()); + + let _alcx = await strategy.getHarvestable(); + console.log("Alusd3Crv Farm harvestable of strategy of the first harvest ===> ", _alcx.toString()); + let _alcx2 = await strategy.getAlcxFarmHarvestable(); + console.log("Alcx Farm harvestable of strategy of the first harvest ===> ", _alcx2.toString()); + + await strategy.harvest({ from: governance }); + await time.increase(60 * 60 * 24 * 30); + + _alcx = await strategy.getHarvestable(); + console.log("Alusd3Crv Farm harvestable of strategy of the second harvest ===> ", _alcx.toString()); + + _alcx2 = await strategy.getAlcxFarmHarvestable(); + console.log("Alcx Farm harvestable of strategy of the second harvest ===> ", _alcx2.toString()); + + await strategy.harvest({ from: governance }); + }; +}); diff --git a/test/abi/ERC20.json b/test/abi/ERC20.json new file mode 100644 index 000000000..493c3a563 --- /dev/null +++ b/test/abi/ERC20.json @@ -0,0 +1,356 @@ +[ + { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "alchemistAddress", "type": "address" }, + { "indexed": false, "internalType": "bool", "name": "isPaused", "type": "bool" } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "previousAdminRole", "type": "bytes32" }, + { "indexed": true, "internalType": "bytes32", "name": "newAdminRole", "type": "bytes32" } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } + ], + "name": "RoleRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "ADMIN_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SENTINEL_ROLE", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "blacklist", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "account", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "ceiling", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], + "name": "getRoleAdmin", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "getRoleMember", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], + "name": "getRoleMemberCount", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "hasMinted", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "hasRole", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], + "name": "lowerHasMinted", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_recipient", "type": "address" }, + { "internalType": "uint256", "name": "_amount", "type": "uint256" } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_toPause", "type": "address" }, + { "internalType": "bool", "name": "_state", "type": "bool" } + ], + "name": "pauseAlchemist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "paused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "role", "type": "bytes32" }, + { "internalType": "address", "name": "account", "type": "address" } + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "_toBlacklist", "type": "address" }], + "name": "setBlacklist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_toSetCeiling", "type": "address" }, + { "internalType": "uint256", "name": "_ceiling", "type": "uint256" } + ], + "name": "setCeiling", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "_newSentinel", "type": "address" }], + "name": "setSentinel", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "_toWhitelist", "type": "address" }, + { "internalType": "bool", "name": "_state", "type": "bool" } + ], + "name": "setWhitelist", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "", "type": "address" }], + "name": "whiteList", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + } +] From d3accb362cbcad698a31d16fdc6c1f2aad0defe5 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Mon, 22 Mar 2021 09:08:30 -0700 Subject: [PATCH 04/19] Add hardhat config and update unit test path --- .gitignore | 1 - hardhat.config.js | 62 +++++++++++++++++++ .../alchemix}/StrategyAlusd3crv.test.js | 0 .../tests/strategies/alchemix}/abi/ERC20.json | 0 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 hardhat.config.js rename {test => src/tests/strategies/alchemix}/StrategyAlusd3crv.test.js (100%) rename {test => src/tests/strategies/alchemix}/abi/ERC20.json (100%) diff --git a/.gitignore b/.gitignore index c409919a1..7a4dd5f8d 100644 --- a/.gitignore +++ b/.gitignore @@ -108,7 +108,6 @@ solc-input.json build cache artifacts -hardhat.config.js yarn.lock hardhat package-lock.json \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js new file mode 100644 index 000000000..bec2d31d3 --- /dev/null +++ b/hardhat.config.js @@ -0,0 +1,62 @@ +require("@nomiclabs/hardhat-truffle5"); +require("@nomiclabs/hardhat-etherscan"); +require("@nomiclabs/hardhat-ethers"); +require("dotenv").config(); + +module.exports = { + defaultNetwork: "hardhat", + networks: { + hardhat: { + chainId: 1, + forking: { + url: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`, + }, + accounts: { + mnemonic: process.env.MNEMONIC, + }, + allowUnlimitedContractSize: true, + }, + mainnet: { + url: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`, + accounts: { + mnemonic: process.env.MNEMONIC, + }, + allowUnlimitedContractSize: true, + }, + ropsten: { + url: `https://ropsten.infura.io/v3/${process.env.INFURA_KEY}`, + accounts: { + mnemonic: process.env.MNEMONIC, + }, + allowUnlimitedContractSize: true, + }, + rinkeby: { + url: `https://rinkeby.infura.io/v3/${process.env.INFURA_KEY}`, + accounts: { + mnemonic: process.env.MNEMONIC, + }, + allowUnlimitedContractSize: true, + }, + }, + etherscan: { + apiKey: process.env.ETHERSCAN_APIKEY, + }, + solidity: { + version: "0.6.7", + settings: { + optimizer: { + enabled: true, + runs: 9999, + }, + }, + }, + mocha: { + timeout: 20000000, + }, + paths: { + sources: "./src", + tests: "./src/tests/strategies", + cache: "./cache", + artifacts: "./artifacts", + }, +}; diff --git a/test/StrategyAlusd3crv.test.js b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js similarity index 100% rename from test/StrategyAlusd3crv.test.js rename to src/tests/strategies/alchemix/StrategyAlusd3crv.test.js diff --git a/test/abi/ERC20.json b/src/tests/strategies/alchemix/abi/ERC20.json similarity index 100% rename from test/abi/ERC20.json rename to src/tests/strategies/alchemix/abi/ERC20.json From 3e15ee3b6c5685fa2f8843ca55f08b86f30a50bf Mon Sep 17 00:00:00 2001 From: Abiencode Date: Wed, 24 Mar 2021 19:37:57 -0700 Subject: [PATCH 05/19] - Update ALCX reward mechanism - Update controller - Update strategybasesymbiotic - Update picklejarsymbiotic --- .prettierrc | 14 + package.json | 3 +- src/controller-v4.sol | 5 + src/interfaces/controller.sol | 2 + src/interfaces/strategy.sol | 4 +- ...lusd-3crv.sol => pickle-jar-symbiotic.sol} | 91 +++-- .../alchemix/strategy-alusd-3crv.sol | 113 ++----- .../alchemix/strategy-sushi-eth-alcx-lp.sol | 9 - src/strategies/strategy-alcx-farm-base.sol | 31 +- src/strategies/strategy-base-symbiotic.sol | 314 ++++++++++++++++++ .../alchemix/StrategyAlusd3crv.test.js | 291 ++++++++-------- 11 files changed, 625 insertions(+), 252 deletions(-) create mode 100644 .prettierrc rename src/{pickle-jar-alusd-3crv.sol => pickle-jar-symbiotic.sol} (53%) create mode 100644 src/strategies/strategy-base-symbiotic.sol diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..daeba4bc3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "overrides": [ + { + "files": "*.sol", + "options": { + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "singleQuote": false, + "bracketSpacing": false, + "explicitTypes": "always" + } + }, +} diff --git a/package.json b/package.json index b05c4ceba..62d20f4e6 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "chai-as-promised": "^7.1.1", "dotenv": "^8.2.0", "eslint": "^7.10.0", - "hardhat": "^2.1.1" + "hardhat": "^2.1.1", + "prettier-plugin-solidity": "^1.0.0-beta.6" }, "eslintConfig": { "extends": "eslint:recommended", diff --git a/src/controller-v4.sol b/src/controller-v4.sol index 26eafe29c..431484476 100644 --- a/src/controller-v4.sol +++ b/src/controller-v4.sol @@ -247,6 +247,11 @@ contract ControllerV4 { IStrategy(strategies[_token]).withdraw(_amount); } + function withdrawReward(address _token, uint256 _reward) public { + require(msg.sender == jars[_token], "!jar"); + IStrategy(strategies[_token]).withdrawReward(_reward); + } + // Function to swap between jars function swapExactJarForJar( address _fromJar, // From which Jar diff --git a/src/interfaces/controller.sol b/src/interfaces/controller.sol index 446b16260..fd0a45e03 100644 --- a/src/interfaces/controller.sol +++ b/src/interfaces/controller.sol @@ -15,6 +15,8 @@ interface IController { function withdraw(address, uint256) external; + function withdrawReward(address, uint256) external; + function earn(address, uint256) external; function strategies(address) external view returns (address); diff --git a/src/interfaces/strategy.sol b/src/interfaces/strategy.sol index 22d650bb9..ea76f2a41 100644 --- a/src/interfaces/strategy.sol +++ b/src/interfaces/strategy.sol @@ -16,10 +16,12 @@ interface IStrategy { function withdraw(address) external; - function getRedeemableReward() external view returns (uint256); + function pendingReward() external view returns (uint256); function withdraw(uint256) external; + function withdrawReward(uint256) external; + function skim() external; function withdrawAll() external returns (uint256); diff --git a/src/pickle-jar-alusd-3crv.sol b/src/pickle-jar-symbiotic.sol similarity index 53% rename from src/pickle-jar-alusd-3crv.sol rename to src/pickle-jar-symbiotic.sol index 94676ea75..189a7da83 100644 --- a/src/pickle-jar-alusd-3crv.sol +++ b/src/pickle-jar-symbiotic.sol @@ -7,7 +7,8 @@ import "./interfaces/controller.sol"; import "./lib/erc20.sol"; import "./interfaces/strategy.sol"; import "./lib/safe-math.sol"; -contract PickleJarAlusd3Crv is ERC20 { + +contract PickleJarSymbiotic is ERC20 { using SafeERC20 for IERC20; using Address for address; using SafeMath for uint256; @@ -15,14 +16,33 @@ contract PickleJarAlusd3Crv is ERC20 { IERC20 public token; IERC20 public reward; - uint256 public min = 9500; + struct UserInfo { + uint256 reward; + uint256 rewardDebt; + } + + mapping(address => UserInfo) public userInfo; + + uint256 accRewardPerShare; + uint256 lastPendingReward; + + uint256 public min = 10000; uint256 public constant max = 10000; address public governance; address public timelock; address public controller; - constructor(address _token, address _reward, address _governance, address _timelock, address _controller) + event Deposit(address indexed user, uint256 _amount, uint256 _shares); + event Withdraw(address indexed user, uint256 _amount, uint256 _shares); + + constructor( + address _token, + address _reward, + address _governance, + address _timelock, + address _controller + ) public ERC20( string(abi.encodePacked("pickling ", ERC20(_token).name())), @@ -38,12 +58,9 @@ contract PickleJarAlusd3Crv is ERC20 { } function balance() public view returns (uint256) { - return - token.balanceOf(address(this)).add( - IController(controller).balanceOf(address(token)) - ); + return token.balanceOf(address(this)).add(IController(controller).balanceOf(address(token))); } - + function setMin(uint256 _min) external { require(msg.sender == governance, "!governance"); require(_min <= max, "numerator cannot be greater than denominator"); @@ -82,6 +99,15 @@ contract PickleJarAlusd3Crv is ERC20 { } function deposit(uint256 _amount) public { + _updateReward(); + UserInfo storage user = userInfo[msg.sender]; + if (_amount > 0) { + uint256 _pending = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36).sub(user.rewardDebt); + IController(controller).withdrawReward(address(token), _pending); + reward.safeTransfer(msg.sender, _pending); + + lastPendingReward = lastPendingReward.sub(_pending); + } uint256 _pool = balance(); uint256 _before = token.balanceOf(address(this)); token.safeTransferFrom(msg.sender, address(this), _amount); @@ -94,7 +120,17 @@ contract PickleJarAlusd3Crv is ERC20 { shares = (_amount.mul(totalSupply())).div(_pool); } _mint(msg.sender, shares); - earn(); //to prevent the exploit + user.rewardDebt = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36); + emit Deposit(msg.sender, _amount, shares); + earn(); //earn everytime deposit happens + } + + function _updateReward() internal { + if (totalSupply() == 0) return; + uint256 curPendingReward = pendingReward(); + uint256 addedReward = curPendingReward.sub(lastPendingReward); + accRewardPerShare = accRewardPerShare.add((addedReward.mul(1e36)).div(totalSupply())); + lastPendingReward = curPendingReward; } function withdrawAll() external { @@ -108,20 +144,37 @@ contract PickleJarAlusd3Crv is ERC20 { IERC20(reserve).safeTransfer(controller, amount); } - // No rebalance implementation for lower fees and faster swaps + function pendingReward() public returns (uint256) { + return IStrategy(IController(controller).strategies(address(token))).pendingReward(); + } + function withdraw(uint256 _shares) public { + UserInfo storage user = userInfo[msg.sender]; + uint256 _balance = balanceOf(msg.sender); + require(_balance >= _shares, "Invalid amount"); + _updateReward(); + uint256 _pending = _balance.mul(accRewardPerShare).div(1e36).sub(user.rewardDebt); + IController(controller).withdrawReward(address(token), _pending); + reward.safeTransfer(msg.sender, _pending); + uint256 r = (balance().mul(_shares)).div(totalSupply()); _burn(msg.sender, _shares); + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _withdraw = r.sub(b); + IController(controller).withdraw(address(token), _withdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _withdraw) { + r = b.add(_diff); + } + } + token.safeTransfer(msg.sender, r); + _balance = balanceOf(msg.sender); - uint256 _redeemable_reward = IStrategy(IController(controller).strategies(address(token))).getRedeemableReward(); - uint256 _rewardAmount = (_redeemable_reward.mul(_shares)).div(totalSupply().add(_shares)); //add missing shares - - IController(controller).withdraw(address(token), _shares); - uint256 _reward_balance = reward.balanceOf(address(this)); - - if (_reward_balance < _rewardAmount) _rewardAmount = _reward_balance; + user.rewardDebt = _balance.mul(accRewardPerShare).div(1e36); - token.safeTransfer(msg.sender, _shares); - reward.safeTransfer(msg.sender, _rewardAmount); + lastPendingReward = lastPendingReward.sub(_pending); + emit Withdraw(msg.sender, r, _shares); } function getRatio() public view returns (uint256) { diff --git a/src/strategies/alchemix/strategy-alusd-3crv.sol b/src/strategies/alchemix/strategy-alusd-3crv.sol index c68eb723f..70dc38d0c 100644 --- a/src/strategies/alchemix/strategy-alusd-3crv.sol +++ b/src/strategies/alchemix/strategy-alusd-3crv.sol @@ -4,7 +4,6 @@ pragma solidity ^0.6.7; import "../strategy-alcx-farm-base.sol"; contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { - uint256 public alusd_3crv_poolId = 4; address public alusd_3crv = 0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c; @@ -14,23 +13,13 @@ contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { address _strategist, address _controller, address _timelock - ) - public - StrategyAlchemixFarmBase( - alusd_3crv_poolId, - alusd_3crv, - _governance, - _strategist, - _controller, - _timelock - ) - { - IERC20(alcx).approve(stakingPool, uint(-1)); + ) public StrategyAlchemixFarmBase(alusd_3crv_poolId, alusd_3crv, _governance, _strategist, _controller, _timelock) { + IERC20(alcx).approve(stakingPool, uint256(-1)); } // **** Views **** - function getName() external override pure returns (string memory) { + function getName() external pure override returns (string memory) { return "StrategyCurveAlusd3Crv"; } @@ -43,8 +32,8 @@ contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { function harvest() public override onlyBenevolent { // Collects Alcx tokens uint256 _alcxHarvestable = getAlcxFarmHarvestable(); - if (_alcxHarvestable > 0) IStakingPools(stakingPool).claim(alcxPoolId); //claim from alcx staking pool - + if (_alcxHarvestable > 0) IStakingPools(stakingPool).claim(alcxPoolId); //claim from alcx staking pool + uint256 _harvestable = getHarvestable(); if (_harvestable > 0) IStakingPools(stakingPool).claim(poolId); //claim from alusd_3crv staking pool @@ -52,76 +41,46 @@ contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { if (_alcx > 0) { // 10% is locked up for future gov uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); - IERC20(alcx).safeTransfer( - IController(controller).treasury(), - _keepAlcx - ); + IERC20(alcx).safeTransfer(IController(controller).treasury(), _keepAlcx); uint256 _amount = _alcx.sub(_keepAlcx); IStakingPools(stakingPool).deposit(alcxPoolId, _amount); //stake to alcx farm } } - function _withdrawToJar(uint256 _amount) internal returns(uint256) { - address _jar = getJarAddress(); + function withdrawReward(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + address _jar = IController(controller).jars(address(want)); address reward_token = IJar(_jar).reward(); - require (reward_token != address(0), "Reward token is not set in the pickle jar"); - - uint256 _rewardAmount = (getRedeemableReward().mul(_amount)) - .div(IJar(_jar).totalSupply().add(_amount)); //add missing shares - _withdrawSomeReward(_rewardAmount); - uint256 _reward_balance = IERC20(reward_token).balanceOf(address(this)); - - if (_reward_balance < _rewardAmount) _rewardAmount = _reward_balance; - - uint256 _feeDev = _rewardAmount.mul(withdrawalDevFundFee).div( - withdrawalDevFundMax - ); - IERC20(reward_token).safeTransfer(IController(controller).devfund(), _feeDev); - - uint256 _feeTreasury = _rewardAmount.mul(withdrawalTreasuryFee).div( - withdrawalTreasuryMax - ); - - IERC20(reward_token).safeTransfer( - IController(controller).treasury(), - _feeTreasury - ); - - uint256 _send_amount = _rewardAmount.sub(_feeDev).sub(_feeTreasury); - - IERC20(reward_token).safeTransfer(_jar, _send_amount); - return _send_amount; - } - function _withdrawSome(uint256 _amount) - internal - override - returns (uint256) - { - _withdrawToJar(_amount); - address _jar = getJarAddress(); - uint256 r = (IJar(_jar).balance().mul(_amount)).div(IJar(_jar).totalSupply().add(_amount)); //add missing amount because it is already burned - uint256 _want_amount = r.sub(IERC20(want).balanceOf(_jar)); //sub existing want balance - IStakingPools(stakingPool).withdraw(poolId, _want_amount); - - return _want_amount; - } - - function getJarAddress() public view returns (address) { - address _jar = IController(controller).jars(address(want)); - require(_jar != address(0), "!jar"); // additional protection so we don't burn the funds - - return _jar; + uint256 _balance = IERC20(alcx).balanceOf(address(this)); + uint256 _pendingReward = pendingReward(); + require(reward_token != address(0), "Reward token is not set in the pickle jar"); + require(reward_token == alcx, "Reward token is invalid"); + require(_pendingReward >= _amount, "[withdrawReward] Withdraw amount exceed redeemable amount"); + + _balance = IERC20(alcx).balanceOf(address(this)); + if (_balance < _amount) IStakingPools(stakingPool).claim(alcxPoolId); + _balance = IERC20(alcx).balanceOf(address(this)); + if (_balance < _amount) { + uint256 _r = _amount.sub(_balance); + uint256 _alcxDeposited = getAlcxDeposited(); + IStakingPools(stakingPool).withdraw(alcxPoolId, _alcxDeposited >= _r ? _r : _alcxDeposited); + } + _balance = IERC20(alcx).balanceOf(address(this)); + if (_balance < _amount) IStakingPools(stakingPool).claim(poolId); + _balance = IERC20(alcx).balanceOf(address(this)); + require(_balance >= _amount, "[WithdrawReward] Withdraw amount exceed balance"); //double check + IERC20(reward_token).safeTransfer(_jar, _amount); + __redeposit(); } - function _withdrawSomeReward(uint256 _amount) - internal - returns (uint256) - { - IStakingPools(stakingPool).withdraw(alcxPoolId, _amount); - return _amount; + function getAlcxDeposited() public view returns (uint256) { + return IStakingPools(stakingPool).getStakeTotalDeposited(address(this), alcxPoolId); } - function getRedeemableReward() public view returns (uint256) { - return IStakingPools(stakingPool).getStakeTotalDeposited(address(this), alcxPoolId); + function pendingReward() public view returns (uint256) { + return + IStakingPools(stakingPool).getStakeTotalDeposited(address(this), alcxPoolId).add( + getHarvestable().add(getAlcxFarmHarvestable()) + ); } } diff --git a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol index b05352f7b..fa2c5b8ef 100644 --- a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol +++ b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol @@ -87,13 +87,4 @@ contract StrategySushiEthAlcxLp is StrategyAlchemixFarmBase { _distributePerformanceFeesAndDeposit(); } - - function _withdrawSome(uint256 _amount) - internal - override - returns (uint256) - { - IStakingPools(stakingPool).withdraw(poolId, _amount); - return _amount; - } } diff --git a/src/strategies/strategy-alcx-farm-base.sol b/src/strategies/strategy-alcx-farm-base.sol index 213847754..3ac41d95a 100644 --- a/src/strategies/strategy-alcx-farm-base.sol +++ b/src/strategies/strategy-alcx-farm-base.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.6.7; -import "./strategy-base.sol"; +import "./strategy-base-symbiotic.sol"; import "../interfaces/alcx-farm.sol"; -abstract contract StrategyAlchemixFarmBase is StrategyBase { +abstract contract StrategyAlchemixFarmBase is StrategyBaseSymbiotic { // Token addresses address public constant alcx = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; address public constant stakingPool = 0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa; @@ -12,7 +12,7 @@ abstract contract StrategyAlchemixFarmBase is StrategyBase { // How much Alcx tokens to keep? uint256 public keepAlcx = 0; uint256 public constant keepAlcxMax = 10000; - + uint256 public alcxPoolId = 1; uint256 public poolId; @@ -24,20 +24,11 @@ abstract contract StrategyAlchemixFarmBase is StrategyBase { address _strategist, address _controller, address _timelock - ) - public - StrategyBase( - _token, - _governance, - _strategist, - _controller, - _timelock - ) - { + ) public StrategyBaseSymbiotic(_token, _governance, _strategist, _controller, _timelock) { poolId = _poolId; } - - function balanceOfPool() public override view returns (uint256) { + + function balanceOfPool() public view override returns (uint256) { uint256 amount = IStakingPools(stakingPool).getStakeTotalDeposited(address(this), poolId); return amount; } @@ -57,6 +48,16 @@ abstract contract StrategyAlchemixFarmBase is StrategyBase { } } + function _withdrawSome(uint256 _amount) internal override returns (uint256) { + IStakingPools(stakingPool).withdraw(poolId, _amount); + return _amount; + } + + function __redeposit() internal override { + uint256 _balance = IERC20(alcx).balanceOf(address(this)); + if (_balance > 0) IStakingPools(stakingPool).deposit(alcxPoolId, _balance); //stake to alcx farm + } + // **** Setters **** function setKeepAlcx(uint256 _keepAlcx) external { diff --git a/src/strategies/strategy-base-symbiotic.sol b/src/strategies/strategy-base-symbiotic.sol new file mode 100644 index 000000000..4fd3d6e58 --- /dev/null +++ b/src/strategies/strategy-base-symbiotic.sol @@ -0,0 +1,314 @@ +pragma solidity ^0.6.7; + +import "../lib/erc20.sol"; +import "../lib/safe-math.sol"; + +import "../interfaces/jar.sol"; +import "../interfaces/staking-rewards.sol"; +import "../interfaces/masterchef.sol"; +import "../interfaces/uniswapv2.sol"; +import "../interfaces/controller.sol"; + +// Strategy Contract Basics + +abstract contract StrategyBaseSymbiotic { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + // Perfomance fees - start with 20% + uint256 public performanceTreasuryFee = 2000; + uint256 public constant performanceTreasuryMax = 10000; + + uint256 public performanceDevFee = 0; + uint256 public constant performanceDevMax = 10000; + + // Withdrawal fee 0% + // - 0% to treasury + // - 0% to dev fund + uint256 public withdrawalTreasuryFee = 0; + uint256 public constant withdrawalTreasuryMax = 100000; + + uint256 public withdrawalDevFundFee = 0; + uint256 public constant withdrawalDevFundMax = 100000; + + // Tokens + address public want; + address public constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // User accounts + address public governance; + address public controller; + address public strategist; + address public timelock; + + // Dex + address public univ2Router2 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + address public sushiRouter = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; + + mapping(address => bool) public harvesters; + + constructor( + address _want, + address _governance, + address _strategist, + address _controller, + address _timelock + ) public { + require(_want != address(0)); + require(_governance != address(0)); + require(_strategist != address(0)); + require(_controller != address(0)); + require(_timelock != address(0)); + + want = _want; + governance = _governance; + strategist = _strategist; + controller = _controller; + timelock = _timelock; + } + + // **** Modifiers **** // + + modifier onlyBenevolent { + require(harvesters[msg.sender] || msg.sender == governance || msg.sender == strategist); + _; + } + + // **** Views **** // + + function balanceOfWant() public view returns (uint256) { + return IERC20(want).balanceOf(address(this)); + } + + function balanceOfPool() public view virtual returns (uint256); + + function balanceOf() public view returns (uint256) { + return balanceOfWant().add(balanceOfPool()); + } + + function getName() external pure virtual returns (string memory); + + // **** Setters **** // + + function whitelistHarvester(address _harvester) external { + require(msg.sender == governance || msg.sender == strategist, "not authorized"); + harvesters[_harvester] = true; + } + + function revokeHarvester(address _harvester) external { + require(msg.sender == governance || msg.sender == strategist, "not authorized"); + harvesters[_harvester] = false; + } + + function setWithdrawalDevFundFee(uint256 _withdrawalDevFundFee) external { + require(msg.sender == timelock, "!timelock"); + withdrawalDevFundFee = _withdrawalDevFundFee; + } + + function setWithdrawalTreasuryFee(uint256 _withdrawalTreasuryFee) external { + require(msg.sender == timelock, "!timelock"); + withdrawalTreasuryFee = _withdrawalTreasuryFee; + } + + function setPerformanceDevFee(uint256 _performanceDevFee) external { + require(msg.sender == timelock, "!timelock"); + performanceDevFee = _performanceDevFee; + } + + function setPerformanceTreasuryFee(uint256 _performanceTreasuryFee) external { + require(msg.sender == timelock, "!timelock"); + performanceTreasuryFee = _performanceTreasuryFee; + } + + function setStrategist(address _strategist) external { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setGovernance(address _governance) external { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setTimelock(address _timelock) external { + require(msg.sender == timelock, "!timelock"); + timelock = _timelock; + } + + function setController(address _controller) external { + require(msg.sender == timelock, "!timelock"); + controller = _controller; + } + + // **** State mutations **** // + function deposit() public virtual; + + function __redeposit() internal virtual; + + // Controller only function for creating additional rewards from dust + function withdraw(IERC20 _asset) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + require(want != address(_asset), "want"); + balance = _asset.balanceOf(address(this)); + _asset.safeTransfer(controller, balance); + } + + // Withdraw partial funds, normally used with a jar withdrawal + function withdraw(uint256 _amount) external { + require(msg.sender == controller, "!controller"); + uint256 _balance = IERC20(want).balanceOf(address(this)); + if (_balance < _amount) { + _amount = _withdrawSome(_amount.sub(_balance)); + _amount = _amount.add(_balance); + } + + uint256 _feeDev = _amount.mul(withdrawalDevFundFee).div(withdrawalDevFundMax); + IERC20(want).safeTransfer(IController(controller).devfund(), _feeDev); + + uint256 _feeTreasury = _amount.mul(withdrawalTreasuryFee).div(withdrawalTreasuryMax); + IERC20(want).safeTransfer(IController(controller).treasury(), _feeTreasury); + + address _jar = IController(controller).jars(address(want)); + require(_jar != address(0), "!jar"); // additional protection so we don't burn the funds + + IERC20(want).safeTransfer(_jar, _amount.sub(_feeDev).sub(_feeTreasury)); + + __redeposit(); + } + + // Withdraw funds, used to swap between strategies + function withdrawForSwap(uint256 _amount) external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawSome(_amount); + + balance = IERC20(want).balanceOf(address(this)); + + address _jar = IController(controller).jars(address(want)); + require(_jar != address(0), "!jar"); + IERC20(want).safeTransfer(_jar, balance); + } + + // Withdraw all funds, normally used when migrating strategies + function withdrawAll() external returns (uint256 balance) { + require(msg.sender == controller, "!controller"); + _withdrawAll(); + + balance = IERC20(want).balanceOf(address(this)); + + address _jar = IController(controller).jars(address(want)); + require(_jar != address(0), "!jar"); // additional protection so we don't burn the funds + IERC20(want).safeTransfer(_jar, balance); + } + + function _withdrawAll() internal { + _withdrawSome(balanceOfPool()); + } + + function _withdrawSome(uint256 _amount) internal virtual returns (uint256); + + function harvest() public virtual; + + // **** Emergency functions **** + + function execute(address _target, bytes memory _data) public payable returns (bytes memory response) { + require(msg.sender == timelock, "!timelock"); + require(_target != address(0), "!target"); + + // call contract in current context + assembly { + let succeeded := delegatecall(sub(gas(), 5000), _target, add(_data, 0x20), mload(_data), 0, 0) + let size := returndatasize() + + response := mload(0x40) + mstore(0x40, add(response, and(add(add(size, 0x20), 0x1f), not(0x1f)))) + mstore(response, size) + returndatacopy(add(response, 0x20), 0, size) + + switch iszero(succeeded) + case 1 { + // throw if delegatecall failed + revert(add(response, 0x20), size) + } + } + } + + // **** Internal functions **** + function _swapUniswap( + address _from, + address _to, + uint256 _amount + ) internal { + require(_to != address(0)); + + address[] memory path; + + if (_from == weth || _to == weth) { + path = new address[](2); + path[0] = _from; + path[1] = _to; + } else { + path = new address[](3); + path[0] = _from; + path[1] = weth; + path[2] = _to; + } + + UniswapRouterV2(univ2Router2).swapExactTokensForTokens(_amount, 0, path, address(this), now.add(60)); + } + + function _swapUniswapWithPath(address[] memory path, uint256 _amount) internal { + require(path[1] != address(0)); + + UniswapRouterV2(univ2Router2).swapExactTokensForTokens(_amount, 0, path, address(this), now.add(60)); + } + + function _swapSushiswap( + address _from, + address _to, + uint256 _amount + ) internal { + require(_to != address(0)); + + address[] memory path; + + if (_from == weth || _to == weth) { + path = new address[](2); + path[0] = _from; + path[1] = _to; + } else { + path = new address[](3); + path[0] = _from; + path[1] = weth; + path[2] = _to; + } + + UniswapRouterV2(sushiRouter).swapExactTokensForTokens(_amount, 0, path, address(this), now.add(60)); + } + + function _swapSushiswapWithPath(address[] memory path, uint256 _amount) internal { + require(path[1] != address(0)); + + UniswapRouterV2(sushiRouter).swapExactTokensForTokens(_amount, 0, path, address(this), now.add(60)); + } + + function _distributePerformanceFeesAndDeposit() internal { + uint256 _want = IERC20(want).balanceOf(address(this)); + + if (_want > 0) { + // Treasury fees + IERC20(want).safeTransfer( + IController(controller).treasury(), + _want.mul(performanceTreasuryFee).div(performanceTreasuryMax) + ); + + // Performance fee + IERC20(want).safeTransfer( + IController(controller).devfund(), + _want.mul(performanceDevFee).div(performanceDevMax) + ); + + deposit(); + } + } +} diff --git a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js index 388eb583e..04912e14b 100644 --- a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js +++ b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js @@ -1,194 +1,225 @@ const hre = require("hardhat"); var chaiAsPromised = require("chai-as-promised"); const StrategyCurveAlusd3Crv = hre.artifacts.require("StrategyCurveAlusd3Crv"); -const PickleJarAlusd3Crv = hre.artifacts.require("PickleJarAlusd3Crv"); +const PickleJarSymbiotic = hre.artifacts.require("PickleJarSymbiotic"); const ControllerV4 = hre.artifacts.require("ControllerV4"); -const { assert } = require("chai").use(chaiAsPromised); -const { time } = require("@openzeppelin/test-helpers"); +const {assert} = require("chai").use(chaiAsPromised); +const {time} = require("@openzeppelin/test-helpers"); const ERC20_ABI = require("./abi/ERC20.json"); const unlockAccount = async (address) => { - await hre.network.provider.send("hardhat_impersonateAccount", [address]); - return hre.ethers.provider.getSigner(address); + await hre.network.provider.send("hardhat_impersonateAccount", [address]); + return hre.ethers.provider.getSigner(address); }; const toWei = (ethAmount) => { - return hre.ethers.constants.WeiPerEther.mul(hre.ethers.BigNumber.from(ethAmount)); + return hre.ethers.constants.WeiPerEther.mul(hre.ethers.BigNumber.from(ethAmount)); }; describe("StrategyCurveAlusd3Crv Unit test", () => { - let strategy, pickleJar, controller; - const want = "0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c"; - let alusd_3crv, alusd_3crv_whale; - let deployer, alice, bob; - let alcx, - alcx_addr = "0xdbdb4d16eda451d0503b854cf79d55697f90c8df"; - let governance, strategist, devfund, treasury, timelock; + let strategy, pickleJar, controller; + const want = "0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c"; + let alusd_3crv, alusd_3crv_whale; + let deployer, alice, bob; + let alcx, + alcx_addr = "0xdbdb4d16eda451d0503b854cf79d55697f90c8df"; + let governance, strategist, devfund, treasury, timelock; - before("Deploy contracts", async () => { - [governance, devfund, treasury] = await web3.eth.getAccounts(); - const signers = await hre.ethers.getSigners(); - deployer = signers[0]; - alice = signers[3]; - bob = signers[4]; + before("Deploy contracts", async () => { + [governance, devfund, treasury] = await web3.eth.getAccounts(); + const signers = await hre.ethers.getSigners(); + deployer = signers[0]; + alice = signers[3]; + bob = signers[4]; + john = signers[5]; - strategist = governance; - timelock = governance; + strategist = governance; + timelock = governance; - controller = await ControllerV4.new(governance, strategist, timelock, devfund, treasury); - console.log("controller is deployed at =====> ", controller.address); + controller = await ControllerV4.new(governance, strategist, timelock, devfund, treasury); + console.log("controller is deployed at =====> ", controller.address); - strategy = await StrategyCurveAlusd3Crv.new(governance, strategist, controller.address, timelock); - console.log("Strategy is deployed at =====> ", strategy.address); + strategy = await StrategyCurveAlusd3Crv.new(governance, strategist, controller.address, timelock); + console.log("Strategy is deployed at =====> ", strategy.address); - pickleJar = await PickleJarAlusd3Crv.new(want, alcx_addr, governance, timelock, controller.address); - console.log("pickleJar is deployed at =====> ", pickleJar.address); + pickleJar = await PickleJarSymbiotic.new(want, alcx_addr, governance, timelock, controller.address); + console.log("pickleJar is deployed at =====> ", pickleJar.address); - await controller.setJar(want, pickleJar.address, { from: governance }); - await controller.approveStrategy(want, strategy.address, { from: governance }); - await controller.setStrategy(want, strategy.address, { from: governance }); + await controller.setJar(want, pickleJar.address, {from: governance}); + await controller.approveStrategy(want, strategy.address, { + from: governance, + }); + await controller.setStrategy(want, strategy.address, {from: governance}); - await strategy.setKeepAlcx("2000", { from: governance }); + await strategy.setKeepAlcx("2000", {from: governance}); - alusd_3crv_whale = await unlockAccount("0xBAF18722C137E725327F1376329d3c99F26f6A60"); - alusd_3crv = await hre.ethers.getContractAt(ERC20_ABI, want); - alcx = await hre.ethers.getContractAt(ERC20_ABI, alcx_addr); + alusd_3crv_whale = await unlockAccount("0xBAF18722C137E725327F1376329d3c99F26f6A60"); + alusd_3crv = await hre.ethers.getContractAt(ERC20_ABI, want); + alcx = await hre.ethers.getContractAt(ERC20_ABI, alcx_addr); - alusd_3crv.connect(alusd_3crv_whale).transfer(alice.address, toWei(5000)); - assert.equal((await alusd_3crv.balanceOf(alice.address)).toString(), toWei(5000).toString()); - alusd_3crv.connect(alusd_3crv_whale).transfer(bob.address, toWei(3000)); - assert.equal((await alusd_3crv.balanceOf(bob.address)).toString(), toWei(3000).toString()); - }); + alusd_3crv.connect(alusd_3crv_whale).transfer(alice.address, toWei(2000)); + assert.equal((await alusd_3crv.balanceOf(alice.address)).toString(), toWei(2000).toString()); + alusd_3crv.connect(alusd_3crv_whale).transfer(bob.address, toWei(1000)); + assert.equal((await alusd_3crv.balanceOf(bob.address)).toString(), toWei(1000).toString()); + alusd_3crv.connect(alusd_3crv_whale).transfer(john.address, toWei(2500)); + assert.equal((await alusd_3crv.balanceOf(john.address)).toString(), toWei(2500).toString()); + }); - it("Should harvest the reward correctly", async () => { - console.log("\n---------------------------Alice deposit---------------------------------------\n"); - await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(5000)); - await pickleJar.deposit(toWei(5000), { from: alice.address }); - console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); - await pickleJar.earn({ from: alice.address }); + it("Should harvest the reward correctly", async () => { + console.log("\n---------------------------Alice deposit---------------------------------------\n"); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(2000)); + await pickleJar.deposit(toWei(2000), {from: alice.address}); + console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); + // await pickleJar.earn({ from: alice.address }); - await harvest(); + await harvest(); - console.log("\n---------------------------Bob deposit---------------------------------------\n"); - await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(3000)); - await pickleJar.deposit(toWei(3000), { from: bob.address }); - console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(bob.address)).toString()); - await pickleJar.earn({ from: bob.address }); - await harvest(); + console.log("\n---------------------------Bob deposit---------------------------------------\n"); + await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(1000)); + await pickleJar.deposit(toWei(1000), {from: bob.address}); + console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(bob.address)).toString()); + // await pickleJar.earn({ from: bob.address }); - console.log("\n---------------------------Alice withdraw---------------------------------------\n"); - console.log("Redeemable Reward of Strategy =====> ", (await strategy.getRedeemableReward()).toString()); + await time.increase(60 * 60 * 24 * 7); - const _devBefore = await alusd_3crv.balanceOf(devfund); - let _treasuryBefore = await alcx.balanceOf(treasury); + console.log("\n---------------------------John deposit---------------------------------------\n"); + await alusd_3crv.connect(john).approve(pickleJar.address, toWei(2500)); + await pickleJar.deposit(toWei(2500), {from: john.address}); + console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(john.address)).toString()); - console.log("Dev fund balance before ===> ", _devBefore.toString()); - console.log("Treasury balance before ===> ", _treasuryBefore.toString()); + await harvest(); - let _alcx_before = await alcx.balanceOf(alice.address); - console.log("Alice alcx balance before =====> ", _alcx_before.toString()); + console.log("\n---------------------------Alice withdraw---------------------------------------\n"); + console.log("Reward balance of strategy ====> ", (await alcx.balanceOf(strategy.address)).toString()); + let _alcx_before = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance before =====> ", _alcx_before.toString()); - await pickleJar.withdrawAll({ from: alice.address }); + await pickleJar.withdrawAll({from: alice.address}); - let _alcx_after = await alcx.balanceOf(alice.address); - console.log("Alice alcx balance after =====> ", _alcx_after.toString()); + let _alcx_after = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance after =====> ", _alcx_after.toString()); - assert.equal(_alcx_after.gt(_alcx_before), true); + assert.equal(_alcx_after.gt(_alcx_before), true); - const _devAfter = await alusd_3crv.balanceOf(devfund); - let _treasuryAfter = await alcx.balanceOf(treasury); + console.log("Pending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); - console.log("Dev fund balance after ===> ", _devAfter.toString()); - console.log("Treasury balance after ===> ", _treasuryAfter.toString()); + await time.increase(60 * 60 * 24 * 3); - // 0% goes to dev when withdraw - assert.equal(_devAfter.eq(_devBefore), true); + console.log("\n---------------------------Alice Redeposit---------------------------------------\n"); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(2000)); + await pickleJar.deposit(toWei(2000), {from: alice.address}); + console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); - // 0% goes to treasury when withdraw - assert.equal(_treasuryAfter.eq(_treasuryBefore), true); + await time.increase(60 * 60 * 24 * 4); - console.log("\n---------------------------Bob withdraw---------------------------------------\n"); + console.log("\n---------------------------Bob withdraw---------------------------------------\n"); - console.log("Redeemable Reward of Strategy =====> ", (await strategy.getRedeemableReward()).toString()); - _alcx_before = await alcx.balanceOf(bob.address); - console.log("Bob alcx balance before =====> ", _alcx_before.toString()); + console.log("Reward balance of strategy ====> ", (await alcx.balanceOf(strategy.address)).toString()); + _alcx_before = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance before =====> ", _alcx_before.toString()); - await pickleJar.withdrawAll({ from: bob.address }); + await pickleJar.withdrawAll({from: bob.address}); - _alcx_after = await alcx.balanceOf(bob.address); - console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); - assert.equal(_alcx_after.gt(_alcx_before), true); - }); + _alcx_after = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); + assert.equal(_alcx_after.gt(_alcx_before), true); - it("Should withdraw the want correctly", async () => { - console.log("\n---------------------------Alice deposit---------------------------------------\n"); - await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(5000)); - await pickleJar.deposit(toWei(5000), { from: alice.address }); - console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); - await pickleJar.earn({ from: alice.address }); + console.log("Pending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); - await harvest(); + console.log("\n---------------------------John withdraw---------------------------------------\n"); + console.log("Reward balance of strategy ====> ", (await alcx.balanceOf(strategy.address)).toString()); - console.log("\n---------------------------Bob deposit---------------------------------------\n"); - await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(3000)); - await pickleJar.deposit(toWei(3000), { from: bob.address }); - console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(bob.address)).toString()); - await pickleJar.earn({ from: bob.address }); - await harvest(); + _alcx_before = await alcx.balanceOf(john.address); + console.log("John alcx balance before =====> ", _alcx_before.toString()); - console.log("\n---------------------------Alice withdraw---------------------------------------\n"); + await pickleJar.withdrawAll({from: john.address}); - let _jar_before = await alusd_3crv.balanceOf(pickleJar.address); - await controller.withdrawAll(alusd_3crv.address, { from: governance }); - let _jar_after = await alusd_3crv.balanceOf(pickleJar.address); + _alcx_after = await alcx.balanceOf(john.address); + console.log("John alcx balance after =====> ", (await alcx.balanceOf(john.address)).toString()); + assert.equal(_alcx_after.gt(_alcx_before), true); - let _alcx_before = await alcx.balanceOf(alice.address); - console.log("Alice alcx balance before =====> ", _alcx_before.toString()); + console.log("\n---------------------------Alice second withdraw---------------------------------------\n"); + _alcx_before = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance before =====> ", _alcx_before.toString()); - await pickleJar.withdrawAll({ from: alice.address }); + await pickleJar.withdrawAll({from: alice.address}); - let _alcx_after = await alcx.balanceOf(alice.address); - console.log("Alice alcx balance after =====> ", _alcx_after.toString()); + _alcx_after = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance after =====> ", _alcx_after.toString()); - console.log("\n---------------------------Bob withdraw---------------------------------------\n"); + assert.equal(_alcx_after.gt(_alcx_before), true); + console.log("Pending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); + }); - _alcx_before = await alcx.balanceOf(bob.address); - console.log("Bob alcx balance before =====> ", _alcx_before.toString()); + it("Should withdraw the want correctly", async () => { + console.log("\n---------------------------Alice deposit---------------------------------------\n"); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(2000)); + await pickleJar.deposit(toWei(2000), {from: alice.address}); + console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); + await pickleJar.earn({from: alice.address}); - _jar_before = await alusd_3crv.balanceOf(pickleJar.address); + await harvest(); - await controller.withdrawAll(alusd_3crv.address, { from: governance }); + console.log("\n---------------------------Bob deposit---------------------------------------\n"); + await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(1000)); + await pickleJar.deposit(toWei(1000), {from: bob.address}); + console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(bob.address)).toString()); + await pickleJar.earn({from: bob.address}); + await harvest(); - _jar_after = await alusd_3crv.balanceOf(pickleJar.address); + console.log("\n---------------------------Alice withdraw---------------------------------------\n"); - await pickleJar.withdrawAll({ from: bob.address }); + let _jar_before = await alusd_3crv.balanceOf(pickleJar.address); + await controller.withdrawAll(alusd_3crv.address, {from: governance}); + let _jar_after = await alusd_3crv.balanceOf(pickleJar.address); - _alcx_after = await alcx.balanceOf(bob.address); - console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); - assert.equal(_alcx_after.gt(_alcx_before), true); - }); + let _alcx_before = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance before =====> ", _alcx_before.toString()); - const harvest = async () => { - await time.increase(60 * 60 * 24 * 15); //15 days - const _balance = await strategy.balanceOfPool(); - console.log("Deposited amount of strategy ===> ", _balance.toString()); + await pickleJar.withdrawAll({from: alice.address}); - let _alcx = await strategy.getHarvestable(); - console.log("Alusd3Crv Farm harvestable of strategy of the first harvest ===> ", _alcx.toString()); - let _alcx2 = await strategy.getAlcxFarmHarvestable(); - console.log("Alcx Farm harvestable of strategy of the first harvest ===> ", _alcx2.toString()); + let _alcx_after = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance after =====> ", _alcx_after.toString()); - await strategy.harvest({ from: governance }); - await time.increase(60 * 60 * 24 * 30); + console.log("\n---------------------------Bob withdraw---------------------------------------\n"); - _alcx = await strategy.getHarvestable(); - console.log("Alusd3Crv Farm harvestable of strategy of the second harvest ===> ", _alcx.toString()); + _alcx_before = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance before =====> ", _alcx_before.toString()); - _alcx2 = await strategy.getAlcxFarmHarvestable(); - console.log("Alcx Farm harvestable of strategy of the second harvest ===> ", _alcx2.toString()); + _jar_before = await alusd_3crv.balanceOf(pickleJar.address); - await strategy.harvest({ from: governance }); - }; + await controller.withdrawAll(alusd_3crv.address, {from: governance}); + + _jar_after = await alusd_3crv.balanceOf(pickleJar.address); + + await pickleJar.withdrawAll({from: bob.address}); + + _alcx_after = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); + assert.equal(_alcx_after.gt(_alcx_before), true); + }); + + const harvest = async () => { + await time.increase(60 * 60 * 24 * 15); //15 days + const _balance = await strategy.balanceOfPool(); + console.log("Deposited amount of strategy ===> ", _balance.toString()); + + let _alcx = await strategy.getHarvestable(); + console.log("Alusd3Crv Farm harvestable of strategy of the first harvest ===> ", _alcx.toString()); + let _alcx2 = await strategy.getAlcxFarmHarvestable(); + console.log("Alcx Farm harvestable of strategy of the first harvest ===> ", _alcx2.toString()); + + await strategy.harvest({from: governance}); + await time.increase(60 * 60 * 24 * 15); + + _alcx = await strategy.getHarvestable(); + console.log("Alusd3Crv Farm harvestable of strategy of the second harvest ===> ", _alcx.toString()); + + _alcx2 = await strategy.getAlcxFarmHarvestable(); + console.log("Alcx Farm harvestable of strategy of the second harvest ===> ", _alcx2.toString()); + + await strategy.harvest({from: governance}); + }; }); From ff1736ec37bd0b41df61d3701a25adf08317c6f0 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Wed, 24 Mar 2021 19:50:08 -0700 Subject: [PATCH 06/19] Update userinfo struct --- src/pickle-jar-symbiotic.sol | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/pickle-jar-symbiotic.sol b/src/pickle-jar-symbiotic.sol index 189a7da83..5b5129995 100644 --- a/src/pickle-jar-symbiotic.sol +++ b/src/pickle-jar-symbiotic.sol @@ -16,12 +16,7 @@ contract PickleJarSymbiotic is ERC20 { IERC20 public token; IERC20 public reward; - struct UserInfo { - uint256 reward; - uint256 rewardDebt; - } - - mapping(address => UserInfo) public userInfo; + mapping(address => uint256) public userRewardDebt; uint256 accRewardPerShare; uint256 lastPendingReward; @@ -100,12 +95,10 @@ contract PickleJarSymbiotic is ERC20 { function deposit(uint256 _amount) public { _updateReward(); - UserInfo storage user = userInfo[msg.sender]; if (_amount > 0) { - uint256 _pending = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36).sub(user.rewardDebt); + uint256 _pending = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36).sub(userRewardDebt[msg.sender]); IController(controller).withdrawReward(address(token), _pending); reward.safeTransfer(msg.sender, _pending); - lastPendingReward = lastPendingReward.sub(_pending); } uint256 _pool = balance(); @@ -120,7 +113,7 @@ contract PickleJarSymbiotic is ERC20 { shares = (_amount.mul(totalSupply())).div(_pool); } _mint(msg.sender, shares); - user.rewardDebt = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36); + userRewardDebt[msg.sender] = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36); emit Deposit(msg.sender, _amount, shares); earn(); //earn everytime deposit happens } @@ -149,11 +142,10 @@ contract PickleJarSymbiotic is ERC20 { } function withdraw(uint256 _shares) public { - UserInfo storage user = userInfo[msg.sender]; uint256 _balance = balanceOf(msg.sender); require(_balance >= _shares, "Invalid amount"); _updateReward(); - uint256 _pending = _balance.mul(accRewardPerShare).div(1e36).sub(user.rewardDebt); + uint256 _pending = _balance.mul(accRewardPerShare).div(1e36).sub(userRewardDebt[msg.sender]); IController(controller).withdrawReward(address(token), _pending); reward.safeTransfer(msg.sender, _pending); uint256 r = (balance().mul(_shares)).div(totalSupply()); @@ -171,7 +163,7 @@ contract PickleJarSymbiotic is ERC20 { token.safeTransfer(msg.sender, r); _balance = balanceOf(msg.sender); - user.rewardDebt = _balance.mul(accRewardPerShare).div(1e36); + userRewardDebt[msg.sender] = _balance.mul(accRewardPerShare).div(1e36); lastPendingReward = lastPendingReward.sub(_pending); emit Withdraw(msg.sender, r, _shares); From e9f3949c9b17dc5d6431ae09bf937a108808d996 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Sat, 27 Mar 2021 00:51:48 -0700 Subject: [PATCH 07/19] Add proxy to the controller --- package.json | 2 + src/Proxy/AdminUpgradeabilityProxy.sol | 425 ++++++++++++++++++ src/Proxy/ProxyAdmin.sol | 70 +++ src/controller-v4.sol | 7 - src/controller-v5.sol | 375 ++++++++++++++++ src/lib/AddressUpgradeable.sol | 165 +++++++ src/lib/Initializable.sol | 55 +++ src/pickle-jar-symbiotic.sol | 41 +- .../alchemix/strategy-alusd-3crv.sol | 11 +- .../alchemix/StrategyAlusd3crv.test.js | 31 +- src/tests/strategies/alchemix/abi/ERC20.json | 356 --------------- .../strategy-sushi-eth-alcx-lp.test.sol | 101 +---- 12 files changed, 1147 insertions(+), 492 deletions(-) create mode 100644 src/Proxy/AdminUpgradeabilityProxy.sol create mode 100644 src/Proxy/ProxyAdmin.sol create mode 100644 src/controller-v5.sol create mode 100644 src/lib/AddressUpgradeable.sol create mode 100644 src/lib/Initializable.sol delete mode 100644 src/tests/strategies/alchemix/abi/ERC20.json diff --git a/package.json b/package.json index 62d20f4e6..4c79f9545 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "@nomiclabs/hardhat-etherscan": "^2.1.1", "@nomiclabs/hardhat-truffle5": "^2.0.0", "@nomiclabs/hardhat-web3": "^2.0.0", + "@openzeppelin/contracts": "^3.4.1", + "@openzeppelin/contracts-upgradeable": "^3.4.1", "@openzeppelin/test-helpers": "^0.5.10", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", diff --git a/src/Proxy/AdminUpgradeabilityProxy.sol b/src/Proxy/AdminUpgradeabilityProxy.sol new file mode 100644 index 000000000..c380bdead --- /dev/null +++ b/src/Proxy/AdminUpgradeabilityProxy.sol @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @title Proxy + * @dev Implements delegation of calls to other contracts, with proper + * forwarding of return values and bubbling of failures. + * It defines a fallback function that delegates all calls to the address + * returned by the abstract _implementation() internal function. + */ +abstract contract Proxy { + /** + * @dev Fallback function. + * Implemented entirely in `_fallback`. + */ + fallback () payable external { + _fallback(); + } + + /** + * @dev Receive function. + * Implemented entirely in `_fallback`. + */ + receive () payable external { + _fallback(); + } + + /** + * @return The Address of the implementation. + */ + function _implementation() internal virtual view returns (address); + + /** + * @dev Delegates execution to an implementation contract. + * This is a low level function that doesn't return to its internal call site. + * It will return to the external caller whatever the implementation returns. + * @param implementation Address to delegate. + */ + function _delegate(address implementation) internal { + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + /** + * @dev Function that is run as the first thing in the fallback function. + * Can be redefined in derived contracts to add functionality. + * Redefinitions must call super._willFallback(). + */ + function _willFallback() internal virtual { + } + + /** + * @dev fallback implementation. + * Extracted to enable manual triggering. + */ + function _fallback() internal { + _willFallback(); + _delegate(_implementation()); + } +} + +// File: @openzeppelin/contracts/utils/Address.sol + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies in extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { size := extcodesize(account) } + return size > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{ value: amount }(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain`call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + return _functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + return _functionCallWithValue(target, data, value, errorMessage); + } + + function _functionCallWithValue(address target, bytes memory data, uint256 weiValue, string memory errorMessage) private returns (bytes memory) { + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.call{ value: weiValue }(data); + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} + +// File: contracts/proxy/UpgradeabilityProxy.sol + + +/** + * @title UpgradeabilityProxy + * @dev This contract implements a proxy that allows to change the + * implementation address to which it will delegate. + * Such a change is called an implementation upgrade. + */ +contract UpgradeabilityProxy is Proxy { + /** + * @dev Contract constructor. + * @param _logic Address of the initial implementation. + * @param _data Data to send as msg.data to the implementation to initialize the proxied contract. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + * This parameter is optional, if no data is given the initialization call to proxied contract will be skipped. + */ + constructor(address _logic, bytes memory _data) public payable { + assert(IMPLEMENTATION_SLOT == bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)); + _setImplementation(_logic); + if(_data.length > 0) { + (bool success,) = _logic.delegatecall(_data); + require(success); + } + } + + /** + * @dev Emitted when the implementation is upgraded. + * @param implementation Address of the new implementation. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Returns the current implementation. + * @return impl Address of the current implementation + */ + function _implementation() internal override view returns (address impl) { + bytes32 slot = IMPLEMENTATION_SLOT; + assembly { + impl := sload(slot) + } + } + + /** + * @dev Upgrades the proxy to a new implementation. + * @param newImplementation Address of the new implementation. + */ + function _upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Sets the implementation address of the proxy. + * @param newImplementation Address of the new implementation. + */ + function _setImplementation(address newImplementation) internal { + require(Address.isContract(newImplementation), "Cannot set a proxy implementation to a non-contract address"); + + bytes32 slot = IMPLEMENTATION_SLOT; + + assembly { + sstore(slot, newImplementation) + } + } +} + +// File: contracts/proxy/AdminUpgradeabilityProxy.sol + +/** + * @title AdminUpgradeabilityProxy + * @dev This contract combines an upgradeability proxy with an authorization + * mechanism for administrative tasks. + * All external functions in this contract must be guarded by the + * `ifAdmin` modifier. See ethereum/solidity#3864 for a Solidity + * feature proposal that would enable this to be done automatically. + */ +contract AdminUpgradeabilityProxy is UpgradeabilityProxy { + /** + * Contract constructor. + * @param _logic address of the initial implementation. + * @param _admin Address of the proxy administrator. + * @param _data Data to send as msg.data to the implementation to initialize the proxied contract. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + * This parameter is optional, if no data is given the initialization call to proxied contract will be skipped. + */ + constructor(address _logic, address _admin, bytes memory _data) UpgradeabilityProxy(_logic, _data) public payable { + assert(ADMIN_SLOT == bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)); + _setAdmin(_admin); + } + + /** + * @dev Emitted when the administration has been transferred. + * @param previousAdmin Address of the previous admin. + * @param newAdmin Address of the new admin. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Modifier to check whether the `msg.sender` is the admin. + * If it is, it will run the function. Otherwise, it will delegate the call + * to the implementation. + */ + modifier ifAdmin() { + if (msg.sender == _admin()) { + _; + } else { + _fallback(); + } + } + + /** + * @return The address of the proxy admin. + */ + function admin() external ifAdmin returns (address) { + return _admin(); + } + + /** + * @return The address of the implementation. + */ + function implementation() external ifAdmin returns (address) { + return _implementation(); + } + + /** + * @dev Changes the admin of the proxy. + * Only the current admin can call this function. + * @param newAdmin Address to transfer proxy administration to. + */ + function changeAdmin(address newAdmin) external ifAdmin { + require(newAdmin != address(0), "Cannot change the admin of a proxy to the zero address"); + emit AdminChanged(_admin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev Upgrade the backing implementation of the proxy. + * Only the admin can call this function. + * @param newImplementation Address of the new implementation. + */ + function upgradeTo(address newImplementation) external ifAdmin { + _upgradeTo(newImplementation); + } + + /** + * @dev Upgrade the backing implementation of the proxy and call a function + * on the new implementation. + * This is useful to initialize the proxied contract. + * @param newImplementation Address of the new implementation. + * @param data Data to send as msg.data in the low level call. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + */ + function upgradeToAndCall(address newImplementation, bytes calldata data) payable external ifAdmin { + _upgradeTo(newImplementation); + (bool success,) = newImplementation.delegatecall(data); + require(success); + } + + /** + * @return adm The admin slot. + */ + function _admin() internal view returns (address adm) { + bytes32 slot = ADMIN_SLOT; + assembly { + adm := sload(slot) + } + } + + /** + * @dev Sets the address of the proxy admin. + * @param newAdmin Address of the new proxy admin. + */ + function _setAdmin(address newAdmin) internal { + bytes32 slot = ADMIN_SLOT; + + assembly { + sstore(slot, newAdmin) + } + } + + /** + * @dev Only fall back when the sender is not the admin. + */ + function _willFallback() internal override virtual { + require(msg.sender != _admin(), "Cannot call fallback function from the proxy admin"); + super._willFallback(); + } +} \ No newline at end of file diff --git a/src/Proxy/ProxyAdmin.sol b/src/Proxy/ProxyAdmin.sol new file mode 100644 index 000000000..db2398a43 --- /dev/null +++ b/src/Proxy/ProxyAdmin.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../lib/ownable.sol"; +import "./AdminUpgradeabilityProxy.sol"; + +/** + * @title ProxyAdmin + * @dev This contract is the admin of a proxy, and is in charge + * of upgrading it as well as transferring it to another admin. + */ +contract ProxyAdmin is Ownable { + + /** + * @dev Returns the current implementation of a proxy. + * This is needed because only the proxy admin can query it. + * @return The address of the current implementation of the proxy. + */ + function getProxyImplementation(AdminUpgradeabilityProxy proxy) public view returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("implementation()")) == 0x5c60da1b + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Returns the admin of a proxy. Only the admin can query it. + * @return The address of the current admin of the proxy. + */ + function getProxyAdmin(AdminUpgradeabilityProxy proxy) public view returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("admin()")) == 0xf851a440 + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Changes the admin of a proxy. + * @param proxy Proxy to change admin. + * @param newAdmin Address to transfer proxy administration to. + */ + function changeProxyAdmin(AdminUpgradeabilityProxy proxy, address newAdmin) public onlyOwner { + proxy.changeAdmin(newAdmin); + } + + /** + * @dev Upgrades a proxy to the newest implementation of a contract. + * @param proxy Proxy to be upgraded. + * @param implementation the address of the Implementation. + */ + function upgrade(AdminUpgradeabilityProxy proxy, address implementation) public onlyOwner { + proxy.upgradeTo(implementation); + } + + /** + * @dev Upgrades a proxy to the newest implementation of a contract and forwards a function call to it. + * This is useful to initialize the proxied contract. + * @param proxy Proxy to be upgraded. + * @param implementation Address of the Implementation. + * @param data Data to send as msg.data in the low level call. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + */ + function upgradeAndCall(AdminUpgradeabilityProxy proxy, address implementation, bytes memory data) payable public onlyOwner { + proxy.upgradeToAndCall{value: msg.value}(implementation, data); + } +} \ No newline at end of file diff --git a/src/controller-v4.sol b/src/controller-v4.sol index 431484476..6e742289d 100644 --- a/src/controller-v4.sol +++ b/src/controller-v4.sol @@ -3,8 +3,6 @@ pragma solidity ^0.6.7; pragma experimental ABIEncoderV2; -import "./interfaces/controller.sol"; - import "./lib/erc20.sol"; import "./lib/safe-math.sol"; @@ -247,11 +245,6 @@ contract ControllerV4 { IStrategy(strategies[_token]).withdraw(_amount); } - function withdrawReward(address _token, uint256 _reward) public { - require(msg.sender == jars[_token], "!jar"); - IStrategy(strategies[_token]).withdrawReward(_reward); - } - // Function to swap between jars function swapExactJarForJar( address _fromJar, // From which Jar diff --git a/src/controller-v5.sol b/src/controller-v5.sol new file mode 100644 index 000000000..2db8f55bd --- /dev/null +++ b/src/controller-v5.sol @@ -0,0 +1,375 @@ +// https://github.com/iearn-finance/jars/blob/master/contracts/controllers/StrategyControllerV1.sol + +pragma solidity ^0.6.7; +pragma experimental ABIEncoderV2; +import "./lib/Initializable.sol"; + +import "./lib/erc20.sol"; +import "./lib/safe-math.sol"; + +import "./interfaces/jar.sol"; +import "./interfaces/jar-converter.sol"; +import "./interfaces/onesplit.sol"; +import "./interfaces/strategy.sol"; +import "./interfaces/converter.sol"; + +contract ControllerV5 is Initializable { + using SafeERC20 for IERC20; + using Address for address; + using SafeMath for uint256; + + address public constant burn = 0x000000000000000000000000000000000000dEaD; + address public onesplit = 0xC586BeF4a0992C495Cf22e1aeEE4E446CECDee0E; + + address public governance; + address public strategist; + address public devfund; + address public treasury; + address public timelock; + + // Convenience fee 0.1% + uint256 public convenienceFee = 100; + uint256 public constant convenienceFeeMax = 100000; + + mapping(address => address) public jars; + mapping(address => address) public strategies; + mapping(address => mapping(address => address)) public converters; + mapping(address => mapping(address => bool)) public approvedStrategies; + mapping(address => bool) public approvedJarConverters; + + uint256 public split = 500; + uint256 public constant max = 10000; + + function initialize( + address _governance, + address _strategist, + address _timelock, + address _devfund, + address _treasury + ) public initializer { + governance = _governance; + strategist = _strategist; + timelock = _timelock; + devfund = _devfund; + treasury = _treasury; + } + + function setDevFund(address _devfund) public { + require(msg.sender == governance, "!governance"); + devfund = _devfund; + } + + function setTreasury(address _treasury) public { + require(msg.sender == governance, "!governance"); + treasury = _treasury; + } + + function setStrategist(address _strategist) public { + require(msg.sender == governance, "!governance"); + strategist = _strategist; + } + + function setSplit(uint256 _split) public { + require(msg.sender == governance, "!governance"); + require(_split <= max, "numerator cannot be greater than denominator"); + split = _split; + } + + function setOneSplit(address _onesplit) public { + require(msg.sender == governance, "!governance"); + onesplit = _onesplit; + } + + function setGovernance(address _governance) public { + require(msg.sender == governance, "!governance"); + governance = _governance; + } + + function setTimelock(address _timelock) public { + require(msg.sender == timelock, "!timelock"); + timelock = _timelock; + } + + function setJar(address _token, address _jar) public { + require( + msg.sender == strategist || msg.sender == governance, + "!strategist" + ); + require(jars[_token] == address(0), "jar"); + jars[_token] = _jar; + } + + function approveJarConverter(address _converter) public { + require(msg.sender == governance, "!governance"); + approvedJarConverters[_converter] = true; + } + + function revokeJarConverter(address _converter) public { + require(msg.sender == governance, "!governance"); + approvedJarConverters[_converter] = false; + } + + function approveStrategy(address _token, address _strategy) public { + require(msg.sender == timelock, "!timelock"); + approvedStrategies[_token][_strategy] = true; + } + + function revokeStrategy(address _token, address _strategy) public { + require(msg.sender == governance, "!governance"); + require(strategies[_token] != _strategy, "cannot revoke active strategy"); + approvedStrategies[_token][_strategy] = false; + } + + function setConvenienceFee(uint256 _convenienceFee) external { + require(msg.sender == timelock, "!timelock"); + convenienceFee = _convenienceFee; + } + + function setStrategy(address _token, address _strategy) public { + require( + msg.sender == strategist || msg.sender == governance, + "!strategist" + ); + require(approvedStrategies[_token][_strategy] == true, "!approved"); + + address _current = strategies[_token]; + if (_current != address(0)) { + IStrategy(_current).withdrawAll(); + } + strategies[_token] = _strategy; + } + + function earn(address _token, uint256 _amount) public { + address _strategy = strategies[_token]; + address _want = IStrategy(_strategy).want(); + if (_want != _token) { + address converter = converters[_token][_want]; + IERC20(_token).safeTransfer(converter, _amount); + _amount = Converter(converter).convert(_strategy); + IERC20(_want).safeTransfer(_strategy, _amount); + } else { + IERC20(_token).safeTransfer(_strategy, _amount); + } + IStrategy(_strategy).deposit(); + } + + function balanceOf(address _token) external view returns (uint256) { + return IStrategy(strategies[_token]).balanceOf(); + } + + function withdrawAll(address _token) public { + require( + msg.sender == strategist || msg.sender == governance, + "!strategist" + ); + IStrategy(strategies[_token]).withdrawAll(); + } + + function inCaseTokensGetStuck(address _token, uint256 _amount) public { + require( + msg.sender == strategist || msg.sender == governance, + "!governance" + ); + IERC20(_token).safeTransfer(msg.sender, _amount); + } + + function inCaseStrategyTokenGetStuck(address _strategy, address _token) + public + { + require( + msg.sender == strategist || msg.sender == governance, + "!governance" + ); + IStrategy(_strategy).withdraw(_token); + } + + function getExpectedReturn( + address _strategy, + address _token, + uint256 parts + ) public view returns (uint256 expected) { + uint256 _balance = IERC20(_token).balanceOf(_strategy); + address _want = IStrategy(_strategy).want(); + (expected, ) = OneSplitAudit(onesplit).getExpectedReturn( + _token, + _want, + _balance, + parts, + 0 + ); + } + + // Only allows to withdraw non-core strategy tokens ~ this is over and above normal yield + function yearn( + address _strategy, + address _token, + uint256 parts + ) public { + require( + msg.sender == strategist || msg.sender == governance, + "!governance" + ); + // This contract should never have value in it, but just incase since this is a public call + uint256 _before = IERC20(_token).balanceOf(address(this)); + IStrategy(_strategy).withdraw(_token); + uint256 _after = IERC20(_token).balanceOf(address(this)); + if (_after > _before) { + uint256 _amount = _after.sub(_before); + address _want = IStrategy(_strategy).want(); + uint256[] memory _distribution; + uint256 _expected; + _before = IERC20(_want).balanceOf(address(this)); + IERC20(_token).safeApprove(onesplit, 0); + IERC20(_token).safeApprove(onesplit, _amount); + (_expected, _distribution) = OneSplitAudit(onesplit) + .getExpectedReturn(_token, _want, _amount, parts, 0); + OneSplitAudit(onesplit).swap( + _token, + _want, + _amount, + _expected, + _distribution, + 0 + ); + _after = IERC20(_want).balanceOf(address(this)); + if (_after > _before) { + _amount = _after.sub(_before); + uint256 _treasury = _amount.mul(split).div(max); + earn(_want, _amount.sub(_treasury)); + IERC20(_want).safeTransfer(treasury, _treasury); + } + } + } + + function withdraw(address _token, uint256 _amount) public { + require(msg.sender == jars[_token], "!jar"); + IStrategy(strategies[_token]).withdraw(_amount); + } + + function withdrawReward(address _token, uint256 _reward) public { + require(msg.sender == jars[_token], "!jar"); + IStrategy(strategies[_token]).withdrawReward(_reward); + } + + // Function to swap between jars + function swapExactJarForJar( + address _fromJar, // From which Jar + address _toJar, // To which Jar + uint256 _fromJarAmount, // How much jar tokens to swap + uint256 _toJarMinAmount, // How much jar tokens you'd like at a minimum + address payable[] calldata _targets, + bytes[] calldata _data + ) external returns (uint256) { + require(_targets.length == _data.length, "!length"); + + // Only return last response + for (uint256 i = 0; i < _targets.length; i++) { + require(_targets[i] != address(0), "!converter"); + require(approvedJarConverters[_targets[i]], "!converter"); + } + + address _fromJarToken = IJar(_fromJar).token(); + address _toJarToken = IJar(_toJar).token(); + + // Get pTokens from msg.sender + IERC20(_fromJar).safeTransferFrom( + msg.sender, + address(this), + _fromJarAmount + ); + + // Calculate how much underlying + // is the amount of pTokens worth + uint256 _fromJarUnderlyingAmount = _fromJarAmount + .mul(IJar(_fromJar).getRatio()) + .div(10**uint256(IJar(_fromJar).decimals())); + + // Call 'withdrawForSwap' on Jar's current strategy if Jar + // doesn't have enough initial capital. + // This has moves the funds from the strategy to the Jar's + // 'earnable' amount. Enabling 'free' withdrawals + uint256 _fromJarAvailUnderlying = IERC20(_fromJarToken).balanceOf( + _fromJar + ); + if (_fromJarAvailUnderlying < _fromJarUnderlyingAmount) { + IStrategy(strategies[_fromJarToken]).withdrawForSwap( + _fromJarUnderlyingAmount.sub(_fromJarAvailUnderlying) + ); + } + + // Withdraw from Jar + // Note: this is free since its still within the "earnable" amount + // as we transferred the access + IERC20(_fromJar).safeApprove(_fromJar, 0); + IERC20(_fromJar).safeApprove(_fromJar, _fromJarAmount); + IJar(_fromJar).withdraw(_fromJarAmount); + + // Calculate fee + uint256 _fromUnderlyingBalance = IERC20(_fromJarToken).balanceOf( + address(this) + ); + uint256 _convenienceFee = _fromUnderlyingBalance.mul(convenienceFee).div( + convenienceFeeMax + ); + + if (_convenienceFee > 1) { + IERC20(_fromJarToken).safeTransfer(devfund, _convenienceFee.div(2)); + IERC20(_fromJarToken).safeTransfer(treasury, _convenienceFee.div(2)); + } + + // Executes sequence of logic + for (uint256 i = 0; i < _targets.length; i++) { + _execute(_targets[i], _data[i]); + } + + // Deposit into new Jar + uint256 _toBal = IERC20(_toJarToken).balanceOf(address(this)); + IERC20(_toJarToken).safeApprove(_toJar, 0); + IERC20(_toJarToken).safeApprove(_toJar, _toBal); + IJar(_toJar).deposit(_toBal); + + // Send Jar Tokens to user + uint256 _toJarBal = IJar(_toJar).balanceOf(address(this)); + if (_toJarBal < _toJarMinAmount) { + revert("!min-jar-amount"); + } + + IJar(_toJar).transfer(msg.sender, _toJarBal); + + return _toJarBal; + } + + function _execute(address _target, bytes memory _data) + internal + returns (bytes memory response) + { + require(_target != address(0), "!target"); + + // call contract in current context + assembly { + let succeeded := delegatecall( + sub(gas(), 5000), + _target, + add(_data, 0x20), + mload(_data), + 0, + 0 + ) + let size := returndatasize() + + response := mload(0x40) + mstore( + 0x40, + add(response, and(add(add(size, 0x20), 0x1f), not(0x1f))) + ) + mstore(response, size) + returndatacopy(add(response, 0x20), 0, size) + + switch iszero(succeeded) + case 1 { + // throw if delegatecall failed + revert(add(response, 0x20), size) + } + } + } +} diff --git a/src/lib/AddressUpgradeable.sol b/src/lib/AddressUpgradeable.sol new file mode 100644 index 000000000..08a59041b --- /dev/null +++ b/src/lib/AddressUpgradeable.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.2 <0.8.0; + +/** + * @dev Collection of functions related to the address type + */ +library AddressUpgradeable { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { size := extcodesize(account) } + return size > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{ value: amount }(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain`call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + return functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.call{ value: value }(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { + return functionStaticCall(target, data, "Address: low-level static call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`], + * but performing a static call. + * + * _Available since v3.3._ + */ + function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) { + require(isContract(target), "Address: static call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.staticcall(data); + return _verifyCallResult(success, returndata, errorMessage); + } + + function _verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) private pure returns(bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} diff --git a/src/lib/Initializable.sol b/src/lib/Initializable.sol new file mode 100644 index 000000000..d4fddcd19 --- /dev/null +++ b/src/lib/Initializable.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +// solhint-disable-next-line compiler-version +pragma solidity >=0.4.24 <0.8.0; + +import "./AddressUpgradeable.sol"; + +/** + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since a proxied contract can't have a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {UpgradeableProxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. + */ +abstract contract Initializable { + + /** + * @dev Indicates that the contract has been initialized. + */ + bool private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Modifier to protect an initializer function from being invoked twice. + */ + modifier initializer() { + require(_initializing || _isConstructor() || !_initialized, "Initializable: contract is already initialized"); + + bool isTopLevelCall = !_initializing; + if (isTopLevelCall) { + _initializing = true; + _initialized = true; + } + + _; + + if (isTopLevelCall) { + _initializing = false; + } + } + + /// @dev Returns true if and only if the function is running in the constructor + function _isConstructor() private view returns (bool) { + return !AddressUpgradeable.isContract(address(this)); + } +} diff --git a/src/pickle-jar-symbiotic.sol b/src/pickle-jar-symbiotic.sol index 5b5129995..76f23d475 100644 --- a/src/pickle-jar-symbiotic.sol +++ b/src/pickle-jar-symbiotic.sol @@ -18,8 +18,9 @@ contract PickleJarSymbiotic is ERC20 { mapping(address => uint256) public userRewardDebt; - uint256 accRewardPerShare; - uint256 lastPendingReward; + uint256 private accRewardPerShare; + uint256 private lastPendingReward; + uint256 private curPendingReward; uint256 public min = 10000; uint256 public constant max = 10000; @@ -94,13 +95,11 @@ contract PickleJarSymbiotic is ERC20 { } function deposit(uint256 _amount) public { - _updateReward(); - if (_amount > 0) { - uint256 _pending = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36).sub(userRewardDebt[msg.sender]); - IController(controller).withdrawReward(address(token), _pending); - reward.safeTransfer(msg.sender, _pending); - lastPendingReward = lastPendingReward.sub(_pending); - } + require(_amount > 0, "Invalid amount"); + _updateAccPerShare(); + + _withdrawReward(); + uint256 _pool = balance(); uint256 _before = token.balanceOf(address(this)); token.safeTransferFrom(msg.sender, address(this), _amount); @@ -118,12 +117,11 @@ contract PickleJarSymbiotic is ERC20 { earn(); //earn everytime deposit happens } - function _updateReward() internal { + function _updateAccPerShare() internal { if (totalSupply() == 0) return; - uint256 curPendingReward = pendingReward(); + curPendingReward = pendingReward(); uint256 addedReward = curPendingReward.sub(lastPendingReward); accRewardPerShare = accRewardPerShare.add((addedReward.mul(1e36)).div(totalSupply())); - lastPendingReward = curPendingReward; } function withdrawAll() external { @@ -141,13 +139,18 @@ contract PickleJarSymbiotic is ERC20 { return IStrategy(IController(controller).strategies(address(token))).pendingReward(); } - function withdraw(uint256 _shares) public { - uint256 _balance = balanceOf(msg.sender); - require(_balance >= _shares, "Invalid amount"); - _updateReward(); - uint256 _pending = _balance.mul(accRewardPerShare).div(1e36).sub(userRewardDebt[msg.sender]); + function _withdrawReward() internal { + uint256 _pending = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36).sub(userRewardDebt[msg.sender]); IController(controller).withdrawReward(address(token), _pending); reward.safeTransfer(msg.sender, _pending); + lastPendingReward = curPendingReward.sub(_pending); + } + + function withdraw(uint256 _shares) public { + require(balanceOf(msg.sender) >= _shares, "Invalid amount"); + _updateAccPerShare(); + _withdrawReward(); + uint256 r = (balance().mul(_shares)).div(totalSupply()); _burn(msg.sender, _shares); uint256 b = token.balanceOf(address(this)); @@ -161,11 +164,9 @@ contract PickleJarSymbiotic is ERC20 { } } token.safeTransfer(msg.sender, r); - _balance = balanceOf(msg.sender); - userRewardDebt[msg.sender] = _balance.mul(accRewardPerShare).div(1e36); + userRewardDebt[msg.sender] = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36); - lastPendingReward = lastPendingReward.sub(_pending); emit Withdraw(msg.sender, r, _shares); } diff --git a/src/strategies/alchemix/strategy-alusd-3crv.sol b/src/strategies/alchemix/strategy-alusd-3crv.sol index 70dc38d0c..b6897952e 100644 --- a/src/strategies/alchemix/strategy-alusd-3crv.sol +++ b/src/strategies/alchemix/strategy-alusd-3crv.sol @@ -57,8 +57,15 @@ contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { require(reward_token == alcx, "Reward token is invalid"); require(_pendingReward >= _amount, "[withdrawReward] Withdraw amount exceed redeemable amount"); + uint256 _alcxHarvestable = getAlcxFarmHarvestable(); + uint256 _harvestable = getHarvestable(); + _balance = IERC20(alcx).balanceOf(address(this)); - if (_balance < _amount) IStakingPools(stakingPool).claim(alcxPoolId); + if (_balance < _amount && _alcxHarvestable > 0) IStakingPools(stakingPool).claim(alcxPoolId); + + _balance = IERC20(alcx).balanceOf(address(this)); + if (_balance < _amount && _harvestable > 0) IStakingPools(stakingPool).claim(poolId); + _balance = IERC20(alcx).balanceOf(address(this)); if (_balance < _amount) { uint256 _r = _amount.sub(_balance); @@ -66,8 +73,6 @@ contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { IStakingPools(stakingPool).withdraw(alcxPoolId, _alcxDeposited >= _r ? _r : _alcxDeposited); } _balance = IERC20(alcx).balanceOf(address(this)); - if (_balance < _amount) IStakingPools(stakingPool).claim(poolId); - _balance = IERC20(alcx).balanceOf(address(this)); require(_balance >= _amount, "[WithdrawReward] Withdraw amount exceed balance"); //double check IERC20(reward_token).safeTransfer(_jar, _amount); __redeposit(); diff --git a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js index 04912e14b..08e08df9a 100644 --- a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js +++ b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js @@ -2,13 +2,13 @@ const hre = require("hardhat"); var chaiAsPromised = require("chai-as-promised"); const StrategyCurveAlusd3Crv = hre.artifacts.require("StrategyCurveAlusd3Crv"); const PickleJarSymbiotic = hre.artifacts.require("PickleJarSymbiotic"); -const ControllerV4 = hre.artifacts.require("ControllerV4"); +const ControllerV5 = hre.artifacts.require("ControllerV5"); +const ProxyAdmin = hre.artifacts.require("ProxyAdmin"); +const AdminUpgradeabilityProxy = hre.artifacts.require("AdminUpgradeabilityProxy"); const {assert} = require("chai").use(chaiAsPromised); const {time} = require("@openzeppelin/test-helpers"); -const ERC20_ABI = require("./abi/ERC20.json"); - const unlockAccount = async (address) => { await hre.network.provider.send("hardhat_impersonateAccount", [address]); return hre.ethers.provider.getSigner(address); @@ -19,7 +19,8 @@ const toWei = (ethAmount) => { }; describe("StrategyCurveAlusd3Crv Unit test", () => { - let strategy, pickleJar, controller; + let strategy, pickleJar; + let proxyAdmin, controller; const want = "0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c"; let alusd_3crv, alusd_3crv_whale; let deployer, alice, bob; @@ -38,7 +39,16 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { strategist = governance; timelock = governance; - controller = await ControllerV4.new(governance, strategist, timelock, devfund, treasury); + proxyAdmin = await ProxyAdmin.new(); + console.log("ProxyAdmin address =====> ", proxyAdmin.address); + const controllerImplement = await ControllerV5.new(); + + console.log("Controller implementation =====> ", controllerImplement.address); + const controllerProxy = await AdminUpgradeabilityProxy.new(controllerImplement.address, proxyAdmin.address, []); + + controller = await hre.ethers.getContractAt("ControllerV5", controllerProxy.address); + + await controller.initialize(governance, strategist, timelock, devfund, treasury); console.log("controller is deployed at =====> ", controller.address); strategy = await StrategyCurveAlusd3Crv.new(governance, strategist, controller.address, timelock); @@ -56,8 +66,8 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { await strategy.setKeepAlcx("2000", {from: governance}); alusd_3crv_whale = await unlockAccount("0xBAF18722C137E725327F1376329d3c99F26f6A60"); - alusd_3crv = await hre.ethers.getContractAt(ERC20_ABI, want); - alcx = await hre.ethers.getContractAt(ERC20_ABI, alcx_addr); + alusd_3crv = await hre.ethers.getContractAt("ERC20", want); + alcx = await hre.ethers.getContractAt("ERC20", alcx_addr); alusd_3crv.connect(alusd_3crv_whale).transfer(alice.address, toWei(2000)); assert.equal((await alusd_3crv.balanceOf(alice.address)).toString(), toWei(2000).toString()); @@ -103,7 +113,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { assert.equal(_alcx_after.gt(_alcx_before), true); - console.log("Pending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); + console.log("\nPending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); await time.increase(60 * 60 * 24 * 3); @@ -126,7 +136,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); assert.equal(_alcx_after.gt(_alcx_before), true); - console.log("Pending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); + console.log("\nPending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); console.log("\n---------------------------John withdraw---------------------------------------\n"); console.log("Reward balance of strategy ====> ", (await alcx.balanceOf(strategy.address)).toString()); @@ -139,6 +149,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { _alcx_after = await alcx.balanceOf(john.address); console.log("John alcx balance after =====> ", (await alcx.balanceOf(john.address)).toString()); assert.equal(_alcx_after.gt(_alcx_before), true); + console.log("\nPending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); console.log("\n---------------------------Alice second withdraw---------------------------------------\n"); _alcx_before = await alcx.balanceOf(alice.address); @@ -150,7 +161,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { console.log("Alice alcx balance after =====> ", _alcx_after.toString()); assert.equal(_alcx_after.gt(_alcx_before), true); - console.log("Pending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); + console.log("\nPending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); }); it("Should withdraw the want correctly", async () => { diff --git a/src/tests/strategies/alchemix/abi/ERC20.json b/src/tests/strategies/alchemix/abi/ERC20.json deleted file mode 100644 index 493c3a563..000000000 --- a/src/tests/strategies/alchemix/abi/ERC20.json +++ /dev/null @@ -1,356 +0,0 @@ -[ - { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, - { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": false, "internalType": "address", "name": "alchemistAddress", "type": "address" }, - { "indexed": false, "internalType": "bool", "name": "isPaused", "type": "bool" } - ], - "name": "Paused", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "previousAdminRole", "type": "bytes32" }, - { "indexed": true, "internalType": "bytes32", "name": "newAdminRole", "type": "bytes32" } - ], - "name": "RoleAdminChanged", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } - ], - "name": "RoleGranted", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "indexed": true, "internalType": "address", "name": "account", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "sender", "type": "address" } - ], - "name": "RoleRevoked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, - { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, - { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } - ], - "name": "Transfer", - "type": "event" - }, - { - "inputs": [], - "name": "ADMIN_ROLE", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "DEFAULT_ADMIN_ROLE", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "SENTINEL_ROLE", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "owner", "type": "address" }, - { "internalType": "address", "name": "spender", "type": "address" } - ], - "name": "allowance", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "spender", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" } - ], - "name": "approve", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "account", "type": "address" }], - "name": "balanceOf", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "", "type": "address" }], - "name": "blacklist", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], - "name": "burn", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "account", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" } - ], - "name": "burnFrom", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "", "type": "address" }], - "name": "ceiling", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "spender", "type": "address" }, - { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } - ], - "name": "decreaseAllowance", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], - "name": "getRoleAdmin", - "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "uint256", "name": "index", "type": "uint256" } - ], - "name": "getRoleMember", - "outputs": [{ "internalType": "address", "name": "", "type": "address" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [{ "internalType": "bytes32", "name": "role", "type": "bytes32" }], - "name": "getRoleMemberCount", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "grantRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "", "type": "address" }], - "name": "hasMinted", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "hasRole", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "spender", "type": "address" }, - { "internalType": "uint256", "name": "addedValue", "type": "uint256" } - ], - "name": "increaseAllowance", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "uint256", "name": "amount", "type": "uint256" }], - "name": "lowerHasMinted", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "_recipient", "type": "address" }, - { "internalType": "uint256", "name": "_amount", "type": "uint256" } - ], - "name": "mint", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "name", - "outputs": [{ "internalType": "string", "name": "", "type": "string" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "_toPause", "type": "address" }, - { "internalType": "bool", "name": "_state", "type": "bool" } - ], - "name": "pauseAlchemist", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "", "type": "address" }], - "name": "paused", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "renounceRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "bytes32", "name": "role", "type": "bytes32" }, - { "internalType": "address", "name": "account", "type": "address" } - ], - "name": "revokeRole", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "_toBlacklist", "type": "address" }], - "name": "setBlacklist", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "_toSetCeiling", "type": "address" }, - { "internalType": "uint256", "name": "_ceiling", "type": "uint256" } - ], - "name": "setCeiling", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "_newSentinel", "type": "address" }], - "name": "setSentinel", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "_toWhitelist", "type": "address" }, - { "internalType": "bool", "name": "_state", "type": "bool" } - ], - "name": "setWhitelist", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "symbol", - "outputs": [{ "internalType": "string", "name": "", "type": "string" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "recipient", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" } - ], - "name": "transfer", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { "internalType": "address", "name": "sender", "type": "address" }, - { "internalType": "address", "name": "recipient", "type": "address" }, - { "internalType": "uint256", "name": "amount", "type": "uint256" } - ], - "name": "transferFrom", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [{ "internalType": "address", "name": "", "type": "address" }], - "name": "whiteList", - "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], - "stateMutability": "view", - "type": "function" - } -] diff --git a/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol b/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol index 21797f80d..605e3165e 100644 --- a/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol +++ b/src/tests/strategies/alchemix/strategy-sushi-eth-alcx-lp.test.sol @@ -7,24 +7,10 @@ import "../../../interfaces/uniswapv2.sol"; import "../../../pickle-jar.sol"; import "../../../controller-v4.sol"; import "../../../lib/erc20.sol"; -import "../../lib/test-sushi-base.sol"; +import "../../lib/test-strategy-sushi-farm-base.sol"; import "../../../strategies/alchemix/strategy-sushi-eth-alcx-lp.sol"; -contract StrategySushiEthAlcxLpTest is DSTestSushiBase { - address want; - address token1; - - address governance; - address strategist; - address timelock; - - address devfund; - address treasury; - - PickleJar pickleJar; - ControllerV4 controller; - IStrategy strategy; - +contract StrategySushiEthAlcxLpTest is StrategySushiFarmTestBase { function setUp() public { want = 0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8; token1 = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; @@ -67,96 +53,19 @@ contract StrategySushiEthAlcxLpTest is DSTestSushiBase { // Set time hevm.warp(startTime); - - uint256 decimals = ERC20(token1).decimals(); - _getWant(100 ether, 50 * (10**decimals)); - } - - function _getWant(uint256 ethAmount, uint256 amount) internal { - _getERC20(token1, amount); - - uint256 _token1 = IERC20(token1).balanceOf(address(this)); - - IERC20(token1).safeApprove(address(sushiRouter), 0); - IERC20(token1).safeApprove(address(sushiRouter), _token1); - - sushiRouter.addLiquidityETH{value: ethAmount}( - token1, - _token1, - 0, - 0, - address(this), - now + 60 - ); } // **** Tests **** function test_ethalcxv1_timelock() public { - assertTrue(strategy.timelock() == timelock); - strategy.setTimelock(address(1)); - assertTrue(strategy.timelock() == address(1)); + _test_timelock(); } function test_ethalcxv1_withdraw_release() public { - uint256 _want = IERC20(want).balanceOf(address(this)); - IERC20(want).safeApprove(address(pickleJar), 0); - IERC20(want).safeApprove(address(pickleJar), _want); - pickleJar.deposit(_want); - pickleJar.earn(); - hevm.roll(block.number + 1000); - strategy.harvest(); - - // Checking withdraw - uint256 _before = IERC20(want).balanceOf(address(pickleJar)); - controller.withdrawAll(want); - uint256 _after = IERC20(want).balanceOf(address(pickleJar)); - assertTrue(_after > _before); - _before = IERC20(want).balanceOf(address(this)); - pickleJar.withdrawAll(); - _after = IERC20(want).balanceOf(address(this)); - assertTrue(_after > _before); - - // Check if we gained interest - assertTrue(_after > _want); + _test_withdraw_release(); } function test_ethalcxv1_get_earn_harvest_rewards() public { - uint256 _want = IERC20(want).balanceOf(address(this)); - IERC20(want).safeApprove(address(pickleJar), 0); - IERC20(want).safeApprove(address(pickleJar), _want); - pickleJar.deposit(_want); - pickleJar.earn(); - hevm.roll(block.number + 1000); - - // Call the harvest function - uint256 _before = pickleJar.balance(); - uint256 _treasuryBefore = IERC20(want).balanceOf(treasury); - strategy.harvest(); - uint256 _after = pickleJar.balance(); - uint256 _treasuryAfter = IERC20(want).balanceOf(treasury); - - uint256 earned = _after.sub(_before).mul(1000).div(800); - uint256 earnedRewards = earned.mul(200).div(1000); // 20% - uint256 actualRewardsEarned = _treasuryAfter.sub(_treasuryBefore); - - // 20% performance fee is given - assertEqApprox(earnedRewards, actualRewardsEarned); - - // Withdraw - uint256 _devBefore = IERC20(want).balanceOf(devfund); - _treasuryBefore = IERC20(want).balanceOf(treasury); - uint256 _stratBal = strategy.balanceOf(); - pickleJar.withdrawAll(); - uint256 _devAfter = IERC20(want).balanceOf(devfund); - _treasuryAfter = IERC20(want).balanceOf(treasury); - - // 0% goes to dev - uint256 _devFund = _devAfter.sub(_devBefore); - assertEq(_devFund, 0); - - // 0% goes to treasury - uint256 _treasuryFund = _treasuryAfter.sub(_treasuryBefore); - assertEq(_treasuryFund, 0); + _test_get_earn_harvest_rewards(); } } From fa2dd5b828f2206ccf0e8e7d13eff8eec9b688cc Mon Sep 17 00:00:00 2001 From: Abiencode Date: Wed, 28 Apr 2021 18:08:56 -0700 Subject: [PATCH 08/19] Fix approve issue and update the alusd3crv strategy --- .../alchemix/strategy-alcx-farm-base.sol | 134 ++++++++++++++++++ .../strategy-alcx-farm-symbiotic.sol} | 7 +- .../alchemix/strategy-alusd-3crv.sol | 8 +- .../alchemix/strategy-sushi-eth-alcx-lp.sol | 64 +-------- .../lib/test-strategy-sushi-farm-base.sol | 4 + 5 files changed, 150 insertions(+), 67 deletions(-) create mode 100644 src/strategies/alchemix/strategy-alcx-farm-base.sol rename src/strategies/{strategy-alcx-farm-base.sol => alchemix/strategy-alcx-farm-symbiotic.sol} (90%) diff --git a/src/strategies/alchemix/strategy-alcx-farm-base.sol b/src/strategies/alchemix/strategy-alcx-farm-base.sol new file mode 100644 index 000000000..224d43364 --- /dev/null +++ b/src/strategies/alchemix/strategy-alcx-farm-base.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +import "../strategy-base.sol"; +import "../../interfaces/alcx-farm.sol"; + +abstract contract StrategyAlcxFarmBase is StrategyBase { + // Token addresses + address public constant alcx = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; + address public constant stakingPool = 0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa; + + // How much ALCX tokens to keep? + uint256 public keepAlcx = 0; + uint256 public constant keepAlcxMax = 10000; + + uint256 public poolId; + + constructor( + uint256 _poolId, + address _lp, + address _governance, + address _strategist, + address _controller, + address _timelock + ) + public + StrategyBase( + _lp, + _governance, + _strategist, + _controller, + _timelock + ) + { + poolId = _poolId; + } + + + function balanceOfPool() public view override returns (uint256) { + uint256 amount = IStakingPools(stakingPool).getStakeTotalDeposited(address(this), poolId); + return amount; + } + + function getHarvestable() public view returns (uint256) { + return IStakingPools(stakingPool).getStakeTotalUnclaimed(address(this), poolId); + } + + // **** Setters **** + + function deposit() public override { + uint256 _want = IERC20(want).balanceOf(address(this)); + if (_want > 0) { + IERC20(want).safeApprove(stakingPool, 0); + IERC20(want).safeApprove(stakingPool, _want); + IStakingPools(stakingPool).deposit(poolId, _want); + } + } + + + function _withdrawSome(uint256 _amount) internal override returns (uint256) { + IStakingPools(stakingPool).withdraw(poolId, _amount); + return _amount; + } + // **** Setters **** + + function setKeepAlcx(uint256 _keepAlcx) external { + require(msg.sender == timelock, "!timelock"); + keepAlcx = _keepAlcx; + } + // **** State Mutations **** + + function harvest() public override onlyBenevolent { + // Anyone can harvest it at any given time. + // I understand the possibility of being frontrun + // But ETH is a dark forest, and I wanna see how this plays out + // i.e. will be be heavily frontrunned? + // if so, a new strategy will be deployed. + + // Collects ALCX tokens + IStakingPools(stakingPool).claim(poolId); + uint256 _alcx = IERC20(alcx).balanceOf(address(this)); + if (_alcx > 0) { + // 10% is locked up for future gov + uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); + IERC20(alcx).safeTransfer( + IController(controller).treasury(), + _keepAlcx + ); + uint256 _amount = (_alcx.sub(_keepAlcx)).div(2); + + if (_amount > 0) { + IERC20(alcx).safeApprove(sushiRouter, 0); + IERC20(alcx).safeApprove(sushiRouter, _amount); + _swapSushiswap(alcx, weth, _amount); + } + } + + // Adds in liquidity for WETH/ALCX + uint256 _weth = IERC20(weth).balanceOf(address(this)); + + _alcx = IERC20(alcx).balanceOf(address(this)); + + if (_weth > 0 && _alcx > 0) { + IERC20(weth).safeApprove(sushiRouter, 0); + IERC20(weth).safeApprove(sushiRouter, _weth); + + IERC20(alcx).safeApprove(sushiRouter, 0); + IERC20(alcx).safeApprove(sushiRouter, _alcx); + + UniswapRouterV2(sushiRouter).addLiquidity( + weth, + alcx, + _weth, + _alcx, + 0, + 0, + address(this), + now + 60 + ); + + // Donates DUST + IERC20(weth).transfer( + IController(controller).treasury(), + IERC20(weth).balanceOf(address(this)) + ); + IERC20(alcx).safeTransfer( + IController(controller).treasury(), + IERC20(alcx).balanceOf(address(this)) + ); + } + + _distributePerformanceFeesAndDeposit(); + } +} diff --git a/src/strategies/strategy-alcx-farm-base.sol b/src/strategies/alchemix/strategy-alcx-farm-symbiotic.sol similarity index 90% rename from src/strategies/strategy-alcx-farm-base.sol rename to src/strategies/alchemix/strategy-alcx-farm-symbiotic.sol index 3ac41d95a..a511b28ee 100644 --- a/src/strategies/strategy-alcx-farm-base.sol +++ b/src/strategies/alchemix/strategy-alcx-farm-symbiotic.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.6.7; -import "./strategy-base-symbiotic.sol"; -import "../interfaces/alcx-farm.sol"; +import "../strategy-base-symbiotic.sol"; +import "../../interfaces/alcx-farm.sol"; -abstract contract StrategyAlchemixFarmBase is StrategyBaseSymbiotic { +abstract contract StrategyAlcxSymbioticFarmBase is StrategyBaseSymbiotic { // Token addresses address public constant alcx = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; address public constant stakingPool = 0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa; @@ -55,6 +55,7 @@ abstract contract StrategyAlchemixFarmBase is StrategyBaseSymbiotic { function __redeposit() internal override { uint256 _balance = IERC20(alcx).balanceOf(address(this)); + IERC20(alcx).safeApprove(stakingPool, _balance); if (_balance > 0) IStakingPools(stakingPool).deposit(alcxPoolId, _balance); //stake to alcx farm } diff --git a/src/strategies/alchemix/strategy-alusd-3crv.sol b/src/strategies/alchemix/strategy-alusd-3crv.sol index b6897952e..9ce4964a9 100644 --- a/src/strategies/alchemix/strategy-alusd-3crv.sol +++ b/src/strategies/alchemix/strategy-alusd-3crv.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.6.7; -import "../strategy-alcx-farm-base.sol"; +import "./strategy-alcx-farm-symbiotic.sol"; -contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { +contract StrategyAlusd3Crv is StrategyAlcxSymbioticFarmBase { uint256 public alusd_3crv_poolId = 4; address public alusd_3crv = 0x43b4FdFD4Ff969587185cDB6f0BD875c5Fc83f8c; @@ -13,14 +13,14 @@ contract StrategyCurveAlusd3Crv is StrategyAlchemixFarmBase { address _strategist, address _controller, address _timelock - ) public StrategyAlchemixFarmBase(alusd_3crv_poolId, alusd_3crv, _governance, _strategist, _controller, _timelock) { + ) public StrategyAlcxSymbioticFarmBase(alusd_3crv_poolId, alusd_3crv, _governance, _strategist, _controller, _timelock) { IERC20(alcx).approve(stakingPool, uint256(-1)); } // **** Views **** function getName() external pure override returns (string memory) { - return "StrategyCurveAlusd3Crv"; + return "StrategyAlusd3Crv"; } function getAlcxFarmHarvestable() public view returns (uint256) { diff --git a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol index fa2c5b8ef..57510faa2 100644 --- a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol +++ b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.6.7; -import "../strategy-alcx-farm-base.sol"; +import "./strategy-alcx-farm-base.sol"; -contract StrategySushiEthAlcxLp is StrategyAlchemixFarmBase { +contract StrategySushiEthAlcxLp is StrategyAlcxFarmBase { uint256 public sushi_alcx_poolId = 2; @@ -16,7 +16,7 @@ contract StrategySushiEthAlcxLp is StrategyAlchemixFarmBase { address _timelock ) public - StrategyAlchemixFarmBase( + StrategyAlcxFarmBase( sushi_alcx_poolId, sushi_eth_alcx_lp, _governance, @@ -24,67 +24,11 @@ contract StrategySushiEthAlcxLp is StrategyAlchemixFarmBase { _controller, _timelock ) - { - IERC20(alcx).approve(sushiRouter, uint(-1)); - } + {} // **** Views **** function getName() external override pure returns (string memory) { return "StrategySushiEthAlcxLp"; } - - // **** State Mutations **** - - function harvest() public override onlyBenevolent { - // Collects Alcx tokens - IStakingPools(stakingPool).claim(poolId); - uint256 _alcx = IERC20(alcx).balanceOf(address(this)); - if (_alcx > 0) { - // 10% is locked up for future gov - uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); - IERC20(alcx).safeTransfer( - IController(controller).treasury(), - _keepAlcx - ); - uint256 _amount = _alcx.sub(_keepAlcx); - _swapSushiswap(alcx, weth, _amount.div(2)); - } - - // Adds in liquidity for WETH/ALCX - uint256 _weth = IERC20(weth).balanceOf(address(this)); - - _alcx = IERC20(alcx).balanceOf(address(this)); - - if (_weth > 0 && _alcx > 0) { - IERC20(weth).safeApprove(sushiRouter, 0); - IERC20(weth).safeApprove(sushiRouter, _weth); - - IERC20(alcx).safeApprove(sushiRouter, 0); - IERC20(alcx).safeApprove(sushiRouter, _alcx); - - UniswapRouterV2(sushiRouter).addLiquidity( - weth, - alcx, - _weth, - _alcx, - 0, - 0, - address(this), - now + 60 - ); - - // Donates DUST - IERC20(weth).transfer( - IController(controller).treasury(), - IERC20(weth).balanceOf(address(this)) - ); - IERC20(alcx).safeTransfer( - IController(controller).treasury(), - IERC20(alcx).balanceOf(address(this)) - ); - } - - _distributePerformanceFeesAndDeposit(); - } } diff --git a/src/tests/lib/test-strategy-sushi-farm-base.sol b/src/tests/lib/test-strategy-sushi-farm-base.sol index 27c52e63b..b4750880e 100644 --- a/src/tests/lib/test-strategy-sushi-farm-base.sol +++ b/src/tests/lib/test-strategy-sushi-farm-base.sol @@ -91,6 +91,10 @@ contract StrategySushiFarmTestBase is DSTestSushiBase { // Call the harvest function uint256 _before = pickleJar.balance(); uint256 _treasuryBefore = IERC20(want).balanceOf(treasury); + strategy.harvest(); + + hevm.roll(block.number + 1000); + strategy.harvest(); uint256 _after = pickleJar.balance(); uint256 _treasuryAfter = IERC20(want).balanceOf(treasury); From 6468a141777778c903cb51669bd99793dca304a1 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Wed, 28 Apr 2021 18:21:11 -0700 Subject: [PATCH 09/19] minor fix --- src/strategies/alchemix/strategy-alcx-farm-base.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/strategies/alchemix/strategy-alcx-farm-base.sol b/src/strategies/alchemix/strategy-alcx-farm-base.sol index 224d43364..0b124d161 100644 --- a/src/strategies/alchemix/strategy-alcx-farm-base.sol +++ b/src/strategies/alchemix/strategy-alcx-farm-base.sol @@ -82,10 +82,12 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { if (_alcx > 0) { // 10% is locked up for future gov uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); - IERC20(alcx).safeTransfer( - IController(controller).treasury(), - _keepAlcx - ); + if (_keepAlcx > 0) { + IERC20(alcx).safeApprove(IController(controller).treasury(), 0); + IERC20(alcx).safeApprove(IController(controller).treasury(), _keepAlcx); + + IERC20(alcx).safeTransfer(IController(controller).treasury(), _keepAlcx); + } uint256 _amount = (_alcx.sub(_keepAlcx)).div(2); if (_amount > 0) { From 76e9fbef8e488adb7882e6f0d7ad9f90b1bbe187 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Wed, 28 Apr 2021 18:28:54 -0700 Subject: [PATCH 10/19] revert --- src/strategies/alchemix/strategy-alcx-farm-base.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/strategies/alchemix/strategy-alcx-farm-base.sol b/src/strategies/alchemix/strategy-alcx-farm-base.sol index 0b124d161..3cd78ddbd 100644 --- a/src/strategies/alchemix/strategy-alcx-farm-base.sol +++ b/src/strategies/alchemix/strategy-alcx-farm-base.sol @@ -82,12 +82,8 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { if (_alcx > 0) { // 10% is locked up for future gov uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); - if (_keepAlcx > 0) { - IERC20(alcx).safeApprove(IController(controller).treasury(), 0); - IERC20(alcx).safeApprove(IController(controller).treasury(), _keepAlcx); - - IERC20(alcx).safeTransfer(IController(controller).treasury(), _keepAlcx); - } + IERC20(alcx).safeTransfer(IController(controller).treasury(), _keepAlcx); + uint256 _amount = (_alcx.sub(_keepAlcx)).div(2); if (_amount > 0) { From cc968d36f73686f6e6d2fab6318280b870b6aaa0 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Mon, 3 May 2021 03:08:29 -0700 Subject: [PATCH 11/19] Fix withdrawAll function --- src/pickle-jar-symbiotic.sol | 15 ++++++++++++--- .../alchemix/strategy-alcx-farm-symbiotic.sol | 15 ++++++++++----- src/strategies/alchemix/strategy-alusd-3crv.sol | 9 +++++---- src/strategies/strategy-base-symbiotic.sol | 15 +++++++++++++++ .../alchemix/StrategyAlusd3crv.test.js | 16 ++++++++++++++-- 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/pickle-jar-symbiotic.sol b/src/pickle-jar-symbiotic.sol index 76f23d475..c6f88d349 100644 --- a/src/pickle-jar-symbiotic.sol +++ b/src/pickle-jar-symbiotic.sol @@ -135,13 +135,22 @@ contract PickleJarSymbiotic is ERC20 { IERC20(reserve).safeTransfer(controller, amount); } - function pendingReward() public returns (uint256) { - return IStrategy(IController(controller).strategies(address(token))).pendingReward(); + function pendingReward() public view returns (uint256) { + return reward.balanceOf(address(this)).add(IStrategy(IController(controller).strategies(address(token))).pendingReward()); } function _withdrawReward() internal { uint256 _pending = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36).sub(userRewardDebt[msg.sender]); - IController(controller).withdrawReward(address(token), _pending); + uint256 _balance = reward.balanceOf(address(this)); + if (_balance < _pending) { + uint256 _withdraw = _pending.sub(_balance); + IController(controller).withdrawReward(address(token), _withdraw); + uint256 _after = reward.balanceOf(address(this)); + uint256 _diff = _after.sub(_balance); + if (_diff < _withdraw) { + _pending = _balance.add(_diff); + } + } reward.safeTransfer(msg.sender, _pending); lastPendingReward = curPendingReward.sub(_pending); } diff --git a/src/strategies/alchemix/strategy-alcx-farm-symbiotic.sol b/src/strategies/alchemix/strategy-alcx-farm-symbiotic.sol index a511b28ee..f78531175 100644 --- a/src/strategies/alchemix/strategy-alcx-farm-symbiotic.sol +++ b/src/strategies/alchemix/strategy-alcx-farm-symbiotic.sol @@ -5,9 +5,6 @@ import "../strategy-base-symbiotic.sol"; import "../../interfaces/alcx-farm.sol"; abstract contract StrategyAlcxSymbioticFarmBase is StrategyBaseSymbiotic { - // Token addresses - address public constant alcx = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; - address public constant stakingPool = 0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa; // How much Alcx tokens to keep? uint256 public keepAlcx = 0; @@ -53,10 +50,18 @@ abstract contract StrategyAlcxSymbioticFarmBase is StrategyBaseSymbiotic { return _amount; } + function _withdrawSomeReward(uint256 _amount) internal override returns (uint256) { + IStakingPools(stakingPool).withdraw(alcxPoolId, _amount); + return _amount; + } + function __redeposit() internal override { uint256 _balance = IERC20(alcx).balanceOf(address(this)); - IERC20(alcx).safeApprove(stakingPool, _balance); - if (_balance > 0) IStakingPools(stakingPool).deposit(alcxPoolId, _balance); //stake to alcx farm + if (_balance > 0) { + IERC20(alcx).safeApprove(stakingPool, 0); + IERC20(alcx).safeApprove(stakingPool, _balance); + IStakingPools(stakingPool).deposit(alcxPoolId, _balance); //stake to alcx farm + } } // **** Setters **** diff --git a/src/strategies/alchemix/strategy-alusd-3crv.sol b/src/strategies/alchemix/strategy-alusd-3crv.sol index 9ce4964a9..c3e5b46a3 100644 --- a/src/strategies/alchemix/strategy-alusd-3crv.sol +++ b/src/strategies/alchemix/strategy-alusd-3crv.sol @@ -13,9 +13,7 @@ contract StrategyAlusd3Crv is StrategyAlcxSymbioticFarmBase { address _strategist, address _controller, address _timelock - ) public StrategyAlcxSymbioticFarmBase(alusd_3crv_poolId, alusd_3crv, _governance, _strategist, _controller, _timelock) { - IERC20(alcx).approve(stakingPool, uint256(-1)); - } + ) public StrategyAlcxSymbioticFarmBase(alusd_3crv_poolId, alusd_3crv, _governance, _strategist, _controller, _timelock) {} // **** Views **** @@ -43,6 +41,9 @@ contract StrategyAlusd3Crv is StrategyAlcxSymbioticFarmBase { uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); IERC20(alcx).safeTransfer(IController(controller).treasury(), _keepAlcx); uint256 _amount = _alcx.sub(_keepAlcx); + + IERC20(alcx).safeApprove(stakingPool, 0); + IERC20(alcx).safeApprove(stakingPool, _amount); IStakingPools(stakingPool).deposit(alcxPoolId, _amount); //stake to alcx farm } } @@ -78,7 +79,7 @@ contract StrategyAlusd3Crv is StrategyAlcxSymbioticFarmBase { __redeposit(); } - function getAlcxDeposited() public view returns (uint256) { + function getAlcxDeposited() public view override returns (uint256) { return IStakingPools(stakingPool).getStakeTotalDeposited(address(this), alcxPoolId); } diff --git a/src/strategies/strategy-base-symbiotic.sol b/src/strategies/strategy-base-symbiotic.sol index 4fd3d6e58..61bc5b4c1 100644 --- a/src/strategies/strategy-base-symbiotic.sol +++ b/src/strategies/strategy-base-symbiotic.sol @@ -46,6 +46,10 @@ abstract contract StrategyBaseSymbiotic { address public univ2Router2 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; address public sushiRouter = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; + // Token addresses + address public constant alcx = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; + address public constant stakingPool = 0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa; + mapping(address => bool) public harvesters; constructor( @@ -83,6 +87,8 @@ abstract contract StrategyBaseSymbiotic { function balanceOfPool() public view virtual returns (uint256); + function getAlcxDeposited() public view virtual returns (uint256); + function balanceOf() public view returns (uint256) { return balanceOfWant().add(balanceOfPool()); } @@ -193,20 +199,29 @@ abstract contract StrategyBaseSymbiotic { function withdrawAll() external returns (uint256 balance) { require(msg.sender == controller, "!controller"); _withdrawAll(); + _withdrawAllReward(); balance = IERC20(want).balanceOf(address(this)); address _jar = IController(controller).jars(address(want)); require(_jar != address(0), "!jar"); // additional protection so we don't burn the funds IERC20(want).safeTransfer(_jar, balance); + + IERC20(alcx).safeTransfer(_jar, IERC20(alcx).balanceOf(address(this))); } function _withdrawAll() internal { _withdrawSome(balanceOfPool()); } + function _withdrawAllReward() internal { + _withdrawSomeReward(getAlcxDeposited()); + } + function _withdrawSome(uint256 _amount) internal virtual returns (uint256); + function _withdrawSomeReward(uint256 _amount) internal virtual returns (uint256); + function harvest() public virtual; // **** Emergency functions **** diff --git a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js index 08e08df9a..97d902133 100644 --- a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js +++ b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js @@ -1,6 +1,6 @@ const hre = require("hardhat"); var chaiAsPromised = require("chai-as-promised"); -const StrategyCurveAlusd3Crv = hre.artifacts.require("StrategyCurveAlusd3Crv"); +const StrategyCurveAlusd3Crv = hre.artifacts.require("StrategyAlusd3Crv"); const PickleJarSymbiotic = hre.artifacts.require("PickleJarSymbiotic"); const ControllerV5 = hre.artifacts.require("ControllerV5"); const ProxyAdmin = hre.artifacts.require("ProxyAdmin"); @@ -27,6 +27,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { let alcx, alcx_addr = "0xdbdb4d16eda451d0503b854cf79d55697f90c8df"; let governance, strategist, devfund, treasury, timelock; + let preTestSnapshotID; before("Deploy contracts", async () => { [governance, devfund, treasury] = await web3.eth.getAccounts(); @@ -65,7 +66,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { await strategy.setKeepAlcx("2000", {from: governance}); - alusd_3crv_whale = await unlockAccount("0xBAF18722C137E725327F1376329d3c99F26f6A60"); + alusd_3crv_whale = await unlockAccount("0x8e47eE0D580a86a9A7D0e155Cb497E926ad0eC96"); alusd_3crv = await hre.ethers.getContractAt("ERC20", want); alcx = await hre.ethers.getContractAt("ERC20", alcx_addr); @@ -77,6 +78,14 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { assert.equal((await alusd_3crv.balanceOf(john.address)).toString(), toWei(2500).toString()); }); + beforeEach(async () => { + preTestSnapshotID = await hre.network.provider.send("evm_snapshot"); + }); + + afterEach(async () => { + await hre.network.provider.send("evm_revert", [preTestSnapshotID]); + }); + it("Should harvest the reward correctly", async () => { console.log("\n---------------------------Alice deposit---------------------------------------\n"); await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(2000)); @@ -162,6 +171,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { assert.equal(_alcx_after.gt(_alcx_before), true); console.log("\nPending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); + console.log("\nPickle Jar Pending reward after all withdrawal ====> ", (await pickleJar.pendingReward()).toString()); }); it("Should withdraw the want correctly", async () => { @@ -210,6 +220,8 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { _alcx_after = await alcx.balanceOf(bob.address); console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); assert.equal(_alcx_after.gt(_alcx_before), true); + console.log("\nStrategy Pending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); + console.log("\nPickle Jar Pending reward after all withdrawal ====> ", (await pickleJar.pendingReward()).toString()); }); const harvest = async () => { From 53554bcf78b0460db9a21d76a11f24a93896a280 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Mon, 3 May 2021 09:45:21 -0700 Subject: [PATCH 12/19] just in case --- src/pickle-jar-symbiotic.sol | 8 +++++--- src/strategies/alchemix/strategy-alusd-3crv.sol | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pickle-jar-symbiotic.sol b/src/pickle-jar-symbiotic.sol index c6f88d349..a24714d36 100644 --- a/src/pickle-jar-symbiotic.sol +++ b/src/pickle-jar-symbiotic.sol @@ -18,9 +18,9 @@ contract PickleJarSymbiotic is ERC20 { mapping(address => uint256) public userRewardDebt; - uint256 private accRewardPerShare; - uint256 private lastPendingReward; - uint256 private curPendingReward; + uint256 public accRewardPerShare; + uint256 public lastPendingReward; + uint256 public curPendingReward; uint256 public min = 10000; uint256 public constant max = 10000; @@ -120,6 +120,8 @@ contract PickleJarSymbiotic is ERC20 { function _updateAccPerShare() internal { if (totalSupply() == 0) return; curPendingReward = pendingReward(); + require(curPendingReward >= lastPendingReward, "Alchemix protocol failed"); + uint256 addedReward = curPendingReward.sub(lastPendingReward); accRewardPerShare = accRewardPerShare.add((addedReward.mul(1e36)).div(totalSupply())); } diff --git a/src/strategies/alchemix/strategy-alusd-3crv.sol b/src/strategies/alchemix/strategy-alusd-3crv.sol index c3e5b46a3..c593541cd 100644 --- a/src/strategies/alchemix/strategy-alusd-3crv.sol +++ b/src/strategies/alchemix/strategy-alusd-3crv.sol @@ -85,8 +85,8 @@ contract StrategyAlusd3Crv is StrategyAlcxSymbioticFarmBase { function pendingReward() public view returns (uint256) { return - IStakingPools(stakingPool).getStakeTotalDeposited(address(this), alcxPoolId).add( - getHarvestable().add(getAlcxFarmHarvestable()) + IERC20(alcx).balanceOf(address(this)).add(IStakingPools(stakingPool).getStakeTotalDeposited(address(this), alcxPoolId).add( + getHarvestable().add(getAlcxFarmHarvestable())) ); } } From f6ed833556ca044ec07765ee678e093e2bc6610e Mon Sep 17 00:00:00 2001 From: Abiencode Date: Sun, 9 May 2021 18:04:56 -0700 Subject: [PATCH 13/19] Remove swapExactJarAndJar --- src/controller-v5.sol | 88 ------------------------------------------- 1 file changed, 88 deletions(-) diff --git a/src/controller-v5.sol b/src/controller-v5.sol index 2db8f55bd..2d937cd55 100644 --- a/src/controller-v5.sol +++ b/src/controller-v5.sol @@ -251,94 +251,6 @@ contract ControllerV5 is Initializable { IStrategy(strategies[_token]).withdrawReward(_reward); } - // Function to swap between jars - function swapExactJarForJar( - address _fromJar, // From which Jar - address _toJar, // To which Jar - uint256 _fromJarAmount, // How much jar tokens to swap - uint256 _toJarMinAmount, // How much jar tokens you'd like at a minimum - address payable[] calldata _targets, - bytes[] calldata _data - ) external returns (uint256) { - require(_targets.length == _data.length, "!length"); - - // Only return last response - for (uint256 i = 0; i < _targets.length; i++) { - require(_targets[i] != address(0), "!converter"); - require(approvedJarConverters[_targets[i]], "!converter"); - } - - address _fromJarToken = IJar(_fromJar).token(); - address _toJarToken = IJar(_toJar).token(); - - // Get pTokens from msg.sender - IERC20(_fromJar).safeTransferFrom( - msg.sender, - address(this), - _fromJarAmount - ); - - // Calculate how much underlying - // is the amount of pTokens worth - uint256 _fromJarUnderlyingAmount = _fromJarAmount - .mul(IJar(_fromJar).getRatio()) - .div(10**uint256(IJar(_fromJar).decimals())); - - // Call 'withdrawForSwap' on Jar's current strategy if Jar - // doesn't have enough initial capital. - // This has moves the funds from the strategy to the Jar's - // 'earnable' amount. Enabling 'free' withdrawals - uint256 _fromJarAvailUnderlying = IERC20(_fromJarToken).balanceOf( - _fromJar - ); - if (_fromJarAvailUnderlying < _fromJarUnderlyingAmount) { - IStrategy(strategies[_fromJarToken]).withdrawForSwap( - _fromJarUnderlyingAmount.sub(_fromJarAvailUnderlying) - ); - } - - // Withdraw from Jar - // Note: this is free since its still within the "earnable" amount - // as we transferred the access - IERC20(_fromJar).safeApprove(_fromJar, 0); - IERC20(_fromJar).safeApprove(_fromJar, _fromJarAmount); - IJar(_fromJar).withdraw(_fromJarAmount); - - // Calculate fee - uint256 _fromUnderlyingBalance = IERC20(_fromJarToken).balanceOf( - address(this) - ); - uint256 _convenienceFee = _fromUnderlyingBalance.mul(convenienceFee).div( - convenienceFeeMax - ); - - if (_convenienceFee > 1) { - IERC20(_fromJarToken).safeTransfer(devfund, _convenienceFee.div(2)); - IERC20(_fromJarToken).safeTransfer(treasury, _convenienceFee.div(2)); - } - - // Executes sequence of logic - for (uint256 i = 0; i < _targets.length; i++) { - _execute(_targets[i], _data[i]); - } - - // Deposit into new Jar - uint256 _toBal = IERC20(_toJarToken).balanceOf(address(this)); - IERC20(_toJarToken).safeApprove(_toJar, 0); - IERC20(_toJarToken).safeApprove(_toJar, _toBal); - IJar(_toJar).deposit(_toBal); - - // Send Jar Tokens to user - uint256 _toJarBal = IJar(_toJar).balanceOf(address(this)); - if (_toJarBal < _toJarMinAmount) { - revert("!min-jar-amount"); - } - - IJar(_toJar).transfer(msg.sender, _toJarBal); - - return _toJarBal; - } - function _execute(address _target, bytes memory _data) internal returns (bytes memory response) From 949e2c45cfffca6063b2e7b7d023bbfdb1634517 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Tue, 11 May 2021 11:32:04 -0700 Subject: [PATCH 14/19] Fix hardhat unit test and remove earn function in the symbiotic jar --- src/pickle-jar-symbiotic.sol | 1 - .../alchemix/StrategyAlusd3crv.test.js | 52 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/pickle-jar-symbiotic.sol b/src/pickle-jar-symbiotic.sol index a24714d36..d0a7928f7 100644 --- a/src/pickle-jar-symbiotic.sol +++ b/src/pickle-jar-symbiotic.sol @@ -114,7 +114,6 @@ contract PickleJarSymbiotic is ERC20 { _mint(msg.sender, shares); userRewardDebt[msg.sender] = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36); emit Deposit(msg.sender, _amount, shares); - earn(); //earn everytime deposit happens } function _updateAccPerShare() internal { diff --git a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js index 97d902133..92939c15d 100644 --- a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js +++ b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js @@ -64,18 +64,20 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { }); await controller.setStrategy(want, strategy.address, {from: governance}); - await strategy.setKeepAlcx("2000", {from: governance}); - - alusd_3crv_whale = await unlockAccount("0x8e47eE0D580a86a9A7D0e155Cb497E926ad0eC96"); + await strategy.setKeepAlcx("2000", { from: governance }); + + const whaleAddr = "0xbFeb87721f0076e6F8c4EC2DaBdc9E2F18472E7b"; + + alusd_3crv_whale = await unlockAccount(whaleAddr); alusd_3crv = await hre.ethers.getContractAt("ERC20", want); alcx = await hre.ethers.getContractAt("ERC20", alcx_addr); - alusd_3crv.connect(alusd_3crv_whale).transfer(alice.address, toWei(2000)); - assert.equal((await alusd_3crv.balanceOf(alice.address)).toString(), toWei(2000).toString()); - alusd_3crv.connect(alusd_3crv_whale).transfer(bob.address, toWei(1000)); - assert.equal((await alusd_3crv.balanceOf(bob.address)).toString(), toWei(1000).toString()); - alusd_3crv.connect(alusd_3crv_whale).transfer(john.address, toWei(2500)); - assert.equal((await alusd_3crv.balanceOf(john.address)).toString(), toWei(2500).toString()); + alusd_3crv.connect(alusd_3crv_whale).transfer(alice.address, toWei(50)); + assert.equal((await alusd_3crv.balanceOf(alice.address)).toString(), toWei(50).toString()); + alusd_3crv.connect(alusd_3crv_whale).transfer(bob.address, toWei(50)); + assert.equal((await alusd_3crv.balanceOf(bob.address)).toString(), toWei(50).toString()); + alusd_3crv.connect(alusd_3crv_whale).transfer(john.address, toWei(50)); + assert.equal((await alusd_3crv.balanceOf(john.address)).toString(), toWei(50).toString()); }); beforeEach(async () => { @@ -88,24 +90,24 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { it("Should harvest the reward correctly", async () => { console.log("\n---------------------------Alice deposit---------------------------------------\n"); - await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(2000)); - await pickleJar.deposit(toWei(2000), {from: alice.address}); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(50)); + await pickleJar.deposit(toWei(50), {from: alice.address}); console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); - // await pickleJar.earn({ from: alice.address }); + //await pickleJar.earn({ from: alice.address }); await harvest(); console.log("\n---------------------------Bob deposit---------------------------------------\n"); - await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(1000)); - await pickleJar.deposit(toWei(1000), {from: bob.address}); + await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(30)); + await pickleJar.deposit(toWei(30), {from: bob.address}); console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(bob.address)).toString()); - // await pickleJar.earn({ from: bob.address }); + await pickleJar.earn({ from: bob.address }); await time.increase(60 * 60 * 24 * 7); console.log("\n---------------------------John deposit---------------------------------------\n"); - await alusd_3crv.connect(john).approve(pickleJar.address, toWei(2500)); - await pickleJar.deposit(toWei(2500), {from: john.address}); + await alusd_3crv.connect(john).approve(pickleJar.address, toWei(50)); + await pickleJar.deposit(toWei(50), {from: john.address}); console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(john.address)).toString()); await harvest(); @@ -115,6 +117,8 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { let _alcx_before = await alcx.balanceOf(alice.address); console.log("Alice alcx balance before =====> ", _alcx_before.toString()); + // console.log("\nPending reward before withdraw ====> ", (await strategy.pendingReward()).toString()); + await pickleJar.withdrawAll({from: alice.address}); let _alcx_after = await alcx.balanceOf(alice.address); @@ -127,8 +131,9 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { await time.increase(60 * 60 * 24 * 3); console.log("\n---------------------------Alice Redeposit---------------------------------------\n"); - await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(2000)); - await pickleJar.deposit(toWei(2000), {from: alice.address}); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(50)); + await pickleJar.deposit(toWei(50), {from: alice.address}); + //await pickleJar.earn({ from: alice.address }); console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); await time.increase(60 * 60 * 24 * 4); @@ -176,16 +181,15 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { it("Should withdraw the want correctly", async () => { console.log("\n---------------------------Alice deposit---------------------------------------\n"); - await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(2000)); - await pickleJar.deposit(toWei(2000), {from: alice.address}); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(50)); + await pickleJar.deposit(toWei(50), {from: alice.address}); console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); - await pickleJar.earn({from: alice.address}); await harvest(); console.log("\n---------------------------Bob deposit---------------------------------------\n"); - await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(1000)); - await pickleJar.deposit(toWei(1000), {from: bob.address}); + await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(30)); + await pickleJar.deposit(toWei(30), {from: bob.address}); console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(bob.address)).toString()); await pickleJar.earn({from: bob.address}); await harvest(); From 43c1f06097e309c2b0e2cc4612de9af506642cc8 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Tue, 18 May 2021 13:05:08 -0700 Subject: [PATCH 15/19] Update controller and pickle symbiotic jar --- src/controller-v5.sol | 1 - src/pickle-jar-symbiotic.sol | 16 +++++- .../alchemix/StrategyAlusd3crv.test.js | 50 +++++++++++++++++-- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/controller-v5.sol b/src/controller-v5.sol index 2d937cd55..8195494ae 100644 --- a/src/controller-v5.sol +++ b/src/controller-v5.sol @@ -95,7 +95,6 @@ contract ControllerV5 is Initializable { msg.sender == strategist || msg.sender == governance, "!strategist" ); - require(jars[_token] == address(0), "jar"); jars[_token] = _jar; } diff --git a/src/pickle-jar-symbiotic.sol b/src/pickle-jar-symbiotic.sol index d0a7928f7..99d17db63 100644 --- a/src/pickle-jar-symbiotic.sol +++ b/src/pickle-jar-symbiotic.sol @@ -117,9 +117,12 @@ contract PickleJarSymbiotic is ERC20 { } function _updateAccPerShare() internal { - if (totalSupply() == 0) return; curPendingReward = pendingReward(); - require(curPendingReward >= lastPendingReward, "Alchemix protocol failed"); + require(curPendingReward >= lastPendingReward, "Alchemix protocol failed"); + if (totalSupply() == 0) { + accRewardPerShare = 0; + return; + } uint256 addedReward = curPendingReward.sub(lastPendingReward); accRewardPerShare = accRewardPerShare.add((addedReward.mul(1e36)).div(totalSupply())); @@ -140,6 +143,15 @@ contract PickleJarSymbiotic is ERC20 { return reward.balanceOf(address(this)).add(IStrategy(IController(controller).strategies(address(token))).pendingReward()); } + function pendingRewardOfUser(address user) external view returns (uint256) { + if (totalSupply() == 0) return 0; + uint256 allPendingReward = pendingReward(); + if (allPendingReward < lastPendingReward) return 0; + uint256 addedReward = allPendingReward.sub(lastPendingReward); + uint256 newAccRewardPerShare = accRewardPerShare.add((addedReward.mul(1e36)).div(totalSupply())); + return balanceOf(user).mul(newAccRewardPerShare).div(1e36).sub(userRewardDebt[user]); + } + function _withdrawReward() internal { uint256 _pending = balanceOf(msg.sender).mul(accRewardPerShare).div(1e36).sub(userRewardDebt[msg.sender]); uint256 _balance = reward.balanceOf(address(this)); diff --git a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js index 92939c15d..715d76e23 100644 --- a/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js +++ b/src/tests/strategies/alchemix/StrategyAlusd3crv.test.js @@ -93,7 +93,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(50)); await pickleJar.deposit(toWei(50), {from: alice.address}); console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); - //await pickleJar.earn({ from: alice.address }); + await pickleJar.earn({ from: alice.address }); await harvest(); @@ -111,14 +111,16 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { console.log("bob pToken balance =====> ", (await pickleJar.balanceOf(john.address)).toString()); await harvest(); + + console.log("\n---------------------------Alice withdraw---------------------------------------\n"); console.log("Reward balance of strategy ====> ", (await alcx.balanceOf(strategy.address)).toString()); let _alcx_before = await alcx.balanceOf(alice.address); console.log("Alice alcx balance before =====> ", _alcx_before.toString()); - // console.log("\nPending reward before withdraw ====> ", (await strategy.pendingReward()).toString()); + // console.log("Alice pending rewards => ", (await pickleJar.pendingRewardOfUser(alice.address)).toString()); await pickleJar.withdrawAll({from: alice.address}); let _alcx_after = await alcx.balanceOf(alice.address); @@ -133,7 +135,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { console.log("\n---------------------------Alice Redeposit---------------------------------------\n"); await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(50)); await pickleJar.deposit(toWei(50), {from: alice.address}); - //await pickleJar.earn({ from: alice.address }); + await pickleJar.earn({ from: alice.address }); console.log("alice pToken balance =====> ", (await pickleJar.balanceOf(alice.address)).toString()); await time.increase(60 * 60 * 24 * 4); @@ -144,6 +146,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { _alcx_before = await alcx.balanceOf(bob.address); console.log("Bob alcx balance before =====> ", _alcx_before.toString()); + // console.log("Bob pending rewards => ", (await pickleJar.pendingRewardOfUser(bob.address)).toString()); await pickleJar.withdrawAll({from: bob.address}); _alcx_after = await alcx.balanceOf(bob.address); @@ -158,6 +161,7 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { _alcx_before = await alcx.balanceOf(john.address); console.log("John alcx balance before =====> ", _alcx_before.toString()); + // console.log("John pending rewards => ", (await pickleJar.pendingRewardOfUser(john.address)).toString()); await pickleJar.withdrawAll({from: john.address}); _alcx_after = await alcx.balanceOf(john.address); @@ -165,6 +169,8 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { assert.equal(_alcx_after.gt(_alcx_before), true); console.log("\nPending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); + // console.log("Alice pending rewards => ", (await pickleJar.pendingRewardOfUser(alice.address)).toString()); + console.log("\n---------------------------Alice second withdraw---------------------------------------\n"); _alcx_before = await alcx.balanceOf(alice.address); console.log("Alice alcx balance before =====> ", _alcx_before.toString()); @@ -177,6 +183,44 @@ describe("StrategyCurveAlusd3Crv Unit test", () => { assert.equal(_alcx_after.gt(_alcx_before), true); console.log("\nPending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); console.log("\nPickle Jar Pending reward after all withdrawal ====> ", (await pickleJar.pendingReward()).toString()); + console.log("\nPickle Jar curPendingReward after all withdrawal ====> ", (await pickleJar.curPendingReward()).toString()); + console.log("\nPickle Jar lastPendingReward after all withdrawal ====> ", (await pickleJar.lastPendingReward()).toString()); + + console.log("\n---------------------------Alice deposit---------------------------------------\n"); + await alusd_3crv.connect(alice).approve(pickleJar.address, toWei(50)); + await pickleJar.deposit(toWei(50), { from: alice.address }); + + console.log("\n---------------------------Bob deposit---------------------------------------\n"); + await alusd_3crv.connect(bob).approve(pickleJar.address, toWei(30)); + await pickleJar.deposit(toWei(30), { from: bob.address }); + await pickleJar.earn({ from: bob.address }); + + await harvest(); + // console.log("Alice pending rewards => ", (await pickleJar.pendingRewardOfUser(alice.address)).toString()); + // console.log("bob pending rewards => ", (await pickleJar.pendingRewardOfUser(bob.address)).toString()); + + console.log("\n---------------------------Alice Withdraw---------------------------------------\n"); + _alcx_before = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance before =====> ", _alcx_before.toString()); + + await pickleJar.withdrawAll({from: alice.address}); + + _alcx_after = await alcx.balanceOf(alice.address); + console.log("Alice alcx balance after =====> ", _alcx_after.toString()); + + console.log("\n---------------------------Bob Withdraw---------------------------------------\n"); + _alcx_before = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance before =====> ", _alcx_before.toString()); + + await pickleJar.withdrawAll({from: bob.address}); + + _alcx_after = await alcx.balanceOf(bob.address); + console.log("Bob alcx balance after =====> ", (await alcx.balanceOf(bob.address)).toString()); + + console.log("\nPending reward after all withdrawal ====> ", (await strategy.pendingReward()).toString()); + console.log("\nPickle Jar Pending reward after all withdrawal ====> ", (await pickleJar.pendingReward()).toString()); + console.log("\nPickle Jar curPendingReward after all withdrawal ====> ", (await pickleJar.curPendingReward()).toString()); + console.log("\nPickle Jar lastPendingReward after all withdrawal ====> ", (await pickleJar.lastPendingReward()).toString()); }); it("Should withdraw the want correctly", async () => { From f5f2cb341ebf1bab3fd36bdfeba61d27e2b1edcf Mon Sep 17 00:00:00 2001 From: Abiencode Date: Tue, 18 May 2021 13:51:54 -0700 Subject: [PATCH 16/19] update pickle jar --- src/pickle-jar-symbiotic.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pickle-jar-symbiotic.sol b/src/pickle-jar-symbiotic.sol index 99d17db63..e1b13afa8 100644 --- a/src/pickle-jar-symbiotic.sol +++ b/src/pickle-jar-symbiotic.sol @@ -193,6 +193,7 @@ contract PickleJarSymbiotic is ERC20 { } function getRatio() public view returns (uint256) { + if (totalSupply() == 0) return 0; return balance().mul(1e18).div(totalSupply()); } } From 0b2f43cd5c2c57ace82c48ee68b87f00863b456a Mon Sep 17 00:00:00 2001 From: Abiencode Date: Wed, 26 May 2021 12:21:54 -0700 Subject: [PATCH 17/19] update pickle jar --- src/pickle-jar.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pickle-jar.sol b/src/pickle-jar.sol index e2388f0f7..3496ab912 100644 --- a/src/pickle-jar.sol +++ b/src/pickle-jar.sol @@ -126,6 +126,7 @@ contract PickleJar is ERC20 { } function getRatio() public view returns (uint256) { + if (totalSupply() == 0) return 0; return balance().mul(1e18).div(totalSupply()); } } From 959da1878bb7585cf3b0973f0ad7af53c52d0d00 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Wed, 26 May 2021 14:28:37 -0700 Subject: [PATCH 18/19] update sushi eth-alcx staking --- src/interfaces/masterchefv2.sol | 57 ++ src/lib/erc20symbiotic.sol | 608 ++++++++++++++++++ src/pickle-jar-symbiotic.sol | 18 +- .../alchemix/strategy-alcx-farm-base.sol | 57 +- .../alchemix/strategy-sushi-eth-alcx-lp.sol | 2 +- 5 files changed, 705 insertions(+), 37 deletions(-) create mode 100644 src/interfaces/masterchefv2.sol create mode 100644 src/lib/erc20symbiotic.sol diff --git a/src/interfaces/masterchefv2.sol b/src/interfaces/masterchefv2.sol new file mode 100644 index 000000000..b51f56ebd --- /dev/null +++ b/src/interfaces/masterchefv2.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +// interface for Sushiswap MasterChef contract +interface IMasterchefV2 { + function MASTER_PID() external view returns (uint256); + + function add( + uint256 _allocPoint, + address _lpToken, + address _rewarder + ) external; + + function deposit(uint256 _pid, uint256 _amount, address _to) external; + + function pendingSushi(uint256 _pid, address _user) + external + view + returns (uint256); + + function sushiPerBlock() external view returns (uint256); + + function poolInfo(uint256) + external + view + returns ( + uint256 lastRewardBlock, + uint256 accsushiPerShare, + uint256 allocPoint + ); + + function poolLength() external view returns (uint256); + + function set( + uint256 _pid, + uint256 _allocPoint, + address _rewarder, + bool overwrite + ) external; + + function harvestFromMasterChef() external; + + function harvest(uint256 pid, address to) external; + + function totalAllocPoint() external view returns (uint256); + + function updatePool(uint256 _pid) external; + + function userInfo(uint256, address) + external + view + returns (uint256 amount, uint256 rewardDebt); + + function withdraw(uint256 _pid, uint256 _amount, address _to) external; + + function withdrawAndHarvest(uint256 _pid, uint256 _amount, address _to) external; +} diff --git a/src/lib/erc20symbiotic.sol b/src/lib/erc20symbiotic.sol new file mode 100644 index 000000000..06092b82f --- /dev/null +++ b/src/lib/erc20symbiotic.sol @@ -0,0 +1,608 @@ + +// File: contracts/GSN/Context.sol + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./safe-math.sol"; +import "./context.sol"; + +// File: contracts/token/ERC20/IERC20.sol + + +/** + * @dev Interface of the ERC20 standard as defined in the EIP. + */ +interface IERC20 { + + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function decimals() external view returns (uint8); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address recipient, uint256 amount) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 amount) external returns (bool); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} + +// File: contracts/utils/Address.sol + + +/** + * @dev Collection of functions related to the address type + */ +library Address { + /** + * @dev Returns true if `account` is a contract. + * + * [IMPORTANT] + * ==== + * It is unsafe to assume that an address for which this function returns + * false is an externally-owned account (EOA) and not a contract. + * + * Among others, `isContract` will return false for the following + * types of addresses: + * + * - an externally-owned account + * - a contract in construction + * - an address where a contract will be created + * - an address where a contract lived, but was destroyed + * ==== + */ + function isContract(address account) internal view returns (bool) { + // This method relies on extcodesize, which returns 0 for contracts in + // construction, since the code is only stored at the end of the + // constructor execution. + + uint256 size; + // solhint-disable-next-line no-inline-assembly + assembly { size := extcodesize(account) } + return size > 0; + } + + /** + * @dev Replacement for Solidity's `transfer`: sends `amount` wei to + * `recipient`, forwarding all available gas and reverting on errors. + * + * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost + * of certain opcodes, possibly making contracts go over the 2300 gas limit + * imposed by `transfer`, making them unable to receive funds via + * `transfer`. {sendValue} removes this limitation. + * + * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more]. + * + * IMPORTANT: because control is transferred to `recipient`, care must be + * taken to not create reentrancy vulnerabilities. Consider using + * {ReentrancyGuard} or the + * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + */ + function sendValue(address payable recipient, uint256 amount) internal { + require(address(this).balance >= amount, "Address: insufficient balance"); + + // solhint-disable-next-line avoid-low-level-calls, avoid-call-value + (bool success, ) = recipient.call{ value: amount }(""); + require(success, "Address: unable to send value, recipient may have reverted"); + } + + /** + * @dev Performs a Solidity function call using a low level `call`. A + * plain`call` is an unsafe replacement for a function call: use this + * function instead. + * + * If `target` reverts with a revert reason, it is bubbled up by this + * function (like regular Solidity function calls). + * + * Returns the raw returned data. To convert to the expected return value, + * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. + * + * Requirements: + * + * - `target` must be a contract. + * - calling `target` with `data` must not revert. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data) internal returns (bytes memory) { + return functionCall(target, data, "Address: low-level call failed"); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with + * `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) { + return _functionCallWithValue(target, data, 0, errorMessage); + } + + /** + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], + * but also transferring `value` wei to `target`. + * + * Requirements: + * + * - the calling contract must have an ETH balance of at least `value`. + * - the called Solidity function must be `payable`. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { + return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + } + + /** + * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but + * with `errorMessage` as a fallback revert reason when `target` reverts. + * + * _Available since v3.1._ + */ + function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) { + require(address(this).balance >= value, "Address: insufficient balance for call"); + return _functionCallWithValue(target, data, value, errorMessage); + } + + function _functionCallWithValue(address target, bytes memory data, uint256 weiValue, string memory errorMessage) private returns (bytes memory) { + require(isContract(target), "Address: call to non-contract"); + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returndata) = target.call{ value: weiValue }(data); + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} + +// File: contracts/token/ERC20/ERC20.sol + +/** + * @dev Implementation of the {IERC20} interface. + * + * This implementation is agnostic to the way tokens are created. This means + * that a supply mechanism has to be added in a derived contract using {_mint}. + * For a generic mechanism see {ERC20PresetMinterPauser}. + * + * TIP: For a detailed writeup see our guide + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How + * to implement supply mechanisms]. + * + * We have followed general OpenZeppelin guidelines: functions revert instead + * of returning `false` on failure. This behavior is nonetheless conventional + * and does not conflict with the expectations of ERC20 applications. + * + * Additionally, an {Approval} event is emitted on calls to {transferFrom}. + * This allows applications to reconstruct the allowance for all accounts just + * by listening to said events. Other implementations of the EIP may not emit + * these events, as it isn't required by the specification. + * + * Finally, the non-standard {decreaseAllowance} and {increaseAllowance} + * functions have been added to mitigate the well-known issues around setting + * allowances. See {IERC20-approve}. + */ +contract ERC20Symbiotic is Context, IERC20 { + using SafeMath for uint256; + using Address for address; + + mapping (address => uint256) private _balances; + + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + address public gauge; + + /** + * @dev Sets the values for {name} and {symbol}, initializes {decimals} with + * a default value of 18. + * + * To select a different value for {decimals}, use {_setupDecimals}. + * + * All three of these values are immutable: they can only be set once during + * construction. + */ + constructor (string memory name, string memory symbol) public { + _name = name; + _symbol = symbol; + _decimals = 18; + } + + /** + * @dev Returns the name of the token. + */ + function name() public view override returns (string memory) { + return _name; + } + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() public view override returns (string memory) { + return _symbol; + } + + /** + * @dev Returns the number of decimals used to get its user representation. + * For example, if `decimals` equals `2`, a balance of `505` tokens should + * be displayed to a user as `5,05` (`505 / 10 ** 2`). + * + * Tokens usually opt for a value of 18, imitating the relationship between + * Ether and Wei. This is the value {ERC20} uses, unless {_setupDecimals} is + * called. + * + * NOTE: This information is only used for _display_ purposes: it in + * no way affects any of the arithmetic of the contract, including + * {IERC20-balanceOf} and {IERC20-transfer}. + */ + function decimals() public view override returns (uint8) { + return _decimals; + } + + /** + * @dev See {IERC20-totalSupply}. + */ + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + /** + * @dev See {IERC20-balanceOf}. + */ + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + /** + * @dev See {IERC20-transfer}. + * + * Requirements: + * + * - `recipient` cannot be the zero address. + * - the caller must have a balance of at least `amount`. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(_msgSender(), recipient, amount); + return true; + } + + /** + * @dev See {IERC20-allowance}. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + /** + * @dev See {IERC20-approve}. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, amount); + return true; + } + + /** + * @dev See {IERC20-transferFrom}. + * + * Emits an {Approval} event indicating the updated allowance. This is not + * required by the EIP. See the note at the beginning of {ERC20}; + * + * Requirements: + * - `sender` and `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + * - the caller must have allowance for ``sender``'s tokens of at least + * `amount`. + */ + function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) { + _transfer(sender, recipient, amount); + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, "ERC20: transfer amount exceeds allowance")); + return true; + } + + /** + * @dev Atomically increases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } + + /** + * @dev Atomically decreases the allowance granted to `spender` by the caller. + * + * This is an alternative to {approve} that can be used as a mitigation for + * problems described in {IERC20-approve}. + * + * Emits an {Approval} event indicating the updated allowance. + * + * Requirements: + * + * - `spender` cannot be the zero address. + * - `spender` must have allowance for the caller of at least + * `subtractedValue`. + */ + function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue, "ERC20: decreased allowance below zero")); + return true; + } + + /** + * @dev Moves tokens `amount` from `sender` to `recipient`. + * + * This is internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * Requirements: + * + * - `sender` cannot be the zero address. + * - `recipient` cannot be the zero address. + * - `sender` must have a balance of at least `amount`. + */ + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + require(gauge != address(0), "gauge address is not set"); + + require(sender == gauge || recipient == gauge, "you can send/receive this token to/from gauge only"); + + _beforeTokenTransfer(sender, recipient, amount); + + _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance"); + _balances[recipient] = _balances[recipient].add(amount); + emit Transfer(sender, recipient, amount); + } + + /** @dev Creates `amount` tokens and assigns them to `account`, increasing + * the total supply. + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * Requirements + * + * - `to` cannot be the zero address. + */ + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply = _totalSupply.add(amount); + _balances[account] = _balances[account].add(amount); + emit Transfer(address(0), account, amount); + } + + /** + * @dev Destroys `amount` tokens from `account`, reducing the + * total supply. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + */ + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance"); + _totalSupply = _totalSupply.sub(amount); + emit Transfer(account, address(0), amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. + * + * This internal function is equivalent to `approve`, and can be used to + * e.g. set automatic allowances for certain subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + /** + * @dev Sets {decimals} to a value other than the default one of 18. + * + * WARNING: This function should only be called from the constructor. Most + * applications that interact with token contracts will not expect + * {decimals} to ever change, and may work incorrectly if it does. + */ + function _setupDecimals(uint8 decimals_) internal { + _decimals = decimals_; + } + + /** + * @dev Hook that is called before any transfer of tokens. This includes + * minting and burning. + * + * Calling conditions: + * + * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens + * will be to transferred to `to`. + * - when `from` is zero, `amount` tokens will be minted for `to`. + * - when `to` is zero, `amount` of ``from``'s tokens will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { } +} + +/** + * @title SafeERC20 + * @dev Wrappers around ERC20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeERC20 { + using SafeMath for uint256; + using Address for address; + + function safeTransfer(IERC20 token, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + } + + /** + * @dev Deprecated. This function has issues similar to the ones found in + * {IERC20-approve}, and its usage is discouraged. + * + * Whenever possible, use {safeIncreaseAllowance} and + * {safeDecreaseAllowance} instead. + */ + function safeApprove(IERC20 token, address spender, uint256 value) internal { + // safeApprove should only be called when setting an initial allowance, + // or when resetting it to zero. To increase and decrease it, use + // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' + // solhint-disable-next-line max-line-length + require((value == 0) || (token.allowance(address(this), spender) == 0), + "SafeERC20: approve from non-zero to non-zero allowance" + ); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + } + + function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 newAllowance = token.allowance(address(this), spender).add(value); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + } + + function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { + uint256 newAllowance = token.allowance(address(this), spender).sub(value, "SafeERC20: decreased allowance below zero"); + _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IERC20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); + if (returndata.length > 0) { // Return data is optional + // solhint-disable-next-line max-line-length + require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + } + } +} \ No newline at end of file diff --git a/src/pickle-jar-symbiotic.sol b/src/pickle-jar-symbiotic.sol index e1b13afa8..6489ac541 100644 --- a/src/pickle-jar-symbiotic.sol +++ b/src/pickle-jar-symbiotic.sol @@ -4,11 +4,11 @@ pragma solidity ^0.6.7; import "./interfaces/controller.sol"; -import "./lib/erc20.sol"; +import "./lib/erc20symbiotic.sol"; import "./interfaces/strategy.sol"; import "./lib/safe-math.sol"; -contract PickleJarSymbiotic is ERC20 { +contract PickleJarSymbiotic is ERC20Symbiotic { using SafeERC20 for IERC20; using Address for address; using SafeMath for uint256; @@ -40,12 +40,12 @@ contract PickleJarSymbiotic is ERC20 { address _controller ) public - ERC20( - string(abi.encodePacked("pickling ", ERC20(_token).name())), - string(abi.encodePacked("p", ERC20(_token).symbol())) + ERC20Symbiotic( + string(abi.encodePacked("pickling ", IERC20(_token).name())), + string(abi.encodePacked("p", IERC20(_token).symbol())) ) { - _setupDecimals(ERC20(_token).decimals()); + _setupDecimals(IERC20(_token).decimals()); token = IERC20(_token); reward = IERC20(_reward); governance = _governance; @@ -57,6 +57,12 @@ contract PickleJarSymbiotic is ERC20 { return token.balanceOf(address(this)).add(IController(controller).balanceOf(address(token))); } + function setGauge(address _gauge) external { + require(msg.sender == governance, "!governance"); + require(_gauge != address(0), "invalid gauge"); + gauge = _gauge; + } + function setMin(uint256 _min) external { require(msg.sender == governance, "!governance"); require(_min <= max, "numerator cannot be greater than denominator"); diff --git a/src/strategies/alchemix/strategy-alcx-farm-base.sol b/src/strategies/alchemix/strategy-alcx-farm-base.sol index 3cd78ddbd..874c51d87 100644 --- a/src/strategies/alchemix/strategy-alcx-farm-base.sol +++ b/src/strategies/alchemix/strategy-alcx-farm-base.sol @@ -2,16 +2,14 @@ pragma solidity ^0.6.7; import "../strategy-base.sol"; -import "../../interfaces/alcx-farm.sol"; +import "../../interfaces/masterchefv2.sol"; abstract contract StrategyAlcxFarmBase is StrategyBase { // Token addresses address public constant alcx = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; - address public constant stakingPool = 0xAB8e74017a8Cc7c15FFcCd726603790d26d7DeCa; + address public constant sushi = 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2; - // How much ALCX tokens to keep? - uint256 public keepAlcx = 0; - uint256 public constant keepAlcxMax = 10000; + address public constant masterChef = 0xEF0881eC094552b2e128Cf945EF17a6752B4Ec5d; uint256 public poolId; @@ -37,12 +35,12 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { function balanceOfPool() public view override returns (uint256) { - uint256 amount = IStakingPools(stakingPool).getStakeTotalDeposited(address(this), poolId); + (uint256 amount, ) = IMasterchefV2(masterChef).userInfo(poolId, address(this)); return amount; } function getHarvestable() public view returns (uint256) { - return IStakingPools(stakingPool).getStakeTotalUnclaimed(address(this), poolId); + return IMasterchefV2(masterChef).pendingSushi(poolId, address(this)); } // **** Setters **** @@ -50,23 +48,18 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { function deposit() public override { uint256 _want = IERC20(want).balanceOf(address(this)); if (_want > 0) { - IERC20(want).safeApprove(stakingPool, 0); - IERC20(want).safeApprove(stakingPool, _want); - IStakingPools(stakingPool).deposit(poolId, _want); + IERC20(want).safeApprove(masterChef, 0); + IERC20(want).safeApprove(masterChef, _want); + IMasterchefV2(masterChef).deposit(poolId, _want, address(this)); } } function _withdrawSome(uint256 _amount) internal override returns (uint256) { - IStakingPools(stakingPool).withdraw(poolId, _amount); + IMasterchefV2(masterChef).withdraw(poolId, _amount, address(this)); return _amount; } - // **** Setters **** - - function setKeepAlcx(uint256 _keepAlcx) external { - require(msg.sender == timelock, "!timelock"); - keepAlcx = _keepAlcx; - } + // **** State Mutations **** function harvest() public override onlyBenevolent { @@ -76,23 +69,27 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { // i.e. will be be heavily frontrunned? // if so, a new strategy will be deployed. - // Collects ALCX tokens - IStakingPools(stakingPool).claim(poolId); + // Collects Sushi and ALCX tokens + IMasterchefV2(masterChef).deposit(poolId, 0, address(this)); + uint256 _alcx = IERC20(alcx).balanceOf(address(this)); if (_alcx > 0) { - // 10% is locked up for future gov - uint256 _keepAlcx = _alcx.mul(keepAlcx).div(keepAlcxMax); - IERC20(alcx).safeTransfer(IController(controller).treasury(), _keepAlcx); - - uint256 _amount = (_alcx.sub(_keepAlcx)).div(2); - - if (_amount > 0) { - IERC20(alcx).safeApprove(sushiRouter, 0); - IERC20(alcx).safeApprove(sushiRouter, _amount); - _swapSushiswap(alcx, weth, _amount); - } + uint256 _amount = _alcx.div(2); + IERC20(alcx).safeApprove(sushiRouter, 0); + IERC20(alcx).safeApprove(sushiRouter, _amount); + _swapSushiswap(alcx, weth, _amount); } + uint256 _sushi = IERC20(sushi).balanceOf(address(this)); + if (_sushi > 0) { + uint256 _amount = _sushi.div(2); + IERC20(sushi).safeApprove(sushiRouter, 0); + IERC20(sushi).safeApprove(sushiRouter, _sushi); + + _swapSushiswap(sushi, weth, _amount); + _swapSushiswap(sushi, alcx, _amount); + } + // Adds in liquidity for WETH/ALCX uint256 _weth = IERC20(weth).balanceOf(address(this)); diff --git a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol index 57510faa2..b599d7029 100644 --- a/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol +++ b/src/strategies/alchemix/strategy-sushi-eth-alcx-lp.sol @@ -5,7 +5,7 @@ import "./strategy-alcx-farm-base.sol"; contract StrategySushiEthAlcxLp is StrategyAlcxFarmBase { - uint256 public sushi_alcx_poolId = 2; + uint256 public sushi_alcx_poolId = 0; address public sushi_eth_alcx_lp = 0xC3f279090a47e80990Fe3a9c30d24Cb117EF91a8; From 6c29e4b6eebabdcfb4c63d91232cce88eead2222 Mon Sep 17 00:00:00 2001 From: Abiencode Date: Thu, 27 May 2021 20:11:35 -0700 Subject: [PATCH 19/19] update alcx-farm --- .prettierrc | 5 ++- src/interfaces/alcx-rewarder.sol | 10 +++++ src/interfaces/masterchefv2.sol | 22 ++++++++-- .../alchemix/strategy-alcx-farm-base.sol | 44 ++++++++++--------- 4 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 src/interfaces/alcx-rewarder.sol diff --git a/.prettierrc b/.prettierrc index daeba4bc3..e368cfec8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,12 +3,13 @@ { "files": "*.sol", "options": { - "printWidth": 120, + "printWidth": 80, "tabWidth": 4, "useTabs": false, "singleQuote": false, "bracketSpacing": false, "explicitTypes": "always" } - }, + } + ] } diff --git a/src/interfaces/alcx-rewarder.sol b/src/interfaces/alcx-rewarder.sol new file mode 100644 index 000000000..d8541dbe4 --- /dev/null +++ b/src/interfaces/alcx-rewarder.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.6.7; + +// interface for Sushiswap MasterChef contract +interface IAlcxRewarder { + function pendingToken(uint256 pid, address user) + external + view + returns (uint256); +} diff --git a/src/interfaces/masterchefv2.sol b/src/interfaces/masterchefv2.sol index b51f56ebd..362e8cd37 100644 --- a/src/interfaces/masterchefv2.sol +++ b/src/interfaces/masterchefv2.sol @@ -5,13 +5,21 @@ pragma solidity ^0.6.7; interface IMasterchefV2 { function MASTER_PID() external view returns (uint256); + function MASTER_CHEF() external view returns (address); + + function rewarder(uint256 pid) external view returns (address); + function add( uint256 _allocPoint, address _lpToken, address _rewarder ) external; - function deposit(uint256 _pid, uint256 _amount, address _to) external; + function deposit( + uint256 _pid, + uint256 _amount, + address _to + ) external; function pendingSushi(uint256 _pid, address _user) external @@ -51,7 +59,15 @@ interface IMasterchefV2 { view returns (uint256 amount, uint256 rewardDebt); - function withdraw(uint256 _pid, uint256 _amount, address _to) external; + function withdraw( + uint256 _pid, + uint256 _amount, + address _to + ) external; - function withdrawAndHarvest(uint256 _pid, uint256 _amount, address _to) external; + function withdrawAndHarvest( + uint256 _pid, + uint256 _amount, + address _to + ) external; } diff --git a/src/strategies/alchemix/strategy-alcx-farm-base.sol b/src/strategies/alchemix/strategy-alcx-farm-base.sol index 874c51d87..177bac378 100644 --- a/src/strategies/alchemix/strategy-alcx-farm-base.sol +++ b/src/strategies/alchemix/strategy-alcx-farm-base.sol @@ -3,13 +3,15 @@ pragma solidity ^0.6.7; import "../strategy-base.sol"; import "../../interfaces/masterchefv2.sol"; +import "../../interfaces/alcx-rewarder.sol"; abstract contract StrategyAlcxFarmBase is StrategyBase { // Token addresses address public constant alcx = 0xdBdb4d16EdA451D0503b854CF79D55697F90c8DF; address public constant sushi = 0x6B3595068778DD592e39A122f4f5a5cF09C90fE2; - address public constant masterChef = 0xEF0881eC094552b2e128Cf945EF17a6752B4Ec5d; + address public constant masterChef = + 0xEF0881eC094552b2e128Cf945EF17a6752B4Ec5d; uint256 public poolId; @@ -22,20 +24,14 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { address _timelock ) public - StrategyBase( - _lp, - _governance, - _strategist, - _controller, - _timelock - ) + StrategyBase(_lp, _governance, _strategist, _controller, _timelock) { poolId = _poolId; } - function balanceOfPool() public view override returns (uint256) { - (uint256 amount, ) = IMasterchefV2(masterChef).userInfo(poolId, address(this)); + (uint256 amount, ) = + IMasterchefV2(masterChef).userInfo(poolId, address(this)); return amount; } @@ -43,6 +39,11 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { return IMasterchefV2(masterChef).pendingSushi(poolId, address(this)); } + function getHarvestableAlcx() public view returns (uint256) { + address rewarder = IMasterchefV2(masterChef).rewarder(poolId); + return IAlcxRewarder(rewarder).pendingToken(poolId, address(this)); + } + // **** Setters **** function deposit() public override { @@ -54,12 +55,15 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { } } - - function _withdrawSome(uint256 _amount) internal override returns (uint256) { + function _withdrawSome(uint256 _amount) + internal + override + returns (uint256) + { IMasterchefV2(masterChef).withdraw(poolId, _amount, address(this)); return _amount; } - + // **** State Mutations **** function harvest() public override onlyBenevolent { @@ -70,29 +74,29 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { // if so, a new strategy will be deployed. // Collects Sushi and ALCX tokens - IMasterchefV2(masterChef).deposit(poolId, 0, address(this)); + IMasterchefV2(masterChef).harvest(poolId, address(this)); uint256 _alcx = IERC20(alcx).balanceOf(address(this)); if (_alcx > 0) { - uint256 _amount = _alcx.div(2); + uint256 _amount = _alcx.div(2); IERC20(alcx).safeApprove(sushiRouter, 0); - IERC20(alcx).safeApprove(sushiRouter, _amount); + IERC20(alcx).safeApprove(sushiRouter, _amount); _swapSushiswap(alcx, weth, _amount); } uint256 _sushi = IERC20(sushi).balanceOf(address(this)); if (_sushi > 0) { - uint256 _amount = _sushi.div(2); + uint256 _amount = _sushi.div(2); IERC20(sushi).safeApprove(sushiRouter, 0); IERC20(sushi).safeApprove(sushiRouter, _sushi); _swapSushiswap(sushi, weth, _amount); _swapSushiswap(sushi, alcx, _amount); } - + // Adds in liquidity for WETH/ALCX uint256 _weth = IERC20(weth).balanceOf(address(this)); - + _alcx = IERC20(alcx).balanceOf(address(this)); if (_weth > 0 && _alcx > 0) { @@ -123,7 +127,7 @@ abstract contract StrategyAlcxFarmBase is StrategyBase { IERC20(alcx).balanceOf(address(this)) ); } - + _distributePerformanceFeesAndDeposit(); } }