diff --git a/contracts/strategies/SingleRewardStrategy.sol b/contracts/strategies/SingleRewardStrategy.sol new file mode 100644 index 00000000..11f35357 --- /dev/null +++ b/contracts/strategies/SingleRewardStrategy.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import "../YakStrategyV2.sol"; +import "../interfaces/IPair.sol"; +import "../interfaces/IWAVAX.sol"; +import "../lib/DexLibrary.sol"; +import "../lib/SafeERC20.sol"; + +/** + * @notice VariableRewardsStrategy + */ +abstract contract SingleRewardStrategy is YakStrategyV2 { + using SafeERC20 for IERC20; + + IWAVAX internal immutable WAVAX; + address immutable rewardSwapPair; + uint256 immutable swapFee; + address immutable poolReward; + + struct SingleRewardStrategySettings { + string name; + address platformToken; + address rewardSwapPair; + uint256 swapFee; + address timelock; + } + + constructor(SingleRewardStrategySettings memory _settings, StrategySettings memory _strategySettings) + YakStrategyV2(_strategySettings) + { + name = _settings.name; + WAVAX = IWAVAX(_settings.platformToken); + devAddr = 0x2D580F9CF2fB2D09BC411532988F2aFdA4E7BefF; + rewardSwapPair = _settings.rewardSwapPair; + swapFee = _settings.swapFee; + address poolRewardToken; + if (rewardSwapPair != address(0)) { + address token0 = IPair(rewardSwapPair).token0(); + address token1 = IPair(rewardSwapPair).token1(); + poolRewardToken = token0 == _strategySettings.rewardToken ? token1 : token0; + } else { + require(_strategySettings.rewardToken == address(WAVAX), "SingleRewardStrategy::Invalid reward token"); + poolRewardToken = address(WAVAX); + } + poolReward = poolRewardToken; + + updateDepositsEnabled(true); + transferOwnership(_settings.timelock); + emit Reinvest(0, 0); + } + + function calculateDepositFee(uint256 _amount) public view returns (uint256) { + return _calculateDepositFee(_amount); + } + + function calculateWithdrawFee(uint256 _amount) public view returns (uint256) { + return _calculateWithdrawFee(_amount); + } + + /** + * @notice Deposit tokens to receive receipt tokens + * @param _amount Amount of tokens to deposit + */ + function deposit(uint256 _amount) external override { + _deposit(msg.sender, _amount); + } + + /** + * @notice Deposit using Permit + * @param _amount Amount of tokens to deposit + * @param _deadline The time at which to expire the signature + * @param _v The recovery byte of the signature + * @param _r Half of the ECDSA signature pair + * @param _s Half of the ECDSA signature pair + */ + function depositWithPermit( + uint256 _amount, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external override { + depositToken.permit(msg.sender, address(this), _amount, _deadline, _v, _r, _s); + _deposit(msg.sender, _amount); + } + + function depositFor(address _account, uint256 _amount) external override { + _deposit(_account, _amount); + } + + function _deposit(address _account, uint256 _amount) internal { + require(DEPOSITS_ENABLED == true, "VariableRewardsStrategy::Deposits disabled"); + uint256 maxPendingRewards = MAX_TOKENS_TO_DEPOSIT_WITHOUT_REINVEST; + if (maxPendingRewards > 0) { + uint256 estimatedTotalReward = checkReward(); + if (estimatedTotalReward > maxPendingRewards) { + _reinvest(true); + } + } + require( + depositToken.transferFrom(msg.sender, address(this), _amount), + "VariableRewardsStrategy::Deposit token transfer failed" + ); + uint256 depositFee = _calculateDepositFee(_amount); + _mint(_account, getSharesForDepositTokens(_amount - depositFee)); + _stakeDepositTokens(_amount, depositFee); + emit Deposit(_account, _amount); + } + + /** + * @notice Withdraw fee bips from underlying farm + */ + function _getDepositFeeBips() internal view virtual returns (uint256) { + return 0; + } + + /** + * @notice Calculate deposit fee of underlying farm + * @dev Override if deposit fee is calculated dynamically + */ + function _calculateDepositFee(uint256 _amount) internal view virtual returns (uint256) { + uint256 depositFeeBips = _getDepositFeeBips(); + return (_amount * depositFeeBips) / _bip(); + } + + function withdraw(uint256 _amount) external override { + uint256 depositTokenAmount = getDepositTokensForShares(_amount); + require(depositTokenAmount > 0, "VariableRewardsStrategy::Withdraw amount too low"); + uint256 withdrawAmount = _withdrawFromStakingContract(depositTokenAmount); + uint256 withdrawFee = _calculateWithdrawFee(depositTokenAmount); + depositToken.safeTransfer(msg.sender, withdrawAmount - withdrawFee); + _burn(msg.sender, _amount); + emit Withdraw(msg.sender, depositTokenAmount); + } + + /** + * @notice Withdraw fee bips from underlying farm + * @dev Important: Do not override if withdraw fee is deducted from the amount returned by _withdrawFromStakingContract + */ + function _getWithdrawFeeBips() internal view virtual returns (uint256) { + return 0; + } + + /** + * @notice Calculate withdraw fee of underlying farm + * @dev Override if withdraw fee is calculated dynamically + * @dev Important: Do not override if withdraw fee is deducted from the amount returned by _withdrawFromStakingContract + */ + function _calculateWithdrawFee(uint256 _amount) internal view virtual returns (uint256) { + uint256 withdrawFeeBips = _getWithdrawFeeBips(); + return (_amount * withdrawFeeBips) / _bip(); + } + + function reinvest() external override onlyEOA { + _reinvest(false); + } + + function _convertPoolRewardsToRewardToken() private returns (uint256) { + uint256 rewardTokenAmount = rewardToken.balanceOf(address(this)); + if (poolReward == address(WAVAX)) { + uint256 balance = address(this).balance; + if (balance > 0) { + WAVAX.deposit{value: balance}(); + } + if (address(rewardToken) == address(WAVAX)) { + return rewardTokenAmount + balance; + } + } + uint256 amount = IERC20(poolReward).balanceOf(address(this)); + if (amount > 0) { + rewardTokenAmount += DexLibrary.swap( + amount, + poolReward, + address(rewardToken), + IPair(rewardSwapPair), + swapFee + ); + } + return rewardTokenAmount; + } + + /** + * @notice Reinvest rewards from staking contract to deposit tokens + * @dev Reverts if the expected amount of tokens are not returned from the staking contract + */ + function _reinvest(bool userDeposit) private { + _getRewards(); + uint256 amount = _convertPoolRewardsToRewardToken(); + if (!userDeposit) { + require(amount >= MIN_TOKENS_TO_REINVEST, "VariableRewardsStrategy::Reinvest amount too low"); + } + + uint256 devFee = (amount * DEV_FEE_BIPS) / BIPS_DIVISOR; + if (devFee > 0) { + rewardToken.safeTransfer(devAddr, devFee); + } + + uint256 reinvestFee = (amount * REINVEST_REWARD_BIPS) / BIPS_DIVISOR; + if (reinvestFee > 0) { + rewardToken.safeTransfer(msg.sender, reinvestFee); + } + + uint256 depositTokenAmount = _convertRewardTokenToDepositToken(amount - devFee - reinvestFee); + + uint256 depositFee = _calculateDepositFee(depositTokenAmount); + _stakeDepositTokens(depositTokenAmount, depositFee); + emit Reinvest(totalDeposits(), totalSupply); + } + + function _stakeDepositTokens(uint256 _amount, uint256 _depositFee) private { + require(_amount > 0, "VariableRewardsStrategy::Stake amount too low"); + _depositToStakingContract(_amount, _depositFee); + } + + function checkReward() public view override returns (uint256) { + uint256 pendingReward = _pendingRewards(); + uint256 rewardTokenBalance = rewardToken.balanceOf(address(this)); + if (poolReward == address(WAVAX)) { + rewardTokenBalance += address(this).balance; + } + if (poolReward == address(rewardToken)) { + return rewardTokenBalance += pendingReward; + } + uint256 poolRewardBalance = IERC20(poolReward).balanceOf(address(this)); + uint256 amount = poolRewardBalance + pendingReward; + if (amount > 0) { + return + rewardTokenBalance + + DexLibrary.estimateConversionThroughPair( + amount, + poolReward, + address(rewardToken), + IPair(rewardSwapPair), + swapFee + ); + } + return rewardTokenBalance; + } + + /** + * @notice Estimate recoverable balance after withdraw fee + * @return deposit tokens after withdraw fee + */ + function estimateDeployedBalance() external view override returns (uint256) { + uint256 depositBalance = totalDeposits(); + uint256 withdrawFee = _calculateWithdrawFee(depositBalance); + return depositBalance - withdrawFee; + } + + function rescueDeployedFunds( + uint256 _minReturnAmountAccepted, + bool /*_disableDeposits*/ + ) external override onlyOwner { + uint256 balanceBefore = depositToken.balanceOf(address(this)); + _emergencyWithdraw(); + uint256 balanceAfter = depositToken.balanceOf(address(this)); + require( + balanceAfter - balanceBefore >= _minReturnAmountAccepted, + "VariableRewardsStrategy::Emergency withdraw minimum return amount not reached" + ); + emit Reinvest(totalDeposits(), totalSupply); + if (DEPOSITS_ENABLED == true) { + updateDepositsEnabled(false); + } + } + + function _bip() internal view virtual returns (uint256) { + return 10000; + } + + /* VIRTUAL */ + function _convertRewardTokenToDepositToken(uint256 _fromAmount) internal virtual returns (uint256 toAmount); + + function _depositToStakingContract(uint256 _amount, uint256 _depositFee) internal virtual; + + function _withdrawFromStakingContract(uint256 _amount) internal virtual returns (uint256 withdrawAmount); + + function _emergencyWithdraw() internal virtual; + + function _getRewards() internal virtual; + + function _pendingRewards() internal view virtual returns (uint256); +} diff --git a/contracts/strategies/SingleRewardStrategyForSA.sol b/contracts/strategies/SingleRewardStrategyForSA.sol new file mode 100644 index 00000000..3345c09d --- /dev/null +++ b/contracts/strategies/SingleRewardStrategyForSA.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import "../interfaces/IPair.sol"; +import "../lib/DexLibrary.sol"; +import "./SingleRewardStrategy.sol"; + +/** + * @notice Adapter strategy for SingleRewardStrategy with SA deposit. + */ +abstract contract SingleRewardStrategyForSA is SingleRewardStrategy { + address private immutable swapPairDepositToken; + + constructor( + address _swapPairDepositToken, + SingleRewardStrategySettings memory _settings, + StrategySettings memory _strategySettings + ) SingleRewardStrategy(_settings, _strategySettings) { + swapPairDepositToken = _swapPairDepositToken; + assignSwapPairSafely(_swapPairDepositToken); + } + + function assignSwapPairSafely(address _swapPairDepositToken) internal virtual { + if (address(rewardToken) != address(depositToken)) { + require( + DexLibrary.checkSwapPairCompatibility( + IPair(_swapPairDepositToken), + address(depositToken), + address(rewardToken) + ), + "SingleRewardStrategyForSA::swapPairDepositToken does not match deposit and reward token" + ); + } + } + + function _convertRewardTokenToDepositToken(uint256 fromAmount) + internal + virtual + override + returns (uint256 toAmount) + { + if (address(rewardToken) == address(depositToken)) { + return fromAmount; + } + return DexLibrary.swap(fromAmount, address(rewardToken), address(depositToken), IPair(swapPairDepositToken)); + } +} diff --git a/contracts/strategies/avalanche/gmx/CompoundingGmxProxy.sol b/contracts/strategies/avalanche/gmx/CompoundingGmxProxy.sol new file mode 100644 index 00000000..776928c6 --- /dev/null +++ b/contracts/strategies/avalanche/gmx/CompoundingGmxProxy.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import "../../../interfaces/IYakStrategy.sol"; +import "../../../lib/SafeERC20.sol"; + +import "./interfaces/IGmxDepositor.sol"; +import "./interfaces/IGmxRewardRouter.sol"; +import "./interfaces/IGmxRewardTracker.sol"; +import "./interfaces/ICompoundingGmxProxy.sol"; +import "./GmxDepositor.sol"; + +library SafeProxy { + function safeExecute( + IGmxDepositor gmxDepositor, + address target, + uint256 value, + bytes memory data + ) internal returns (bytes memory) { + (bool success, bytes memory returnValue) = gmxDepositor.execute(target, value, data); + if (!success) revert("GmxProxy::safeExecute failed"); + return returnValue; + } +} + +contract CompoundingGmxProxy is ICompoundingGmxProxy { + using SafeProxy for IGmxDepositor; + using SafeERC20 for IERC20; + + address internal constant GMX = 0x62edc0692BD897D2295872a9FFCac5425011c661; + + IGmxDepositor public immutable gmxDepositor; + IGmxDepositor public immutable esGmxHolder; + + address public immutable override gmxRewardRouter; + + address public immutable gmxRewardTracker; + address public immutable feeGmxTracker; + + address public devAddr; + address public approvedStrategy; + + modifier onlyDev() { + require(msg.sender == devAddr, "GmxProxy::onlyDev"); + _; + } + + modifier onlyGmxStrategy() { + require(approvedStrategy == msg.sender, "GmxProxy::onlyGmxStrategy"); + _; + } + + constructor( + address _gmxDepositor, + address _esGmxHolder, + address _gmxRewardRouter, + address _devAddr + ) { + require(_gmxDepositor > address(0), "GmxProxy::Invalid depositor address provided"); + require(_gmxRewardRouter > address(0), "GmxProxy::Invalid reward router address provided"); + require(_devAddr > address(0), "GmxProxy::Invalid dev address provided"); + devAddr = _devAddr; + gmxDepositor = IGmxDepositor(_gmxDepositor); + esGmxHolder = IGmxDepositor(_esGmxHolder); + gmxRewardRouter = _gmxRewardRouter; + gmxRewardTracker = IGmxRewardRouter(_gmxRewardRouter).stakedGmxTracker(); + feeGmxTracker = IGmxRewardRouter(_gmxRewardRouter).feeGmxTracker(); + } + + function updateDevAddr(address newValue) public onlyDev { + require(newValue > address(0), "GmxProxy::Invalid dev address provided"); + devAddr = newValue; + } + + function approveStrategy(address _strategy) external onlyDev { + require(approvedStrategy == address(0), "GmxProxy::Strategy already approved"); + approvedStrategy = _strategy; + } + + function stake(uint256 _amount) external override onlyGmxStrategy { + gmxDepositor.safeExecute( + GMX, + 0, + abi.encodeWithSignature("approve(address,uint256)", gmxRewardTracker, _amount) + ); + gmxDepositor.safeExecute(gmxRewardRouter, 0, abi.encodeWithSignature("stakeGmx(uint256)", _amount)); + } + + function withdraw(uint256 _amount) external override onlyGmxStrategy { + _withdraw(_amount); + } + + function _withdraw(uint256 _amount) private { + gmxDepositor.safeExecute(gmxRewardRouter, 0, abi.encodeWithSignature("unstakeGmx(uint256)", _amount)); + gmxDepositor.safeExecute(GMX, 0, abi.encodeWithSignature("transfer(address,uint256)", msg.sender, _amount)); + } + + function pendingRewards() external view override returns (uint256 pending) { + pending += IGmxRewardTracker(feeGmxTracker).claimable(address(gmxDepositor)); + pending += IGmxRewardTracker(feeGmxTracker).claimable(address(esGmxHolder)); + } + + function claimReward() external override onlyGmxStrategy { + _claim(address(gmxDepositor)); + _compoundEsGmx(address(gmxDepositor)); + _claim(address(esGmxHolder)); + _compoundEsGmx(address(esGmxHolder)); + } + + function _claim(address _depositor) internal { + IGmxDepositor(_depositor).safeExecute( + feeGmxTracker, + 0, + abi.encodeWithSignature("claim(address)", approvedStrategy) + ); + } + + function _compoundEsGmx(address _depositor) internal { + IGmxDepositor(_depositor).safeExecute( + gmxRewardRouter, + 0, + abi.encodeWithSignature( + "handleRewards(bool,bool,bool,bool,bool,bool,bool)", + false, // _shouldClaimGmx + false, // _shouldStakeGmx + true, // _shouldClaimEsGmx + true, // _shouldStakeEsGmx + true, // _shouldStakeMultiplierPoints + false, // _shouldClaimWeth + false // _shouldConvertWethToEth + ) + ); + } + + function totalDeposits() public view override returns (uint256) { + address rewardTracker = IGmxRewardRouter(gmxRewardRouter).stakedGmxTracker(); + return IGmxRewardTracker(rewardTracker).depositBalances(address(gmxDepositor), GMX); + } + + function emergencyWithdraw(uint256 _balance) external override onlyGmxStrategy { + _withdraw(_balance); + } +} diff --git a/contracts/strategies/avalanche/gmx/CompoundingGmxStrategy.sol b/contracts/strategies/avalanche/gmx/CompoundingGmxStrategy.sol new file mode 100644 index 00000000..2d3a9a00 --- /dev/null +++ b/contracts/strategies/avalanche/gmx/CompoundingGmxStrategy.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.13; + +import "../../SingleRewardStrategyForSA.sol"; +import "../../../interfaces/IERC20.sol"; +import "../../../lib/SafeERC20.sol"; + +import "./interfaces/IGmxProxy.sol"; +import "./interfaces/ICompoundingGmxProxy.sol"; + +contract CompoundingGmxStrategy is SingleRewardStrategyForSA { + using SafeERC20 for IERC20; + + ICompoundingGmxProxy public proxy; + + constructor( + address _swapPairDepositToken, + address _gmxProxy, + SingleRewardStrategySettings memory _settings, + StrategySettings memory _strategySettings + ) SingleRewardStrategyForSA(_swapPairDepositToken, _settings, _strategySettings) { + proxy = ICompoundingGmxProxy(_gmxProxy); + } + + function setProxy(address _proxy) external onlyOwner { + proxy = ICompoundingGmxProxy(_proxy); + } + + function _depositToStakingContract(uint256 _amount, uint256) internal override { + depositToken.safeTransfer(address(proxy.gmxDepositor()), _amount); + proxy.stake(_amount); + } + + function _withdrawFromStakingContract(uint256 _amount) internal override returns (uint256 _withdrawAmount) { + proxy.withdraw(_amount); + return _amount; + } + + function _pendingRewards() internal view override returns (uint256) { + return proxy.pendingRewards(); + } + + function _getRewards() internal override { + proxy.claimReward(); + } + + function totalDeposits() public view override returns (uint256) { + return proxy.totalDeposits(); + } + + function _emergencyWithdraw() internal override { + uint256 balance = totalDeposits(); + proxy.emergencyWithdraw(balance); + } +} diff --git a/contracts/strategies/avalanche/gmx/GmxStrategyForGMX.sol b/contracts/strategies/avalanche/gmx/GmxStrategyForGMX.sol deleted file mode 100644 index 6d99029f..00000000 --- a/contracts/strategies/avalanche/gmx/GmxStrategyForGMX.sol +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.13; - -import "../../MasterChefStrategyForSA.sol"; -import "../../../interfaces/IERC20.sol"; -import "../../../interfaces/IWAVAX.sol"; -import "../../../lib/SafeERC20.sol"; - -import "./interfaces/IGmxProxy.sol"; -import "./interfaces/IGmxRewardRouter.sol"; - -contract GmxStrategyForGMX is MasterChefStrategyForSA { - using SafeMath for uint256; - using SafeERC20 for IERC20; - - IWAVAX private constant WAVAX = IWAVAX(0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7); - - IGmxProxy public proxy; - - constructor( - string memory _name, - address _swapPairToken, - address _gmxProxy, - address _timelock, - StrategySettings memory _strategySettings - ) - MasterChefStrategyForSA( - _name, - address(WAVAX), - _swapPairToken, - address(0), - _swapPairToken, - _timelock, - 0, - _strategySettings - ) - { - proxy = IGmxProxy(_gmxProxy); - } - - function setProxy(address _proxy) external onlyOwner { - proxy = IGmxProxy(_proxy); - } - - function _depositMasterchef( - uint256, /*_pid*/ - uint256 _amount - ) internal override { - depositToken.safeTransfer(address(proxy), _amount); - proxy.stakeGmx(_amount); - } - - function _withdrawMasterchef( - uint256, /*_pid*/ - uint256 _amount - ) internal override { - proxy.withdrawGmx(_amount); - } - - function _pendingRewards( - uint256, /*_pid*/ - address /*_user*/ - ) - internal - view - override - returns ( - uint256, - uint256, - address - ) - { - uint256 pendingReward = proxy.pendingRewards(_rewardTracker()); - return (pendingReward, 0, address(0)); - } - - function _getRewards( - uint256 /*_pid*/ - ) internal override { - proxy.claimReward(_rewardTracker()); - } - - function _getDepositBalance( - uint256, /*_pid*/ - address /*user*/ - ) internal view override returns (uint256) { - return _gmxDepositBalance(); - } - - function _emergencyWithdraw( - uint256 /*_pid*/ - ) internal override { - uint256 balance = _gmxDepositBalance(); - proxy.emergencyWithdrawGMX(balance); - } - - function _gmxDepositBalance() private view returns (uint256) { - return proxy.totalDeposits(_rewardTracker()); - } - - function _rewardTracker() private view returns (address) { - address gmxRewardRouter = proxy.gmxRewardRouter(); - return IGmxRewardRouter(gmxRewardRouter).feeGmxTracker(); - } - - function _getDepositFeeBips( - uint256 /*_pid*/ - ) internal pure override returns (uint256) { - return 0; - } - - function _getWithdrawFeeBips( - uint256 /*_pid*/ - ) internal pure override returns (uint256) { - return 0; - } - - function _bip() internal pure override returns (uint256) { - return 10000; - } -} diff --git a/contracts/strategies/avalanche/gmx/interfaces/ICompoundingGmxProxy.sol b/contracts/strategies/avalanche/gmx/interfaces/ICompoundingGmxProxy.sol new file mode 100644 index 00000000..a89135e7 --- /dev/null +++ b/contracts/strategies/avalanche/gmx/interfaces/ICompoundingGmxProxy.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import "./IGmxDepositor.sol"; + +interface ICompoundingGmxProxy { + function gmxDepositor() external view returns (IGmxDepositor); + + function gmxRewardRouter() external view returns (address); + + function stake(uint256 _amount) external; + + function withdraw(uint256 _amount) external; + + function pendingRewards() external view returns (uint256); + + function claimReward() external; + + function totalDeposits() external view returns (uint256); + + function emergencyWithdraw(uint256 _balance) external; +}