diff --git a/foundry.toml b/foundry.toml index f93a49c..ff03d83 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ out = 'out' libs = ['lib'] remappings = [ - "@openzeppelin/=lib/openzeppelin-contracts/", + '@openzeppelin/=lib/openzeppelin-contracts/', "forge-std/=lib/forge-std/src/", "@tokenized-strategy/=lib/tokenized-strategy/src/", "@periphery/=lib/tokenized-strategy-periphery/src/", diff --git a/src/Depositer.sol b/src/Depositer.sol new file mode 100644 index 0000000..0277fd4 --- /dev/null +++ b/src/Depositer.sol @@ -0,0 +1,299 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.18; +pragma experimental ABIEncoderV2; + +import {IStrategyInterface} from "./interfaces/IStrategyInterface.sol"; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {CometStructs} from "./interfaces/Compound/V3/CompoundV3.sol"; +import {Comet} from "./interfaces/Compound/V3/CompoundV3.sol"; +import {CometRewards} from "./interfaces/Compound/V3/CompoundV3.sol"; + +contract Depositer { + using SafeERC20 for ERC20; + //Used for cloning + bool public original = true; + + //Used for Comp apr calculations + uint64 internal constant DAYS_PER_YEAR = 365; + uint64 internal constant SECONDS_PER_DAY = 60 * 60 * 24; + uint64 internal constant SECONDS_PER_YEAR = 365 days; + + // price feeds for the reward apr calculation, can be updated manually if needed + address public rewardTokenPriceFeed; + address public baseTokenPriceFeed; + + // scaler used in reward apr calculations + uint256 internal SCALER; + + // This is the address of the main V3 pool + Comet public comet; + // This is the token we will be borrowing/supplying + ERC20 public baseToken; + // The contract to get Comp rewards from + CometRewards public constant rewardsContract = + CometRewards(0x1B0e765F6224C21223AeA2af16c1C46E38885a40); + + IStrategyInterface public strategy; + + //The reward Token + address internal constant comp = 0xc00e94Cb662C3520282E6f5717214004A7f26888; + + modifier onlyManagement() { + checkManagement(); + _; + } + + modifier onlyStrategy() { + checkStrategy(); + _; + } + + function checkManagement() internal view { + require(msg.sender == strategy.management(), "!authorized"); + } + + function checkStrategy() internal view { + require(msg.sender == address(strategy), "!authorized"); + } + + event Cloned(address indexed clone); + + function cloneDepositer( + address _comet + ) external returns (address newDepositer) { + require(original, "!original"); + newDepositer = _clone(_comet); + } + + function _clone(address _comet) internal returns (address newDepositer) { + // Copied from https://github.com/optionality/clone-factory/blob/master/contracts/CloneFactory.sol + bytes20 addressBytes = bytes20(address(this)); + + assembly { + // EIP-1167 bytecode + let clone_code := mload(0x40) + mstore( + clone_code, + 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000 + ) + mstore(add(clone_code, 0x14), addressBytes) + mstore( + add(clone_code, 0x28), + 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000 + ) + newDepositer := create(0, clone_code, 0x37) + } + + Depositer(newDepositer).initialize(_comet); + emit Cloned(newDepositer); + } + + function initialize(address _comet) public { + require(address(comet) == address(0), "!initiliazd"); + comet = Comet(_comet); + baseToken = ERC20(comet.baseToken()); + + baseToken.safeApprove(_comet, type(uint256).max); + + //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 + SCALER = (BASE_MANTISSA * 1e18) / BASE_INDEX_SCALE; + + // default to the base token feed given + baseTokenPriceFeed = comet.baseTokenPriceFeed(); + // default to the COMP/USD feed + rewardTokenPriceFeed = 0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5; + } + + function setStrategy(address _strategy) external { + // Can only set the strategy once + require(address(strategy) == address(0), "set"); + + strategy = IStrategyInterface(_strategy); + + // make sure it has the same base token + require(address(baseToken) == strategy.baseToken(), "!base"); + // Make sure this contract is set as the depositer + require(address(this) == address(strategy.depositer()), "!depositer"); + } + + function setPriceFeeds( + address _baseTokenPriceFeed, + address _rewardTokenPriceFeed + ) external onlyManagement { + // just check the call doesnt revert. We dont care about the amount returned + comet.getPrice(_baseTokenPriceFeed); + comet.getPrice(_rewardTokenPriceFeed); + baseTokenPriceFeed = _baseTokenPriceFeed; + rewardTokenPriceFeed = _rewardTokenPriceFeed; + } + + function cometBalance() external view returns (uint256) { + return comet.balanceOf(address(this)); + } + + // Non-view function to accrue account for the most accurate accounting + function accruedCometBalance() public returns (uint256) { + comet.accrueAccount(address(this)); + return comet.balanceOf(address(this)); + } + + function withdraw(uint256 _amount) external onlyStrategy { + if (_amount == 0) return; + ERC20 _baseToken = baseToken; + + comet.withdraw(address(_baseToken), _amount); + + uint256 balance = _baseToken.balanceOf(address(this)); + require(balance >= _amount, "!bal"); + _baseToken.safeTransfer(address(strategy), balance); + } + + function deposit() external onlyStrategy { + ERC20 _baseToken = baseToken; + // msg.sender has been checked to be strategy + uint256 _amount = _baseToken.balanceOf(msg.sender); + if (_amount == 0) return; + + _baseToken.safeTransferFrom(msg.sender, address(this), _amount); + comet.supply(address(_baseToken), _amount); + } + + function claimRewards() external onlyStrategy { + rewardsContract.claim(address(comet), address(this), true); + + uint256 compBal = ERC20(comp).balanceOf(address(this)); + + if (compBal > 0) { + ERC20(comp).safeTransfer(address(strategy), compBal); + } + } + + // ----------------- COMET VIEW FUNCTIONS ----------------- + + // We put these in the depositer contract to save byte code in the main strategy \\ + + /* + * Gets the amount of reward tokens due to this contract and the base strategy + */ + function getRewardsOwed() external view returns (uint256) { + CometStructs.RewardConfig memory config = rewardsContract.rewardConfig( + address(comet) + ); + uint256 accrued = comet.baseTrackingAccrued(address(this)) + + comet.baseTrackingAccrued(address(strategy)); + if (config.shouldUpscale) { + accrued *= config.rescaleFactor; + } else { + accrued /= config.rescaleFactor; + } + uint256 claimed = rewardsContract.rewardsClaimed( + address(comet), + address(this) + ) + rewardsContract.rewardsClaimed(address(comet), address(strategy)); + + return accrued > claimed ? accrued - claimed : 0; + } + + function getNetBorrowApr( + uint256 newAmount + ) public view returns (uint256 netApr) { + uint256 newUtilization = ((comet.totalBorrow() + newAmount) * 1e18) / + (comet.totalSupply() + newAmount); + uint256 borrowApr = getBorrowApr(newUtilization); + uint256 supplyApr = getSupplyApr(newUtilization); + // supply rate can be higher than borrow when utilization is very high + netApr = borrowApr > supplyApr ? borrowApr - supplyApr : 0; + } + + /* + * Get the current supply APR in Compound III + */ + function getSupplyApr(uint256 newUtilization) public view returns (uint) { + unchecked { + return + comet.getSupplyRate( + newUtilization // New utilization + ) * SECONDS_PER_YEAR; + } + } + + /* + * Get the current borrow APR in Compound III + */ + function getBorrowApr( + uint256 newUtilization + ) public view returns (uint256) { + unchecked { + return + comet.getBorrowRate( + newUtilization // New utilization + ) * SECONDS_PER_YEAR; + } + } + + function getNetRewardApr(uint256 newAmount) public view returns (uint256) { + unchecked { + return + getRewardAprForBorrowBase(newAmount) + + getRewardAprForSupplyBase(newAmount); + } + } + + /* + * Get the current reward for supplying APR in Compound III + * @param newAmount The new amount we will be supplying + * @return The reward APR in USD as a decimal scaled up by 1e18 + */ + function getRewardAprForSupplyBase( + uint256 newAmount + ) public view returns (uint) { + Comet _comet = comet; + unchecked { + uint256 rewardToSuppliersPerDay = _comet.baseTrackingSupplySpeed() * + SECONDS_PER_DAY * + SCALER; + if (rewardToSuppliersPerDay == 0) return 0; + return + ((_comet.getPrice(rewardTokenPriceFeed) * + rewardToSuppliersPerDay) / + ((_comet.totalSupply() + newAmount) * + _comet.getPrice(baseTokenPriceFeed))) * DAYS_PER_YEAR; + } + } + + /* + * Get the current reward for borrowing APR in Compound III + * @param newAmount The new amount we will be borrowing + * @return The reward APR in USD as a decimal scaled up by 1e18 + */ + function getRewardAprForBorrowBase( + uint256 newAmount + ) public view returns (uint256) { + // borrowBaseRewardApr = (rewardTokenPriceInUsd * rewardToBorrowersPerDay / (baseTokenTotalBorrow * baseTokenPriceInUsd)) * DAYS_PER_YEAR; + Comet _comet = comet; + unchecked { + uint256 rewardToBorrowersPerDay = _comet.baseTrackingBorrowSpeed() * + SECONDS_PER_DAY * + SCALER; + if (rewardToBorrowersPerDay == 0) return 0; + return + ((_comet.getPrice(rewardTokenPriceFeed) * + rewardToBorrowersPerDay) / + ((_comet.totalBorrow() + newAmount) * + _comet.getPrice(baseTokenPriceFeed))) * DAYS_PER_YEAR; + } + } + + function manualWithdraw() external onlyManagement { + // Withdraw everything we have + comet.withdraw(address(baseToken), accruedCometBalance()); + } +} \ No newline at end of file diff --git a/src/Strategy.sol b/src/Strategy.sol index f74b427..e49212d 100644 --- a/src/Strategy.sol +++ b/src/Strategy.sol @@ -3,32 +3,162 @@ pragma solidity 0.8.18; import {BaseTokenizedStrategy} from "@tokenized-strategy/BaseTokenizedStrategy.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -// Import interfaces for many popular DeFi projects, or add your own! -//import "../interfaces//.sol"; +import {CometStructs} from "./interfaces/Compound/V3/CompoundV3.sol"; +import {Comet} from "./interfaces/Compound/V3/CompoundV3.sol"; +import {CometRewards} from "./interfaces/Compound/V3/CompoundV3.sol"; -/** - * The `TokenizedStrategy` variable can be used to retrieve the strategies - * specifc storage data your contract. - * - * i.e. uint256 totalAssets = TokenizedStrategy.totalAssets() - * - * This can not be used for write functions. Any TokenizedStrategy - * variables that need to be udpated post deployement will need to - * come from an external call from the strategies specific `management`. - */ +// Uniswap V3 Swapper +import {UniswapV3Swapper} from "@periphery/swappers/UniswapV3Swapper.sol"; -// NOTE: To implement permissioned functions you can use the onlyManagement and onlyKeepers modifiers +import {Depositer} from "./Depositer.sol"; -contract Strategy is BaseTokenizedStrategy { +interface IBaseFeeGlobal { + function basefee_global() external view returns (uint256); +} + +contract Strategy is BaseTokenizedStrategy, UniswapV3Swapper { using SafeERC20 for ERC20; + // if set to true, the strategy will not try to repay debt by selling rewards or asset + bool public leaveDebtBehind; + + // This is the address of the main V3 pool + Comet public comet; + // This is the token we will be borrowing/supplying + address public baseToken; + // The contract to get Comp rewards from + CometRewards public constant rewardsContract = + CometRewards(0x1B0e765F6224C21223AeA2af16c1C46E38885a40); + + // The Contract that will deposit the baseToken back into Compound + Depositer public depositer; + + // The reward Token + address internal constant comp = 0xc00e94Cb662C3520282E6f5717214004A7f26888; + + // mapping of price feeds. Cheaper and management can customize if needed + mapping(address => address) public priceFeeds; + + // NOTE: LTV = Loan-To-Value = debt/collateral + // Target LTV: ratio up to which which we will borrow up to liquidation threshold + uint16 public targetLTVMultiplier = 7_000; + + // 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 + uint256 internal minThreshold; + uint256 public maxGasPriceToTend; + constructor( address _asset, - string memory _name - ) BaseTokenizedStrategy(_asset, _name) {} + string memory _name, + address _comet, + uint24 _ethToAssetFee, + address _depositer + ) BaseTokenizedStrategy(_asset, _name) { + initializeCompV3LenderBorrower(_comet, _ethToAssetFee, _depositer); + } + + // ----------------- SETTERS ----------------- + // we put all together to save contract bytecode (!) + function setStrategyParams( + uint16 _targetLTVMultiplier, + uint16 _warningLTVMultiplier, + uint256 _minToSell, + bool _leaveDebtBehind, + uint256 _maxGasPriceToTend + ) external onlyManagement { + require( + _warningLTVMultiplier <= 9_000 && + _targetLTVMultiplier < _warningLTVMultiplier + ); + targetLTVMultiplier = _targetLTVMultiplier; + warningLTVMultiplier = _warningLTVMultiplier; + minAmountToSell = _minToSell; + leaveDebtBehind = _leaveDebtBehind; + maxGasPriceToTend = _maxGasPriceToTend; + } + + function setPriceFeed( + address token, + address priceFeed + ) external onlyManagement { + // just check it doesnt revert + comet.getPrice(priceFeed); + priceFeeds[token] = priceFeed; + } + + function setFees( + uint24 _compToEthFee, + uint24 _ethToBaseFee, + uint24 _ethToAssetFee + ) external onlyManagement { + _setFees(_compToEthFee, _ethToBaseFee, _ethToAssetFee); + } + + function _setFees( + uint24 _compToEthFee, + uint24 _ethToBaseFee, + uint24 _ethToAssetFee + ) internal { + address _weth = base; + _setUniFees(comp, _weth, _compToEthFee); + _setUniFees(baseToken, _weth, _ethToBaseFee); + _setUniFees(asset, _weth, _ethToAssetFee); + } + + function initializeCompV3LenderBorrower( + address _comet, + uint24 _ethToAssetFee, + address _depositer + ) public { + // Make sure we only initialize one time + require(address(comet) == address(0)); + comet = Comet(_comet); + + //Get the baseToken we wil borrow and the min + baseToken = comet.baseToken(); + minThreshold = comet.baseBorrowMin(); + + depositer = Depositer(_depositer); + require(baseToken == address(depositer.baseToken()), "!base"); + + // to supply asset as collateral + ERC20(asset).safeApprove(_comet, type(uint256).max); + // to repay debt + ERC20(baseToken).safeApprove(_comet, type(uint256).max); + // 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); + + // Set the needed variables for the Uni Swapper + // Base will be weth. + base = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + // UniV3 mainnet router. + router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + // Set the min amount for the swapper to sell + minAmountToSell = 1e12; + + //Default to .3% pool for comp/eth and to .05% pool for eth/baseToken + _setFees(3000, 500, _ethToAssetFee); + + // set default price feeds + priceFeeds[baseToken] = comet.baseTokenPriceFeed(); + // default to COMP/USD + priceFeeds[comp] = 0xdbd020CAeF83eFd542f4De03e3cF0C28A4428bd5; + // default to given feed for asset + priceFeeds[asset] = comet.getAssetInfoByAddress(asset).priceFeed; + } + /*////////////////////////////////////////////////////////////// NEEDED TO BE OVERRIDEN BY STRATEGIST @@ -46,9 +176,7 @@ contract Strategy is BaseTokenizedStrategy { * to deposit in the yield source. */ function _deployFunds(uint256 _amount) internal override { - // TODO: implement deposit logice EX: - // - // lendingpool.deposit(asset, _amount ,0); + _leveragePosition(_amount); } /** @@ -73,9 +201,7 @@ contract Strategy is BaseTokenizedStrategy { * @param _amount, The amount of 'asset' to be freed. */ function _freeFunds(uint256 _amount) internal override { - // TODO: implement withdraw logic EX: - // - // lendingPool.withdraw(asset, _amount); + _liquidatePosition(_amount); } /** @@ -105,11 +231,22 @@ contract Strategy is BaseTokenizedStrategy { override returns (uint256 _totalAssets) { - // TODO: Implement harvesting logic and accurate accounting EX: - // - // _claminAndSellRewards(); - // _totalAssets = aToken.balanceof(address(this)) + ERC20(asset).balanceOf(address(this)); - _totalAssets = ERC20(asset).balanceOf(address(this)); + if (!TokenizedStrategy.isShutdown()) { + // 1. claim rewards, 2. even baseToken deposits and borrows 3. sell remainder of rewards to asset. + // This will accrue this account as well as the depositer so all future calls are accurate + _claimAndSellRewards(); + + uint256 loose = balanceOfAsset(); + if (loose > 0) { + _leveragePosition(loose); + } + } + + //base token owed should be 0 here but we count it just in case + _totalAssets = + balanceOfAsset() + + balanceOfCollateral() - + baseTokenOwedInAsset(); } /*////////////////////////////////////////////////////////////// @@ -138,9 +275,22 @@ contract Strategy is BaseTokenizedStrategy { * till report() is called. * * @param _totalIdle The current amount of idle funds that are available to deploy. - * - function _tend(uint256 _totalIdle) internal override {} - */ + */ + function _tend(uint256 _totalIdle) internal override { + // Accrue account for accurate balances + comet.accrueAccount(address(this)); + + // If the cost to borrow > rewards rate we will pull out all funds to not report a loss + if (getNetBorrowApr(0) > getNetRewardApr(0)) { + // Liquidate everything so not to report a loss + _liquidatePosition(balanceOfCollateral()); + // Return since we dont asset to do anything else + return; + } + + _leveragePosition(_totalIdle); + } + /** * @notice Returns wether or not tend() should be called by a keeper. @@ -148,9 +298,41 @@ contract Strategy is BaseTokenizedStrategy { * This must be implemented if the strategy hopes to invoke _tend(). * * @return . Should return true if tend() should be called by keeper or false if not. - * - function tendTrigger() public view override returns (bool) {} - */ + */ + function tendTrigger() public view override returns (bool) { + // if we are in danger of being liquidated tend no matter what + if (comet.isLiquidatable(address(this))) return true; + + // we adjust position if: + // 1. LTV ratios are not in the HEALTHY range (either we take on more debt or repay debt) + // 2. costs are acceptable + uint256 collateralInUsd = _toUsd(balanceOfCollateral(), asset); + + // Nothing to rebalance if we do not have collateral locked + if (collateralInUsd == 0) return false; + + uint256 currentLTV = (_toUsd(balanceOfDebt(), baseToken) * 1e18) / + collateralInUsd; + uint256 targetLTV = _getTargetLTV(); + + // Check if we are over our warning LTV + if (currentLTV > _getWarningLTV()) { + // We have a higher tolerance for gas cost here since we are closer to liquidation + return + IBaseFeeGlobal(0xf8d0Ec04e94296773cE20eFbeeA82e76220cD549) + .basefee_global() <= maxGasPriceToTend; + } + + if ( + // WE NEED TO TAKE ON MORE DEBT (we need a 10p.p (1000bps) difference) + (currentLTV < targetLTV && targetLTV - currentLTV > 1e17) || + (getNetBorrowApr(0) > getNetRewardApr(0)) // UNHEALTHY BORROWING COSTS + ) { + return _isBaseFeeAcceptable(); + } + + return false; + } /** * @notice Gets the max amount of `asset` that an adress can deposit. @@ -172,45 +354,415 @@ contract Strategy is BaseTokenizedStrategy { * * @param . The address that is depositing into the strategy. * @return . The avialable amount the `_owner` can deposit in terms of `asset` - * + */ function availableDepositLimit( address _owner ) public view override returns (uint256) { - TODO: If desired Implement deposit limit logic and any needed state variables . - - EX: - uint256 totalAssets = TokenizedStrategy.totalAssets(); - return totalAssets >= depositLimit ? 0 : depositLimit - totalAssets; + return + uint256( + comet.getAssetInfoByAddress(asset).supplyCap - + comet.totalsCollateral(asset).totalSupplyAsset + ); } - */ - /** - * @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) { - TODO: If desired Implement withdraw limit logic and any needed state variables. - - EX: - return TokenizedStrategy.totalIdle(); + // ----------------- INTERNAL FUNCTIONS SUPPORT ----------------- \\ + + function _leveragePosition(uint256 _amount) internal { + // Cache variables + address _asset = asset; + address _baseToken = baseToken; + + // Could be 0 n tends. + if (_amount > 0) { + _supply(_asset, _amount); + } + + // NOTE: debt + collateral calcs are done in USD + uint256 collateralInUsd = _toUsd(balanceOfCollateral(), _asset); + + // convert debt to USD + uint256 debtInUsd = _toUsd(balanceOfDebt(), _baseToken); + + // LTV numbers are always in 1e18 + uint256 currentLTV = (debtInUsd * 1e18) / collateralInUsd; + uint256 targetLTV = _getTargetLTV(); // 70% under default liquidation Threshold + + // decide in which range we are and act accordingly: + // SUBOPTIMAL(borrow) (e.g. from 0 to 70% liqLTV) + // HEALTHY(do nothing) (e.g. from 70% to 80% liqLTV) + // UNHEALTHY(repay) (e.g. from 80% to 100% liqLTV) + + if (targetLTV > currentLTV) { + // SUBOPTIMAL RATIO: our current Loan-to-Value is lower than what we want + // AND costs are lower than our max acceptable costs + + // we need to take on more debt + uint256 targetDebtUsd = (collateralInUsd * targetLTV) / 1e18; + + uint256 amountToBorrowUsd = targetDebtUsd - debtInUsd; // safe bc we checked ratios + // convert to BaseToken + uint256 amountToBorrowBT = _fromUsd(amountToBorrowUsd, _baseToken); + + // We want to make sure that the reward apr > borrow apr so we dont reprot a loss + // Borrowing will cause the borrow apr to go up and the rewards apr to go down + if ( + getNetBorrowApr(amountToBorrowBT) > + getNetRewardApr(amountToBorrowBT) + ) { + // If we would push it over the limit dont borrow anything + amountToBorrowBT = 0; + } + + // Need to have at least the min set by comet + if (balanceOfDebt() + amountToBorrowBT > minThreshold) { + _withdraw(baseToken, amountToBorrowBT); + } + } else if (currentLTV > _getWarningLTV()) { + // UNHEALTHY RATIO + // we repay debt to set it to targetLTV + uint256 targetDebtUsd = (targetLTV * collateralInUsd) / 1e18; + + // Withdraw the difference from the Depositer + _withdrawFromDepositer( + _fromUsd(debtInUsd - targetDebtUsd, _baseToken) + ); // we withdraw from BaseToken depositer + _repayTokenDebt(); // we repay the BaseToken debt with compound + } + + if (balanceOfBaseToken() > 0) { + depositer.deposit(); + } + } + + function _liquidatePosition(uint256 _needed) internal { + // NOTE: amountNeeded is in asset + // NOTE: repayment amount is in BaseToken + // NOTE: collateral and debt calcs are done in USD + + // Cache balance for withdraw checks + uint256 balance = balanceOfAsset(); + + // Accrue account for accurate balances + comet.accrueAccount(address(this)); + + // We first repay whatever we need to repay to keep healthy ratios + _withdrawFromDepositer(_calculateAmountToRepay(_needed)); + + // we repay the BaseToken debt with the amount withdrawn from the vault + _repayTokenDebt(); + + // Withdraw as much as we can up to the amount needed while maintaning a health ltv + _withdraw(asset, Math.min(_needed, _maxWithdrawal())); + + // it will return the free amount of asset + uint256 withdrawn = balanceOfAsset() - balance; + // we check if we withdrew less than expected, we have not more baseToken + // left AND should harvest or buy BaseToken with asset (potentially realising losses) + if ( + _needed > withdrawn && // if we didn't get enough + balanceOfDebt() > 0 && // still some debt remaining + balanceOfDepositer() == 0 && // but no capital to repay + !leaveDebtBehind // if set to true, the strategy will not try to repay debt by selling asset + ) { + // using this part of code may result in losses but it is necessary to unlock full collateral in case of wind down + // This should only occur when depleting the strategy so we asset to swap the full amount of our debt + // we buy BaseToken first with available rewards then with asset + _buyBaseToken(); + + // we repay debt to actually unlock collateral + // after this, balanceOfDebt should be 0 + _repayTokenDebt(); + + // then we try withdraw once more + // still withdraw with target LTV since management can potentially save any left over manually + _withdraw(asset, _maxWithdrawal()); + } + } + + function _withdrawFromDepositer(uint256 _amountBT) internal { + uint256 balancePrior = balanceOfBaseToken(); + // Only withdraw what we dont already have free + _amountBT = balancePrior >= _amountBT ? 0 : _amountBT - balancePrior; + if (_amountBT == 0) return; + + // Make sure we have enough balance. This accrues the account first. + _amountBT = Math.min(_amountBT, depositer.accruedCometBalance()); + // need to check liquidity of the comet + _amountBT = Math.min( + _amountBT, + ERC20(baseToken).balanceOf(address(comet)) + ); + + depositer.withdraw(_amountBT); + } + + /* + * Supply an asset that this contract holds to Compound III + * This is used both to supply collateral as well as the baseToken + */ + function _supply(address _asset, uint256 amount) internal { + if (amount == 0) return; + comet.supply(_asset, amount); + } + + /* + * Withdraws an _asset from Compound III to this contract + * for both collateral and borrowing baseToken + */ + function _withdraw(address _asset, uint256 amount) internal { + if (amount == 0) return; + comet.withdraw(_asset, amount); + } + + function _repayTokenDebt() internal { + // we cannot pay more than loose balance or more than we owe + _supply(baseToken, Math.min(balanceOfBaseToken(), balanceOfDebt())); + } + + function _maxWithdrawal() internal view returns (uint256) { + uint256 collateralInUsd = _toUsd(balanceOfCollateral(), asset); + uint256 debtInUsd = _toUsd(balanceOfDebt(), baseToken); + + // If there is no debt we can withdraw everything + if (debtInUsd == 0) return balanceOfCollateral(); + + // What we need to maintain a health LTV + uint256 neededCollateralUsd = (debtInUsd * 1e18) / _getTargetLTV(); + // We need more collateral so we cant withdraw anything + if (neededCollateralUsd > collateralInUsd) { + return 0; + } + // Return the difference in terms of asset + return _fromUsd(collateralInUsd - neededCollateralUsd, asset); + } + + function _calculateAmountToRepay( + uint256 amount + ) internal view returns (uint256) { + if (amount == 0) return 0; + uint256 collateral = balanceOfCollateral(); + // to unlock all collateral we must repay all the debt + if (amount >= collateral) return balanceOfDebt(); + + // we check if the collateral that we are withdrawing leaves us in a risky range, we then take action + uint256 newCollateralUsd = _toUsd(collateral - amount, asset); + + uint256 targetDebtUsd = (newCollateralUsd * _getTargetLTV()) / 1e18; + uint256 targetDebt = _fromUsd(targetDebtUsd, baseToken); + uint256 currentDebt = balanceOfDebt(); + // Repay only if our target debt is lower than our current debt + return targetDebt < currentDebt ? currentDebt - targetDebt : 0; + } + + // ----------------- INTERNAL CALCS ----------------- + + // Returns the _amount of _token in terms of USD, i.e 1e8 + function _toUsd( + uint256 _amount, + address _token + ) internal view returns (uint256) { + if (_amount == 0) return _amount; + // usd price is returned as 1e8 + unchecked { + return + (_amount * getCompoundPrice(_token)) / + (10 ** ERC20(_token).decimals()); + } + } + + // Returns the _amount of usd (1e8) in terms of _token + function _fromUsd( + uint256 _amount, + address _token + ) internal view returns (uint256) { + if (_amount == 0) return _amount; + unchecked { + return + (_amount * (10 ** ERC20(_token).decimals())) / + getCompoundPrice(_token); + } + } + + function balanceOfAsset() public view returns (uint256) { + return ERC20(asset).balanceOf(address(this)); + } + + function balanceOfCollateral() public view returns (uint256) { + return uint256(comet.userCollateral(address(this), asset).balance); + } + + function balanceOfBaseToken() public view returns (uint256) { + return ERC20(baseToken).balanceOf(address(this)); + } + + function balanceOfDepositer() public view returns (uint256) { + return depositer.cometBalance(); + } + + function balanceOfDebt() public view returns (uint256) { + return comet.borrowBalanceOf(address(this)); + } + + // Returns the negative position of base token. i.e. borrowed - supplied + // if supplied is higher it will return 0 + function baseTokenOwedBalance() public view returns (uint256) { + uint256 supplied = balanceOfDepositer(); + uint256 borrowed = balanceOfDebt(); + uint256 loose = balanceOfBaseToken(); + + // If they are the same or supply > debt return 0 + if (supplied + loose >= borrowed) return 0; + + unchecked { + return borrowed - supplied - loose; + } + } + + function baseTokenOwedInAsset() internal view returns (uint256) { + return _fromUsd(_toUsd(baseTokenOwedBalance(), baseToken), asset); + } + + function rewardsInAsset() public view returns (uint256) { + // underreport by 10% for safety + return + (_fromUsd(_toUsd(depositer.getRewardsOwed(), comp), asset) * + 9_000) / MAX_BPS; + } + + // We put the logic for these APR functions in the depositer contract to save byte code in the main strategy \\ + function getNetBorrowApr(uint256 newAmount) public view returns (uint256) { + return depositer.getNetBorrowApr(newAmount); + } + + function getNetRewardApr(uint256 newAmount) public view returns (uint256) { + return depositer.getNetRewardApr(newAmount); + } + + /* + * Get the liquidation collateral factor for an asset + */ + function getLiquidateCollateralFactor() public view returns (uint256) { + return + uint256( + comet.getAssetInfoByAddress(asset).liquidateCollateralFactor + ); + } + + /* + * Get the price feed address for an asset + */ + function getPriceFeedAddress( + address _asset + ) internal view returns (address priceFeed) { + priceFeed = priceFeeds[_asset]; + if (priceFeed == address(0)) { + priceFeed = comet.getAssetInfoByAddress(_asset).priceFeed; + } + } + + /* + * Get the current price of an _asset from the protocol's persepctive + */ + function getCompoundPrice( + address _asset + ) internal view returns (uint256 price) { + price = comet.getPrice(getPriceFeedAddress(_asset)); + // If weth is base token we need to scale response to e18 + if (price == 1e8 && _asset == base) price = 1e18; + } + + // External function used to easisly calculate the current LTV of the strat + function getCurrentLTV() external view returns (uint256) { + unchecked { + return + (_toUsd(balanceOfDebt(), baseToken) * 1e18) / + _toUsd(balanceOfCollateral(), asset); + } + } + + function _getTargetLTV() internal view returns (uint256) { + unchecked { + return + (getLiquidateCollateralFactor() * targetLTVMultiplier) / + MAX_BPS; + } + } + + function _getWarningLTV() internal view returns (uint256) { + unchecked { + return + (getLiquidateCollateralFactor() * warningLTVMultiplier) / + MAX_BPS; + } + } + + // ----------------- HARVEST / TOKEN CONVERSIONS ----------------- + + function claimRewards() external onlyKeepers { + _claimRewards(); + } + + function _claimRewards() internal { + rewardsContract.claim(address(comet), address(this), true); + // Pull rewards from depositer even if not incentivised to accrue the account + depositer.claimRewards(); + } + + function _claimAndSellRewards() internal { + _claimRewards(); + + address _comp = comp; + + uint256 compBalance = ERC20(_comp).balanceOf(address(this)); + + uint256 baseNeeded = baseTokenOwedBalance(); + + if (baseNeeded > 0) { + address _baseToken = baseToken; + // 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); + } else { + // if not swap everything we have + _swapFrom(_comp, _baseToken, compBalance, 0); + } + } + + compBalance = ERC20(_comp).balanceOf(address(this)); + _swapFrom(_comp, asset, compBalance, 0); + } + + // This should only ever get called when withdrawing all funds from the strategy if there is debt left over. + // It will first try and sell rewards for the needed amount of base token. then will swap asset + // Using this in a normal withdraw can cause it to be sandwhiched which is why we use rewards first + function _buyBaseToken() internal { + // We should be able to get the needed amount from rewards tokens. + // We first try that before swapping asset and reporting losses. + _claimAndSellRewards(); + + uint256 baseStillOwed = baseTokenOwedBalance(); + // Check if our debt balance is still greater than our base token balance + if (baseStillOwed > 0) { + // Need to account for both slippage and diff in the oracle price. + // Should be only swapping very small amounts so its just to make sure there is no massive sandwhich + uint256 maxAssetBalance = (_fromUsd( + _toUsd(baseStillOwed, baseToken), + asset + ) * 10_500) / MAX_BPS; + // Under 10 can cause rounding errors from token conversions, no need to swap that small amount + if (maxAssetBalance <= 10) return; + + _swapFrom(asset, baseToken, baseStillOwed, maxAssetBalance); + } + } + + function _isBaseFeeAcceptable() internal view returns (bool) { + return true; } - */ /** * @dev Optional function for a strategist to override that will @@ -232,14 +784,11 @@ contract Strategy is BaseTokenizedStrategy { * } * * @param _amount The amount of asset to attempt to free. - * + */ function _emergencyWithdraw(uint256 _amount) internal override { - TODO: If desired implement simple logic to free deployed funds. - - EX: - _amount = min(_amount, atoken.balanceOf(address(this))); - lendingPool.withdraw(asset, _amount); + if (_amount > 0) { + depositer.withdraw(_amount); + } + _repayTokenDebt(); } - - */ } diff --git a/src/TokenizedCompV3LenderBorrowerFactory.sol b/src/TokenizedCompV3LenderBorrowerFactory.sol new file mode 100644 index 0000000..a4937d1 --- /dev/null +++ b/src/TokenizedCompV3LenderBorrowerFactory.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.18; + +import "./Depositer.sol"; +import "./Strategy.sol"; + +import {IStrategyInterface} from "./interfaces/IStrategyInterface.sol"; + +contract TokenizedCompV3LenderBorrowerFactory { + + address public immutable originalDepositer; + address public immutable managment; + address public immutable rewards; + address public immutable keeper; + + event Deployed(address indexed depositer, address indexed strategy); + + constructor( + address _managment, + address _rewards, + address _keeper + ) { + managment = _managment; + rewards = _rewards; + keeper = _keeper; + + originalDepositer = address(new Depositer()); + } + + function name() external pure returns (string memory) { + return "Yearnv3-TokeinzedCompV3LenderBorrowerFactory"; + } + + function newCompV3LenderBorrower( + address _asset, + string memory _name, + address _comet, + uint24 _ethToAssetFee + ) external returns (address, address) { + Depositer depositer = new Depositer(); + depositer.initialize(_comet); + + // Need to give the address the correct interface. + IStrategyInterface strategy = IStrategyInterface( + address(new Strategy( + _asset, + _name, + _comet, + _ethToAssetFee, + address(depositer) + )) + ); + + // Set strategy on Depositer. + depositer.setStrategy(address(strategy)); + + // Set the initial Strategy Params. + strategy.setStrategyParams( + 7_000, // targetLTVMultiplier (default: 7_000) + 8_000, // warningLTVMultiplier default: 8_000 + 1e10, // min rewards to sell + false, // leave debt behind (default: false) + 40 * 1e9 // max base fee to perform non-emergency tends (default: 40 gwei) + ); + + // Set the addresses. + strategy.setPerformanceFeeRecipient(rewards); + strategy.setKeeper(keeper); + strategy.setPendingManagement(managment); + + emit Deployed(address(depositer), address(strategy)); + return (address(depositer), address(strategy)); + } +} \ No newline at end of file diff --git a/src/interfaces/Compound/V3/CompoundV3.sol b/src/interfaces/Compound/V3/CompoundV3.sol new file mode 100644 index 0000000..4f8f45b --- /dev/null +++ b/src/interfaces/Compound/V3/CompoundV3.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.12; +pragma experimental ABIEncoderV2; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +library CometStructs { + struct AssetInfo { + uint8 offset; + address asset; + address priceFeed; + uint64 scale; + uint64 borrowCollateralFactor; + uint64 liquidateCollateralFactor; + uint64 liquidationFactor; + uint128 supplyCap; + } + + struct UserBasic { + int104 principal; + uint64 baseTrackingIndex; + uint64 baseTrackingAccrued; + uint16 assetsIn; + uint8 _reserved; + } + + struct TotalsBasic { + uint64 baseSupplyIndex; + uint64 baseBorrowIndex; + uint64 trackingSupplyIndex; + uint64 trackingBorrowIndex; + uint104 totalSupplyBase; + uint104 totalBorrowBase; + uint40 lastAccrualTime; + uint8 pauseFlags; + } + + struct UserCollateral { + uint128 balance; + uint128 _reserved; + } + + struct RewardOwed { + address token; + uint256 owed; + } + + struct TotalsCollateral { + uint128 totalSupplyAsset; + uint128 _reserved; + } + + struct RewardConfig { + address token; + uint64 rescaleFactor; + bool shouldUpscale; + } +} + +interface Comet is IERC20 { + function baseScale() external view returns (uint256); + + function supply(address asset, uint256 amount) external; + + function supplyTo(address to, address asset, uint256 amount) external; + + function withdraw(address asset, uint256 amount) external; + + function getSupplyRate(uint256 utilization) external view returns (uint256); + + function getBorrowRate(uint256 utilization) external view returns (uint256); + + function getAssetInfoByAddress( + address asset + ) external view returns (CometStructs.AssetInfo memory); + + function getAssetInfo( + uint8 i + ) external view returns (CometStructs.AssetInfo memory); + + function borrowBalanceOf(address account) external view returns (uint256); + + function getPrice(address priceFeed) external view returns (uint128); + + function userBasic( + address + ) external view returns (CometStructs.UserBasic memory); + + function totalsBasic() + external + view + returns (CometStructs.TotalsBasic memory); + + function userCollateral( + address, + address + ) external view returns (CometStructs.UserCollateral memory); + + function baseTokenPriceFeed() external view returns (address); + + function numAssets() external view returns (uint8); + + function getUtilization() external view returns (uint256); + + function baseTrackingSupplySpeed() external view returns (uint256); + + function baseTrackingBorrowSpeed() external view returns (uint256); + + function totalSupply() external view override returns (uint256); + + function totalBorrow() external view returns (uint256); + + function baseIndexScale() external pure returns (uint64); + + function baseTrackingAccrued( + address account + ) external view returns (uint64); + + function totalsCollateral( + address asset + ) external view returns (CometStructs.TotalsCollateral memory); + + function baseMinForRewards() external view returns (uint256); + + function baseToken() external view returns (address); + + function accrueAccount(address account) external; + + function isLiquidatable(address _address) external view returns (bool); + + function baseBorrowMin() external view returns (uint256); +} + +interface CometRewards { + function getRewardOwed( + address comet, + address account + ) external returns (CometStructs.RewardOwed memory); + + function claim(address comet, address src, bool shouldAccrue) external; + + function rewardsClaimed( + address comet, + address account + ) external view returns (uint256); + + function rewardConfig( + address comet + ) external view returns (CometStructs.RewardConfig memory); +} \ No newline at end of file diff --git a/src/interfaces/IStrategyInterface.sol b/src/interfaces/IStrategyInterface.sol index de2f38c..c649959 100644 --- a/src/interfaces/IStrategyInterface.sol +++ b/src/interfaces/IStrategyInterface.sol @@ -2,7 +2,24 @@ pragma solidity 0.8.18; import {IStrategy} from "@tokenized-strategy/interfaces/IStrategy.sol"; +import {IUniswapV3Swapper} from "@periphery/swappers/interfaces/IUniswapV3Swapper.sol"; -interface IStrategyInterface is IStrategy { - //TODO: Add your specific implementation interface in here. +interface IStrategyInterface is IStrategy, IUniswapV3Swapper { + function baseToken() external view returns (address); + + function depositer() external view returns (address); + + function setStrategyParams( + uint16 _targetLTVMultiplier, + uint16 _warningLTVMultiplier, + uint256 _minToSell, + bool _leaveDebtBehind, + uint256 _maxGasPriceToTend + ) external; + + function initializeCompV3LenderBorrower( + address _comet, + uint24 _ethToAssetFee, + address _depositer + ) external; } diff --git a/src/test/Operation.t.sol b/src/test/Operation.t.sol index b3e01c0..87feb67 100644 --- a/src/test/Operation.t.sol +++ b/src/test/Operation.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.18; import "forge-std/console.sol"; -import {Setup} from "./utils/Setup.sol"; +import {Setup, IStrategyInterface} from "./utils/Setup.sol"; contract OperationTest is Setup { function setUp() public override { @@ -25,8 +25,7 @@ contract OperationTest is Setup { // Deposit into strategy mintAndDepositIntoStrategy(strategy, user, _amount); - // TODO: Implement logic so totalDebt is _amount and totalIdle = 0. - checkStrategyTotals(strategy, _amount, 0, _amount); + checkStrategyTotals(strategy, _amount, _amount, 0); // Earn Interest skip(1 days); @@ -64,8 +63,7 @@ contract OperationTest is Setup { // Deposit into strategy mintAndDepositIntoStrategy(strategy, user, _amount); - // TODO: Implement logic so totalDebt is _amount and totalIdle = 0. - checkStrategyTotals(strategy, _amount, 0, _amount); + checkStrategyTotals(strategy, _amount, _amount, 0); // Earn Interest skip(1 days); @@ -110,8 +108,7 @@ contract OperationTest is Setup { // Deposit into strategy mintAndDepositIntoStrategy(strategy, user, _amount); - // TODO: Implement logic so totalDebt is _amount and totalIdle = 0. - checkStrategyTotals(strategy, _amount, 0, _amount); + checkStrategyTotals(strategy, _amount, _amount, 0); // Earn Interest skip(1 days); diff --git a/src/test/Shutdown.t.sol b/src/test/Shutdown.t.sol index 72aa39c..346d3dd 100644 --- a/src/test/Shutdown.t.sol +++ b/src/test/Shutdown.t.sol @@ -14,8 +14,7 @@ contract ShutdownTest is Setup { // Deposit into strategy mintAndDepositIntoStrategy(strategy, user, _amount); - // TODO: Implement logic so totalDebt is _amount and totalIdle = 0. - checkStrategyTotals(strategy, _amount, 0, _amount); + checkStrategyTotals(strategy, _amount, _amount, 0); // Earn Interest skip(1 days); @@ -24,8 +23,7 @@ contract ShutdownTest is Setup { vm.prank(management); strategy.shutdownStrategy(); - // TODO: Implement logic so totalDebt is _amount and totalIdle = 0. - checkStrategyTotals(strategy, _amount, 0, _amount); + checkStrategyTotals(strategy, _amount, _amount, 0); // Make sure we can still withdraw the full amount uint256 balanceBefore = asset.balanceOf(user); diff --git a/src/test/utils/Setup.sol b/src/test/utils/Setup.sol index 023a1d5..0942c56 100644 --- a/src/test/utils/Setup.sol +++ b/src/test/utils/Setup.sol @@ -12,6 +12,10 @@ import {IStrategyInterface} from "../../interfaces/IStrategyInterface.sol"; // Inherit the events so they can be checked if desired. import {IEvents} from "@tokenized-strategy/interfaces/IEvents.sol"; +import {TokenizedCompV3LenderBorrowerFactory} from "../../TokenizedCompV3LenderBorrowerFactory.sol"; +import {Depositer} from "../../Depositer.sol"; +import {Strategy} from "../../Strategy.sol"; + interface IFactory { function governance() external view returns (address); @@ -24,8 +28,14 @@ contract Setup is ExtendedTest, IEvents { // Contract instancees that we will use repeatedly. ERC20 public asset; IStrategyInterface public strategy; + TokenizedCompV3LenderBorrowerFactory public strategyFactory; + Depositer public depositer; mapping(string => address) public tokenAddrs; + mapping(string => address) public comets; + + address public comet; + uint24 public ethToAssetFee; // Addresses for different roles we will use repeatedly. address public user = address(10); @@ -41,8 +51,8 @@ contract Setup is ExtendedTest, IEvents { uint256 public MAX_BPS = 10_000; // Fuzz from $0.01 of 1e6 stable coins up to 1 trillion of a 1e18 coin - uint256 public maxFuzzAmount = 1e30; - uint256 public minFuzzAmount = 10_000; + uint256 public maxFuzzAmount = 1e10; + uint256 public minFuzzAmount = 100_000; // Default prfot max unlock time is set for 10 days uint256 public profitMaxUnlockTime = 10 days; @@ -51,7 +61,9 @@ contract Setup is ExtendedTest, IEvents { _setTokenAddrs(); // Set asset - asset = ERC20(tokenAddrs["DAI"]); + asset = ERC20(tokenAddrs["WBTC"]); + comet = comets["USDC"]; + ethToAssetFee = 3_000; // Set decimals decimals = asset.decimals(); @@ -67,21 +79,17 @@ contract Setup is ExtendedTest, IEvents { vm.label(address(asset), "asset"); vm.label(management, "management"); vm.label(address(strategy), "strategy"); + vm.label(address(depositer), "Depositer"); + vm.label(address(strategyFactory), "Strategy Factory"); vm.label(performanceFeeRecipient, "performanceFeeRecipient"); } function setUpStrategy() public returns (address) { - // we save the strategy as a IStrategyInterface to give it the needed interface - IStrategyInterface _strategy = IStrategyInterface( - address(new Strategy(address(asset), "Tokenized Strategy")) - ); + strategyFactory = new TokenizedCompV3LenderBorrowerFactory(management, performanceFeeRecipient, keeper); - // set keeper - _strategy.setKeeper(keeper); - // set treasury - _strategy.setPerformanceFeeRecipient(performanceFeeRecipient); - // set management of the strategy - _strategy.setPendingManagement(management); + (address _depoister, address strategy_) = strategyFactory.newCompV3LenderBorrower(address(asset), "Test Lender Borrower", comet, ethToAssetFee); + // we save the strategy as a IStrategyInterface to give it the needed interface + IStrategyInterface _strategy = IStrategyInterface(strategy_); vm.prank(management); _strategy.acceptManagement(); @@ -150,5 +158,7 @@ contract Setup is ExtendedTest, IEvents { tokenAddrs["USDT"] = 0xdAC17F958D2ee523a2206206994597C13D831ec7; tokenAddrs["DAI"] = 0x6B175474E89094C44Da98b954EedeAC495271d0F; tokenAddrs["USDC"] = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + comets["WETH"] = 0xA17581A9E3356d9A858b789D68B4d866e593aE94; + comets["USDC"] = 0xc3d688B66703497DAA19211EEdff47f25384cdc3; } }