From a9ae2cee3f9d88be20275ba15393a3ba316abd94 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Mon, 14 Aug 2023 17:47:58 -0600 Subject: [PATCH] feat: withdraw limit --- src/Depositer.sol | 20 ++-- src/Strategy.sol | 115 +++++++++++++++++----- src/interfaces/Compound/V3/CompoundV3.sol | 6 +- 3 files changed, 110 insertions(+), 31 deletions(-) diff --git a/src/Depositer.sol b/src/Depositer.sol index be8b1b8..869c308 100644 --- a/src/Depositer.sol +++ b/src/Depositer.sol @@ -32,14 +32,14 @@ contract Depositer { Comet public comet; // This is the token we will be borrowing/supplying ERC20 public baseToken; - // The contract to get Comp rewards from + // The contract to get rewards from CometRewards public constant rewardsContract = CometRewards(0x1B0e765F6224C21223AeA2af16c1C46E38885a40); IStrategyInterface public strategy; //The reward Token - address internal constant comp = 0xc00e94Cb662C3520282E6f5717214004A7f26888; + address internal rewardToken; modifier onlyManagement() { checkManagement(); @@ -98,12 +98,15 @@ contract Depositer { baseToken.safeApprove(_comet, type(uint256).max); + rewardToken = rewardsContract.rewardConfig(_comet).token; + //For APR calculations uint256 BASE_MANTISSA = comet.baseScale(); uint256 BASE_INDEX_SCALE = comet.baseIndexScale(); // this is needed for reward apr calculations based on decimals of Asset - // we scale rewards per second to the base token decimals and diff between comp decimals and the index scale + // we scale rewards per second to the base token decimals and diff between + // reward token decimals and the index scale SCALER = (BASE_MANTISSA * 1e18) / BASE_INDEX_SCALE; // default to the base token feed given @@ -169,10 +172,15 @@ contract Depositer { function claimRewards() external onlyStrategy { rewardsContract.claim(address(comet), address(this), true); - uint256 compBal = ERC20(comp).balanceOf(address(this)); + uint256 rewardTokenBalance = ERC20(rewardToken).balanceOf( + address(this) + ); - if (compBal > 0) { - ERC20(comp).safeTransfer(address(strategy), compBal); + if (rewardTokenBalance > 0) { + ERC20(rewardToken).safeTransfer( + address(strategy), + rewardTokenBalance + ); } } diff --git a/src/Strategy.sol b/src/Strategy.sol index c0aa97a..9a2e7c9 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -38,7 +38,7 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { Depositer public immutable depositer; // The reward Token - address internal constant comp = 0xc00e94Cb662C3520282E6f5717214004A7f26888; + address internal immutable rewardToken; // mapping of price feeds. Management can customize if needed mapping(address => address) public priceFeeds; @@ -50,7 +50,6 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { // Warning LTV: ratio at which we will repay uint16 public warningLTVMultiplier = 8_000; // 80% of liquidation LTV - // support uint16 internal constant MAX_BPS = 10_000; // 100% //Thresholds @@ -75,6 +74,9 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { depositer = Depositer(_depositer); require(baseToken == address(depositer.baseToken()), "!base"); + // Set the rewardToken token we will get. + rewardToken = rewardsContract.rewardConfig(_comet).token; + // to supply asset as collateral ERC20(asset).safeApprove(_comet, type(uint256).max); // to repay debt @@ -82,7 +84,7 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { // for depositer to pull funds to deposit ERC20(baseToken).safeApprove(_depositer, type(uint256).max); // to sell reward tokens - ERC20(comp).safeApprove(address(router), type(uint256).max); + ERC20(rewardToken).safeApprove(address(router), type(uint256).max); // Set the needed variables for the Uni Swapper // Base will be weth. @@ -98,7 +100,7 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { // set default price feeds priceFeeds[baseToken] = comet.baseTokenPriceFeed(); // default to COMP/USD - priceFeeds[comp] = 0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5; + priceFeeds[rewardToken] = 0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5; // default to given feed for asset priceFeeds[asset] = comet.getAssetInfoByAddress(asset).priceFeed; } @@ -124,29 +126,29 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { } function setPriceFeed( - address token, - address priceFeed + address _token, + address _priceFeed ) external onlyManagement { // just check it doesnt revert - comet.getPrice(priceFeed); - priceFeeds[token] = priceFeed; + comet.getPrice(_priceFeed); + priceFeeds[_token] = _priceFeed; } function setFees( - uint24 _compToEthFee, + uint24 _rewardToEthFee, uint24 _ethToBaseFee, uint24 _ethToAssetFee ) external onlyManagement { - _setFees(_compToEthFee, _ethToBaseFee, _ethToAssetFee); + _setFees(_rewardToEthFee, _ethToBaseFee, _ethToAssetFee); } function _setFees( - uint24 _compToEthFee, + uint24 _rewardToEthFee, uint24 _ethToBaseFee, uint24 _ethToAssetFee ) internal { address _weth = base; - _setUniFees(comp, _weth, _compToEthFee); + _setUniFees(rewardToken, _weth, _rewardToEthFee); _setUniFees(baseToken, _weth, _ethToBaseFee); _setUniFees(asset, _weth, _ethToAssetFee); } @@ -232,6 +234,10 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { _leveragePosition( Math.min(balanceOfAsset(), availableDepositLimit(address(this))) ); + } else { + // Accrue the balances of both contracts. + comet.accrueAccount(address(this)); + comet.accrueAccount(address(depositer)); } //base token owed should be 0 here but we count it just in case @@ -281,7 +287,9 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { } // Else we need to either adjust LTV up or down. - _leveragePosition(_totalIdle); + _leveragePosition( + Math.min(_totalIdle, availableDepositLimit(address(this))) + ); } /** @@ -292,6 +300,8 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { * @return . Should return true if tend() should be called by keeper or false if not. */ function tendTrigger() public view override returns (bool) { + if (comet.isSupplyPaused() || comet.isWithdrawPaused()) return false; + // if we are in danger of being liquidated tend no matter what if (comet.isLiquidatable(address(this))) return true; @@ -349,6 +359,9 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { function availableDepositLimit( address /*_owner*/ ) public view override returns (uint256) { + // We need to be able to both supply and withdraw on deposits. + if (comet.isSupplyPaused() || comet.isWithdrawPaused()) return 0; + return uint256( comet.getAssetInfoByAddress(asset).supplyCap - @@ -356,6 +369,58 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { ); } + /** + * @notice Gets the max amount of `asset` that can be withdrawn. + * @dev Defaults to an unlimited amount for any address. But can + * be overriden by strategists. + * + * This function will be called before any withdraw or redeem to enforce + * any limits desired by the strategist. This can be used for illiquid + * or sandwhichable strategies. It should never be lower than `totalIdle`. + * + * EX: + * return TokenIzedStrategy.totalIdle(); + * + * This does not need to take into account the `_owner`'s share balance + * or conversion rates from shares to assets. + * + * @param . The address that is withdrawing from the strategy. + * @return . The avialable amount that can be withdrawn in terms of `asset` + */ + function availableWithdrawLimit( + address /*_owner*/ + ) public view override returns (uint256) { + // Default liquidity is the balance of base token + uint256 liquidity = balanceOfCollateral(); + + // If we can't withdraw or supply, set liquidity = 0. + if (comet.isSupplyPaused() || comet.isWithdrawPaused()) { + liquidity = 0; + + // If there is not enough liquidity to pay back our full debt. + } else if ( + ERC20(baseToken).balanceOf(address(comet)) < balanceOfDebt() + ) { + // Adjust liquidity based on withdrawing the full amount of debt. + unchecked { + liquidity = + liquidity - + ( + ((_fromUsd( + _toUsd( + balanceOfDebt() - + ERC20(baseToken).balanceOf(address(comet)), + baseToken + ), + asset + ) * MAX_BPS) / _getTargetLTV()) + ); + } + } + + return TokenizedStrategy.totalIdle() + liquidity; + } + // ----------------- INTERNAL FUNCTIONS SUPPORT ----------------- \\ function _leveragePosition(uint256 _amount) internal { @@ -623,7 +688,7 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { function rewardsInAsset() public view returns (uint256) { // underreport by 10% for safety return - (_fromUsd(_toUsd(depositer.getRewardsOwed(), comp), asset) * + (_fromUsd(_toUsd(depositer.getRewardsOwed(), rewardToken), asset) * 9_000) / MAX_BPS; } @@ -709,26 +774,28 @@ contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { function _claimAndSellRewards() internal { _claimRewards(); - uint256 compBalance; + uint256 rewardTokenBalance; uint256 baseNeeded = baseTokenOwedBalance(); if (baseNeeded > 0) { - compBalance = ERC20(comp).balanceOf(address(this)); + rewardTokenBalance = ERC20(rewardToken).balanceOf(address(this)); // We estimate how much we will need in order to get the amount of base // Accounts for slippage and diff from oracle price, just to assure no horrible sandwhich - uint256 maxComp = (_fromUsd(_toUsd(baseNeeded, baseToken), comp) * - 10_500) / MAX_BPS; - if (maxComp < compBalance) { - // If we have enough swap and exact amount out - _swapTo(comp, baseToken, baseNeeded, maxComp); + uint256 maxRewardToken = (_fromUsd( + _toUsd(baseNeeded, baseToken), + rewardToken + ) * 10_500) / MAX_BPS; + if (maxRewardToken < rewardTokenBalance) { + // If we have enough swap an exact amount out + _swapTo(rewardToken, baseToken, baseNeeded, maxRewardToken); } else { // if not swap everything we have - _swapFrom(comp, baseToken, compBalance, 0); + _swapFrom(rewardToken, baseToken, rewardTokenBalance, 0); } } - compBalance = ERC20(comp).balanceOf(address(this)); - _swapFrom(comp, asset, compBalance, 0); + rewardTokenBalance = ERC20(rewardToken).balanceOf(address(this)); + _swapFrom(rewardToken, asset, rewardTokenBalance, 0); } // This should only ever get called when withdrawing all funds from the strategy if there is debt left over. diff --git a/src/interfaces/Compound/V3/CompoundV3.sol b/src/interfaces/Compound/V3/CompoundV3.sol index 694ef6c..bb5dbf3 100644 --- a/src/interfaces/Compound/V3/CompoundV3.sol +++ b/src/interfaces/Compound/V3/CompoundV3.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.6.12; +pragma solidity 0.8.18; pragma experimental ABIEncoderV2; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -129,6 +129,10 @@ interface Comet is IERC20 { function isLiquidatable(address _address) external view returns (bool); function baseBorrowMin() external view returns (uint256); + + function isSupplyPaused() external view returns (bool); + + function isWithdrawPaused() external view returns (bool); } interface CometRewards {