diff --git a/contracts/MagpieHelper.sol b/contracts/MagpieHelper.sol new file mode 100644 index 0000000..c908f07 --- /dev/null +++ b/contracts/MagpieHelper.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; + +import { IFarmingV2 } from "./interfaces/IFarmingV2.sol"; +import { IWombatPool } from "./interfaces/IWombatPool.sol"; + +// hay: 0x0782b6d8c4551B9760e74c0545a9bCD90bdc41E5 +// hay-lp: 0x1fa71DF4b344ffa5755726Ea7a9a56fbbEe0D38b + +contract MagpieHelper is Initializable { + using SafeERC20Upgradeable for IERC20Upgradeable; + + uint256 public pid; + address public token; + address public tokenLp; + address public farming; + address public wombatPool; + + function initialize( + uint256 _pid, + address _token, + address _tokenLp, + address _farming, + address _wombatPool + ) public initializer { + pid = _pid; + token = _token; + tokenLp = _tokenLp; + farming = _farming; + wombatPool = _wombatPool; + IERC20Upgradeable(token).approve(wombatPool, type(uint256).max); + } + + function deposit(uint256 _amount, uint256 _minimumLiquidity) external { + address user = msg.sender; + IERC20Upgradeable(token).safeTransferFrom(user, address(this), _amount); + IWombatPool(wombatPool).deposit( + token, + _amount, + _minimumLiquidity, + address(this), + block.timestamp, + false + ); + + IFarmingV2(farming).deposit( + pid, + IERC20Upgradeable(tokenLp).balanceOf(address(this)), + false, + user + ); + } + + function withdraw(uint256 _amount, uint256 _minimumLiquidity) external { + address user = msg.sender; + IFarmingV2(farming).withdrawFor(pid, _amount, false, user, address(this)); + + IWombatPool(wombatPool).withdraw( + token, + IERC20Upgradeable(tokenLp).balanceOf(address(this)), + _minimumLiquidity, + user, + block.timestamp + ); + } + + function depositLp(uint256 _amount) external { + address user = msg.sender; + IERC20Upgradeable(tokenLp).safeTransferFrom(user, address(this), _amount); + + IFarmingV2(farming).deposit( + pid, + IERC20Upgradeable(tokenLp).balanceOf(address(this)), + false, + user + ); + } + + function withdrawLp(uint256 _amount) external { + address user = msg.sender; + IERC20Upgradeable(tokenLp).safeTransferFrom(user, address(this), _amount); + + IFarmingV2(farming).withdrawFor(pid, _amount, false, user, user); + } +} diff --git a/contracts/MagpieStrategy.sol b/contracts/MagpieStrategy.sol new file mode 100644 index 0000000..79f19c4 --- /dev/null +++ b/contracts/MagpieStrategy.sol @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +import { IStrategy } from "./interfaces/IStrategy.sol"; +import { IWombatPool } from "./interfaces/IWombatPool.sol"; +import { IPancakeRouter02 } from "./interfaces/IPancakeRouter02.sol"; +import { IHarvesttablePoolHelper } from "./interfaces/IHarvesttablePoolHelper.sol"; + +// solhint-disable max-states-count +contract MagpieStrategy is + IStrategy, + OwnableUpgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable +{ + using SafeERC20Upgradeable for IERC20Upgradeable; + + event AutoharvestChanged(bool value); + event MinEarnAmountChanged(uint256 indexed oldAmount, uint256 indexed newAmount); + + uint256 public pid; + address public farmContractAddress; + address public want; + address public hay; + address public wom; + address public wombatPool; + address public router; + address public helioFarming; + + bool public enableAutoHarvest; + + address[] public earnedToHayPath; + + uint256 internal _wantLockedTotal; + uint256 public sharesTotal; + + uint256 public minEarnAmount; + uint256 public constant MIN_EARN_AMOUNT_LL = 10**10; + + uint256 public slippageFactor; + uint256 public constant SLIPPAGE_FACTOR_UL = 995; + uint256 public constant SLIPPAGE_FACTOR_MAX = 1000; + + modifier onlyHelioFarming() { + require(msg.sender == helioFarming, "!helio Farming"); + _; + } + + function initialize( + uint256 _pid, + uint256 _minEarnAmount, + bool _enableAutoHarvest, + address[] memory _addresses, + // 0 address _farmContractAddress, + // 1 address _want, + // 2 address _cake, + // 3 address _cake, + // 4 address _wombatPool, + // 5 address _router, + // 6 address _helioFarming, + address[] memory _earnedToHayPath + ) public initializer { + __Ownable_init(); + __ReentrancyGuard_init(); + __Pausable_init(); + require(_minEarnAmount >= MIN_EARN_AMOUNT_LL, "min earn amount is too low"); + slippageFactor = 950; + pid = _pid; + minEarnAmount = _minEarnAmount; + farmContractAddress = _addresses[0]; + want = _addresses[1]; + hay = _addresses[2]; + wom = _addresses[3]; + wombatPool = _addresses[4]; + router = _addresses[5]; + helioFarming = _addresses[6]; + enableAutoHarvest = _enableAutoHarvest; + earnedToHayPath = _earnedToHayPath; + } + + function inCaseTokensGetStuck( + address _token, + uint256 _amount, + address _to + ) public virtual onlyOwner { + require(_token != wom, "!safe"); + require(_token != hay, "!safe"); + require(_token != want, "!safe"); + IERC20Upgradeable(_token).safeTransfer(_to, _amount); + } + + function pause() public virtual onlyOwner { + _pause(); + } + + function unpause() public virtual onlyOwner { + _unpause(); + } + + // Receives new deposits from user + function deposit(address, uint256 _wantAmt) + public + virtual + override + onlyHelioFarming + whenNotPaused + returns (uint256) + { + if (enableAutoHarvest) { + _harvest(); + } + IERC20Upgradeable(want).safeTransferFrom(address(msg.sender), address(this), _wantAmt); + + uint256 sharesAdded = _wantAmt; + + uint256 sharesTotalLocal = sharesTotal; + uint256 wantLockedTotalLocal = _wantLockedTotal; + + if (wantLockedTotalLocal > 0 && sharesTotalLocal > 0) { + sharesAdded = (_wantAmt * sharesTotalLocal) / wantLockedTotalLocal; + } + sharesTotal = sharesTotalLocal + sharesAdded; + + _farm(); + + return sharesAdded; + } + + function withdraw(address, uint256 _wantAmt) + public + virtual + override + onlyHelioFarming + nonReentrant + returns (uint256) + { + require(_wantAmt > 0, "_wantAmt <= 0"); + + if (enableAutoHarvest) { + _harvest(); + } + + uint256 sharesRemoved = (_wantAmt * sharesTotal) / _wantLockedTotal; + + uint256 sharesTotalLocal = sharesTotal; + if (sharesRemoved > sharesTotalLocal) { + sharesRemoved = sharesTotalLocal; + } + sharesTotal = sharesTotalLocal - sharesRemoved; + + _unfarm(_wantAmt); + + uint256 wantAmt = IERC20Upgradeable(want).balanceOf(address(this)); + if (_wantAmt > wantAmt) { + _wantAmt = wantAmt; + } + + if (_wantLockedTotal < _wantAmt) { + _wantAmt = _wantLockedTotal; + } + + _wantLockedTotal -= _wantAmt; + + IERC20Upgradeable(want).safeTransfer(helioFarming, _wantAmt); + + return sharesRemoved; + } + + function farm() public virtual nonReentrant { + _farm(); + } + + function _farm() internal virtual { + uint256 wantAmt = IERC20Upgradeable(want).balanceOf(address(this)); + _wantLockedTotal += wantAmt; + IERC20Upgradeable(want).safeIncreaseAllowance(farmContractAddress, wantAmt); + + IHarvesttablePoolHelper(farmContractAddress).depositLP(wantAmt); + } + + function _unfarm(uint256 _wantAmt) internal virtual { + IHarvesttablePoolHelper(farmContractAddress).withdraw( + _wantAmt, + (_wantAmt * slippageFactor) / SLIPPAGE_FACTOR_MAX + ); + } + + function _getRewards() internal virtual { + IHarvesttablePoolHelper(farmContractAddress).depositLP(0); + } + + // 1. Harvest farm tokens + // 2. Converts farm tokens into want tokens + // 3. Deposits want tokens + function harvest() public virtual nonReentrant whenNotPaused { + _harvest(); + } + + // 1. Harvest farm tokens + // 2. Converts farm tokens into want tokens + // 3. Deposits want tokens + function _harvest() internal virtual { + // Harvest farm tokens + _getRewards(); + + // Converts farm tokens into want tokens + uint256 earnedHayAmt = IERC20Upgradeable(hay).balanceOf(address(this)); + uint256 earnedWomAmt = IERC20Upgradeable(wom).balanceOf(address(this)); + uint256[] memory amounts = IPancakeRouter02(router).getAmountsOut( + earnedWomAmt, + earnedToHayPath + ); + uint256 hayEarned = earnedHayAmt + amounts[amounts.length - 1]; + + if (hayEarned < minEarnAmount) { + return; + } + + IERC20Upgradeable(wom).safeApprove(router, 0); + IERC20Upgradeable(wom).safeIncreaseAllowance(router, earnedWomAmt); + + // Swap half earned to token1 + _safeSwap( + router, + earnedWomAmt, + slippageFactor, + earnedToHayPath, + address(this), + block.timestamp + 500 + ); + + // Get want tokens, ie. add liquidity + earnedHayAmt = IERC20Upgradeable(hay).balanceOf(address(this)); + IERC20Upgradeable(hay).safeIncreaseAllowance(wombatPool, earnedHayAmt); + IWombatPool(wombatPool).deposit( + hay, + earnedHayAmt, + (earnedHayAmt * slippageFactor) / SLIPPAGE_FACTOR_MAX, + address(this), + block.timestamp, + false + ); + + _farm(); + } + + function _safeSwap( + address _uniRouterAddress, + uint256 _amountIn, + uint256 _slippageFactor, + address[] memory _path, + address _to, + uint256 _deadline + ) internal virtual { + uint256[] memory amounts = IPancakeRouter02(_uniRouterAddress).getAmountsOut(_amountIn, _path); + uint256 amountOut = amounts[amounts.length - 1]; + + IPancakeRouter02(_uniRouterAddress).swapExactTokensForTokensSupportingFeeOnTransferTokens( + _amountIn, + (amountOut * _slippageFactor) / SLIPPAGE_FACTOR_MAX, + _path, + _to, + _deadline + ); + } + + function setAutoHarvest(bool _value) external onlyOwner { + enableAutoHarvest = _value; + emit AutoharvestChanged(_value); + } + + function setSlippageFactor(uint256 _slippageFactor) external onlyOwner { + require(_slippageFactor <= SLIPPAGE_FACTOR_UL, "slippageFactor too high"); + slippageFactor = _slippageFactor; + } + + function setMinEarnAmount(uint256 _minEarnAmount) external onlyOwner { + require(_minEarnAmount >= MIN_EARN_AMOUNT_LL, "min earn amount is too low"); + minEarnAmount = _minEarnAmount; + emit MinEarnAmountChanged(minEarnAmount, _minEarnAmount); + } + + function wantLockedTotal() external view virtual override returns (uint256) { + return _wantLockedTotal; + } +} diff --git a/contracts/interfaces/IFarmingV2.sol b/contracts/interfaces/IFarmingV2.sol new file mode 100644 index 0000000..eabcd90 --- /dev/null +++ b/contracts/interfaces/IFarmingV2.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { IFarming } from "./IFarming.sol"; + +interface IFarmingV2 is IFarming { + function withdrawFor( + uint256 _pid, + uint256 _wantAmt, + bool _claimRewards, + address _userAddress, + address _receiver + ) external returns (uint256); +} diff --git a/contracts/interfaces/IHarvesttablePoolHelper.sol b/contracts/interfaces/IHarvesttablePoolHelper.sol new file mode 100644 index 0000000..06b3d48 --- /dev/null +++ b/contracts/interfaces/IHarvesttablePoolHelper.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IPoolHelper.sol"; + +interface IHarvesttablePoolHelper is IPoolHelper { + function harvest() external; +} diff --git a/contracts/interfaces/IPoolHelper.sol b/contracts/interfaces/IPoolHelper.sol new file mode 100644 index 0000000..8a1bec4 --- /dev/null +++ b/contracts/interfaces/IPoolHelper.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +interface IPoolHelper { + function totalStaked() external view returns (uint256); + + function balance(address _address) external view returns (uint256); + + function deposit(uint256 amount, uint256 minimumAmount) external; + + function withdraw(uint256 amount, uint256 minimumAmount) external; + + function depositLP(uint256 _lpAmount) external; +} diff --git a/contracts/interfaces/IWombatPool.sol b/contracts/interfaces/IWombatPool.sol new file mode 100644 index 0000000..11f44fc --- /dev/null +++ b/contracts/interfaces/IWombatPool.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +interface IWombatPool { + function getTokens() external view returns (address[] memory); + + function addressOfAsset(address token) external view returns (address); + + function deposit( + address token, + uint256 amount, + uint256 minimumLiquidity, + address to, + uint256 deadline, + bool shouldStake + ) external returns (uint256 liquidity); + + function withdraw( + address token, + uint256 liquidity, + uint256 minimumAmount, + address to, + uint256 deadline + ) external returns (uint256 amount); + + function withdrawFromOtherAsset( + address fromToken, + address toToken, + uint256 liquidity, + uint256 minimumAmount, + address to, + uint256 deadline + ) external returns (uint256 amount); + + function swap( + address fromToken, + address toToken, + uint256 fromAmount, + uint256 minimumToAmount, + address to, + uint256 deadline + ) external returns (uint256 actualToAmount, uint256 haircut); + + function quotePotentialDeposit(address token, uint256 amount) + external + view + returns (uint256 liquidity, uint256 reward); + + function quotePotentialSwap( + address fromToken, + address toToken, + int256 fromAmount + ) external view returns (uint256 potentialOutcome, uint256 haircut); + + function quotePotentialWithdraw(address token, uint256 liquidity) + external + view + returns (uint256 amount, uint256 fee); + + function quoteAmountIn( + address fromToken, + address toToken, + int256 toAmount + ) external view returns (uint256 amountIn, uint256 haircut); +} diff --git a/contracts/upgrades/FarmingV2.sol b/contracts/upgrades/FarmingV2.sol new file mode 100644 index 0000000..0775ae3 --- /dev/null +++ b/contracts/upgrades/FarmingV2.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { SafeERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +import { Farming } from "../Farming.sol"; +import { IStrategy } from "../interfaces/IStrategy.sol"; +import { IFarmingV2, IFarming } from "../interfaces/IFarmingV2.sol"; +import { IIncentiveVoting } from "../interfaces/IIncentiveVoting.sol"; + +contract FarmingV2 is Farming, IFarmingV2 { + using SafeERC20Upgradeable for IERC20Upgradeable; + + event HelperWhitelist(address indexed helper, bool indexed whitelisted); + + mapping(address => bool) public whitelistedHelpers; + + function withdraw( + uint256 _pid, + uint256 _wantAmt, + bool _claimRewards + ) public virtual override(Farming, IFarming) nonReentrant returns (uint256) { + return _withdraw(_pid, _wantAmt, _claimRewards, msg.sender, msg.sender); + } + + function withdrawFor( + uint256 _pid, + uint256 _wantAmt, + bool _claimRewards, + address _userAddress, + address _receiver + ) external nonReentrant returns (uint256) { + require(whitelistedHelpers[msg.sender], "caller is not whitelisted"); + require(!blockThirdPartyActions[_userAddress], "third party actions are blocked by user"); + return _withdraw(_pid, _wantAmt, _claimRewards, _userAddress, _receiver); + } + + function _withdraw( + uint256 _pid, + uint256 _wantAmt, + bool _claimRewards, + address _userAddress, + address _receiver + ) internal virtual returns (uint256) { + require(_wantAmt > 0, "Cannot withdraw zero"); + uint256 accRewardPerShare = updatePool(_pid); + PoolInfo storage pool = poolInfo[_pid]; + UserInfo storage user = userInfo[_pid][_userAddress]; + + uint256 sharesTotal = pool.strategy.sharesTotal(); + + require(user.shares > 0, "user.shares is 0"); + require(sharesTotal > 0, "sharesTotal is 0"); + + uint256 pending = (user.shares * accRewardPerShare) / 1e12 - user.rewardDebt; + if (_claimRewards) { + pending += user.claimable; + user.claimable = 0; + pending = _safeRewardTransfer(_userAddress, pending); + } else if (pending > 0) { + user.claimable += pending; + pending = 0; + } + // Withdraw want tokens + uint256 amount = (user.shares * pool.strategy.wantLockedTotal()) / sharesTotal; + if (_wantAmt > amount) { + _wantAmt = amount; + } + uint256 sharesRemoved = pool.strategy.withdraw(_userAddress, _wantAmt); + + if (sharesRemoved > user.shares) { + user.shares = 0; + } else { + user.shares -= sharesRemoved; + } + + uint256 wantBal = pool.token.balanceOf(address(this)); + if (wantBal < _wantAmt) { + _wantAmt = wantBal; + } + user.rewardDebt = (user.shares * pool.accRewardPerShare) / 1e12; + pool.token.safeTransfer(_receiver, _wantAmt); + + emit Withdraw(_userAddress, _pid, _wantAmt); + return pending; + } + + function whitelistHelper(address helper, bool whitelist) external onlyOwner { + whitelistedHelpers[helper] = whitelist; + emit HelperWhitelist(helper, whitelist); + } +}