diff --git a/src/interfaces/external/Compound/CometMainInterface.sol b/src/interfaces/external/Compound/CometMainInterface.sol index 556f6913f..84dcdbcd9 100644 --- a/src/interfaces/external/Compound/CometMainInterface.sol +++ b/src/interfaces/external/Compound/CometMainInterface.sol @@ -9,11 +9,6 @@ import "./CometCore.sol"; * @author Compound */ abstract contract CometMainInterface is CometCore { - struct UserCollateral { - uint128 balance; - uint128 _reserved; - } - error Absurd(); error AlreadyInitialized(); error BadAsset(); @@ -210,5 +205,12 @@ abstract contract CometMainInterface is CometCore { function initializeStorage() external virtual; // extra functions to access public vars as needed for Sommelier integration + + /// @notice Mapping of users to collateral data per collateral asset + /// @dev See CometStorage.sol for struct UserCollateral function userCollateral(address, address) external returns (UserCollateral); + + /// @notice Mapping of users to base principal and other basic data + /// @dev See CometStorage.sol for struct UserBasic + function userBasic(address) external returns (UserBasic); } diff --git a/src/interfaces/external/Compound/External/IPriceFeed.sol b/src/interfaces/external/Compound/External/IPriceFeed.sol new file mode 100644 index 000000000..ff6b42d32 --- /dev/null +++ b/src/interfaces/external/Compound/External/IPriceFeed.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +/** + * @dev Interface for price feeds used by Comet + * Note This is Chainlink's AggregatorV3Interface, but without the `getRoundData` function. + */ +interface IPriceFeed { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} \ No newline at end of file diff --git a/src/modules/adaptors/Compound/v3/CompoundV3CollateralAdaptor.sol b/src/modules/adaptors/Compound/v3/CompoundV3CollateralAdaptor.sol index 8378fd010..45885d154 100644 --- a/src/modules/adaptors/Compound/v3/CompoundV3CollateralAdaptor.sol +++ b/src/modules/adaptors/Compound/v3/CompoundV3CollateralAdaptor.sol @@ -47,7 +47,7 @@ contract CompoundV3CollateralAdaptor is BaseAdaptor, CompoundHealthFactorLogic { uint256 public immutable minimumHealthFactor; // TODO: might need a mapping of health factors for different assets because Compound accounts have different HFs for different assets (collateral). If this is needed it would be more-so for the internal calcs we do to ensure that any collateral adjustments don't affect the `minimumHealthFactor` which is a buffer above the minHealthFactor from Compound itself. - constructor(bool _accountForInterest, uint256 _healthFactor) { + constructor(bool _accountForInterest, uint256 _healthFactor) CompoundV3ExtraLogic(_healthFactor) { ACCOUNT_FOR_INTEREST = _accountForInterest; _verifyConstructorMinimumHealthFactor(_healthFactor); minimumHealthFactor = _healthFactor; @@ -155,7 +155,8 @@ contract CompoundV3CollateralAdaptor is BaseAdaptor, CompoundHealthFactorLogic { // withdraw collateral _compMarket.withdraw(address(_asset), _amount); // Collateral adjustment is checked against `isBorrowCollateralized(src)` in CompoundV3 and will revert if uncollateralized result. See `withdrawCollateral()` for more context in `Comet.sol` - // TODO: add logic (incl. helper functions likely in HealthFactorLogic.sol) to calculate the new CR with this adjustment to compare against the `minimumHealthFactor` which should be higher than the minHealthFactor_CompMarket + // Check if cellar account is unsafe after this collateral withdrawal tx, revert if they are + if (_checkLiquidity(_compMarket) < 0) + revert CompoundV3CollateralAdaptor__HealthFactorTooLow(address(_compMarket)); } - } diff --git a/src/modules/adaptors/Compound/v3/CompoundV3DebtAdaptor.sol b/src/modules/adaptors/Compound/v3/CompoundV3DebtAdaptor.sol index e07319da9..4d0da47a4 100644 --- a/src/modules/adaptors/Compound/v3/CompoundV3DebtAdaptor.sol +++ b/src/modules/adaptors/Compound/v3/CompoundV3DebtAdaptor.sol @@ -12,6 +12,7 @@ import { CompoundV3ExtraLogic } from "src/modules/adaptors/Compound/v3/CompoundV * @notice Allows Cellars to borrow assets from CompoundV3 Lending Markets. * @author crispymangoes, 0xEinCodes * NOTE: In efforts to keep the smart contracts simple, the three main services for accounts with CompoundV3; supplying `BaseAssets`, supplying `Collateral`, and `Borrowing` against `Collateral` are isolated to three separate adaptor contracts. Therefore, repayment of open `borrow` positions are done within this adaptor, and cannot be carried out through the use of `CompoundV3SupplyAdaptor`. + * TODO: health factor logic. Adaptor checks the resulting health factor of the account position within CompoundV3 Lending Market after: increasing borrow, or decreasing collateral. */ contract CompoundV3DebtAdaptor is BaseAdaptor, CompoundV3ExtraLogic { using SafeTransferLib for ERC20; @@ -54,7 +55,7 @@ contract CompoundV3DebtAdaptor is BaseAdaptor, CompoundV3ExtraLogic { */ uint256 public immutable minimumHealthFactor; - constructor(bool _accountForInterest, uint256 _healthFactor) { + constructor(bool _accountForInterest, uint256 _healthFactor) CompoundV3ExtraLogic(_healthFactor) { _verifyConstructorMinimumHealthFactor(_healthFactor); ACCOUNT_FOR_INTEREST = _accountForInterest; minimumHealthFactor = _healthFactor; @@ -102,7 +103,8 @@ contract CompoundV3DebtAdaptor is BaseAdaptor, CompoundV3ExtraLogic { function balanceOf(bytes memory adaptorData) public view override returns (uint256) { (CometInterface compMarket, ERC20 asset) = abi.decode(adaptorData, (CometInterface, ERC20)); - _validateCompMarketAndAsset(compMarket, asset); + _validateCompMarket(compMarket); + return compMarket.borrowBalanceOf(address(this)); // RETURNS: The balance of the base asset, including interest, borrowed by the specified account as an unsigned integer scaled up by 10 to the “decimals” integer in the asset’s contract. TODO: assess how we need to work with this return value, decimals-wise. } @@ -132,8 +134,9 @@ contract CompoundV3DebtAdaptor is BaseAdaptor, CompoundV3ExtraLogic { * NOTE: need to take the higher value btw minBorrowAmount and amountToBorrow or else will revert */ function borrowFromCompoundV3(CometInterface _compMarket, uint256 amountToBorrow) public { - _validateCompMarketAndAsset(compMarket, asset); // TODO: fix - see other TODOs here re: health factor calcs. - ERC20 baseToken = ERC20(compMarket.baseToken()); + _validateCompMarket(_compMarket); + // TODO: check that there is an active collateral position? Check that the resulting borrow wouldn't result in a liquidatable position? EIN - I think we do it after mutative part of the tx cause then we can query what we need from the compound contracts vs incorrectly replicating it. + ERC20 baseToken = ERC20(_compMarket.baseToken()); // TODO: do we want to have conditional logic handle when the strategist passes in `type(uint256).max`? @@ -142,11 +145,12 @@ contract CompoundV3DebtAdaptor is BaseAdaptor, CompoundV3ExtraLogic { amountToBorrow = minBorrowAmount > amountToBorrow ? minBorrowAmount : amountToBorrow; - // TODO: do we want to compare requested `amountToBorrow` against what is allowed to be borrowed? + // TODO: do we want to compare requested `amountToBorrow` against what is allowed to be borrowed? We need to check how Compound V3 lending markets go about this. If there's not enough being supplied, do they just give you what is possible or...? - compMarket.withdraw(address(baseToken), amountToBorrow); + _compMarket.withdraw(address(baseToken), amountToBorrow); - // TODO: Health Factor logic implementation. + // Check if borrower is insolvent after this borrow tx, revert if they are + if (_checkLiquidity(_compMarket) < 0) revert CompoundV3DebtAdaptor_HealthFactorTooLow(address(_compMarket)); } // `repayDebt` @@ -157,8 +161,8 @@ contract CompoundV3DebtAdaptor is BaseAdaptor, CompoundV3ExtraLogic { * @param _compMarket the Fraxlend Pair to repay debt from. * @param _debtTokenRepayAmount the amount of `debtToken` (`baseAsset`) to repay with. */ - function repayFraxlendDebt(CometInterface _compMarket, uint256 _debtTokenRepayAmount) public { - _validateCompMarketAndAsset(_compMarket, asset); // TODO: fix - see other TODOs here re: health factor calcs. + function repayCompoundV3Debt(CometInterface _compMarket, uint256 _debtTokenRepayAmount) public { + _validateCompMarket(_compMarket); // TODO: fix - see other TODOs here re: health factor calcs. ERC20 baseToken = ERC20(compMarket.baseToken()); _debtTokenRepayAmount = _maxAvailable(baseToken, _debtTokenRepayAmount); // TODO: check what happens when one tries to repay more than what they owe in CompoundV3, and what happens if they try to repay on an account that shows zero for their collateral, or for their loan? @@ -176,90 +180,4 @@ contract CompoundV3DebtAdaptor is BaseAdaptor, CompoundV3ExtraLogic { _revokeExternalApproval(tokenToRepay, address(_compMarket)); } - - //============================================ Interface Helper Functions =========================================== - - //============================== Interface Details ============================== - // The Frax Pair interface can slightly change between versions. - // To account for this, FTokenAdaptors (including debt and collateral adaptors) will use the below internal functions when - // interacting with Frax Pairs, this way new pairs can be added by creating a - // new contract that inherits from this one, and overrides any function it needs - // so it conforms with the new Frax Pair interface. - - // Current versions in use for `Fraxlend Pair` include v1 and v2. - - // IMPORTANT: This `DebtFTokenAdaptor.sol` is associated to the v2 version of `Fraxlend Pair` - // whereas DebtFTokenAdaptorV1 is actually associated to `FraxLendPairv1`. - // The reasoning to name it like this was to set up the base DebtFTokenAdaptor for the - // most current version, v2. This is in anticipation that more FraxLendPairs will - // be deployed following v2 in the near future. When later versions are deployed, - // then the described inheritance pattern above will be used. - - // NOTE: FraxlendHealthFactorLogic.sol has helper functions used for both v1 and v2 fraxlend pairs (`_getHealthFactor()`). - // This function has a helper `_toBorrowAmount()` that corresponds to v2 by default, but is virtual and overwritten for - // fraxlendV1 pairs as seen in Collateral and Debt adaptors for v1 pairs. - //=============================================================================== - - /** - * @notice gets the asset of the specified fraxlend pair - * @param _fraxlendPair The specified Fraxlend Pair - * @return asset of fraxlend pair - */ - function _fraxlendPairAsset(IFToken _fraxlendPair) internal view virtual returns (address asset) { - return _fraxlendPair.asset(); - } - - /** - * @notice Caller calls `addInterest` on specified 'v2' Fraxlend Pair - * @dev fraxlendPair.addInterest() calls into the respective version (v2 by default) of Fraxlend Pair - * @param fraxlendPair The specified Fraxlend Pair - */ - function _addInterest(IFToken fraxlendPair) internal virtual { - fraxlendPair.addInterest(false); - } - - /** - * @notice Converts a given asset amount to a number of asset shares (fTokens) from specified 'v2' Fraxlend Pair - * @dev This is one of the adjusted functions from v1 to v2. ftoken.toAssetShares() calls into the respective version (v2 by default) of Fraxlend Pair - * @param fToken The specified Fraxlend Pair - * @param amount The amount of asset - * @param roundUp Whether to round up after division - * @param previewInterest Whether to preview interest accrual before calculation - * @return number of asset shares - */ - function _toAssetShares( - IFToken fToken, - uint256 amount, - bool roundUp, - bool previewInterest - ) internal view virtual returns (uint256) { - return fToken.toAssetShares(amount, roundUp, previewInterest); - } - - /** - * @notice Borrow amount of borrowAsset in cellar account within fraxlend pair - * @param _borrowAmount The amount of borrowAsset to borrow - * @param _fraxlendPair The specified Fraxlend Pair - */ - function _borrowAsset(uint256 _borrowAmount, IFToken _fraxlendPair) internal virtual { - _fraxlendPair.borrowAsset(_borrowAmount, 0, address(this)); // NOTE: explitly have the collateral var as zero so Strategists must do collateral increasing tx via the CollateralFTokenAdaptor for this fraxlendPair - } - - /** - * @notice Caller calls `updateExchangeRate()` on specified FraxlendV2 Pair - * @param _fraxlendPair The specified FraxLendPair - * @return exchangeRate needed to calculate the current health factor - */ - function _getExchangeRateInfo(IFToken _fraxlendPair) internal virtual returns (uint256 exchangeRate) { - exchangeRate = _fraxlendPair.exchangeRateInfo().highExchangeRate; - } - - /** - * @notice Repay fraxlend pair debt by an amount - * @param _fraxlendPair The specified Fraxlend Pair - * @param sharesToRepay The amount of shares to repay - */ - function _repayAsset(IFToken _fraxlendPair, uint256 sharesToRepay) internal virtual { - _fraxlendPair.repayAsset(sharesToRepay, address(this)); - } } diff --git a/src/modules/adaptors/Compound/v3/CompoundV3ExtraLogic.sol b/src/modules/adaptors/Compound/v3/CompoundV3ExtraLogic.sol index cff31bc70..8bb05a0a7 100644 --- a/src/modules/adaptors/Compound/v3/CompoundV3ExtraLogic.sol +++ b/src/modules/adaptors/Compound/v3/CompoundV3ExtraLogic.sol @@ -13,8 +13,9 @@ import { CometInterface } from "src/interfaces/external/Compound/CometInterface. * the CompoundV3SupplyAdaptor && CompoundV3DebtAdaptor. * @author crispymangoes, 0xEinCodes * NOTE: helper functions made virtual in case future versions require different implementation logic. The logic here is written in compliance with CompoundV3 + * NOTE: we inherit CometInterface in order to use the CometMath */ -abstract contract CompoundV3ExtraLogic { +abstract contract CompoundV3ExtraLogic is CometInterface { using Math for uint256; /** @@ -22,6 +23,37 @@ abstract contract CompoundV3ExtraLogic { */ error CompoundV3ExtraLogic__MarketPositionsMustBeTracked(address compMarket); + /** + * @notice Attempted tx that results in unhealthy cellar + */ + error CompoundV3ExtraLogic__PositionIsNotABorrowPosition(address compMarket); + + /** + * @notice var referencing specific compMarket + */ + uint8 public numAssets; + + /** + * @notice Minimum Health Factor enforced after every borrow or added collateral + * @notice Overwrites strategist set minimums if they are lower. + */ + uint256 public immutable minimumHealthFactor; + + constructor(uint256 _healthFactor) { + _verifyConstructorMinimumHealthFactor(_healthFactor); + minimumHealthFactor = _healthFactor; + } + + /** + * @notice Minimum Health Factor enforced after every borrow. + * @notice Overwrites strategist set minimums if they are lower. + */ + uint256 public immutable minimumHealthFactor; + + constructor(bool _accountForInterest, address _frax, uint256 _healthFactor) { + minimumHealthFactor = _healthFactor; + } + /** * @notice Get current collateral balance for caller in specified CompMarket and Collateral Asset. * @dev Queries the `CometStorage.sol` nested mapping for struct UserCollateral. @@ -52,6 +84,8 @@ abstract contract CompoundV3ExtraLogic { /** * @notice Validates that a given CompMarket and Asset are set up as a position in the Cellar. * @dev This function uses `address(this)` as the address of the Cellar. + * TODO: When calling this helper within txs from the CompoundV3DebtAdaptor, we assume that there are collateral positions within respective CompoundV3 lending market. If not then it should revert within ext. call to Compound contracts. + * This assumption is fine, because the health factor logic that compound carries out loops through all assets the respective account has as collateral within lending market. */ function _validateCompMarket(CometInterface _compMarket) internal view virtual { bytes32 positionHash = keccak256(abi.encode(identifier(), false, abi.encode(_compMarket))); @@ -68,9 +102,183 @@ abstract contract CompoundV3ExtraLogic { bytes32 positionHash = keccak256(abi.encode(identifier(), false, abi.encode(_compMarket, _asset))); uint32 positionId = Cellar(address(this)).registry().getPositionHashToPositionId(positionHash); if (!Cellar(address(this)).isPositionUsed(positionId)) - revert CompoundV3ExtraLogic__MarketAndAssetPositionsMustBeTracked( - address(_compMarket), - address(_asset) - ); + revert CompoundV3ExtraLogic__MarketAndAssetPositionsMustBeTracked(address(_compMarket), address(_asset)); + } + + // TODO: EIN THIS IS WHERE YOU LEFT OFF - YOU JUST BROUGHT IN THE MATH METHODS AND EVERYTHING ELSE NEEDED TO MAKE THIS WORK... IN THEORY. YOU'LL NEED TO GET IT TO COMPILE, BUT ALSO YOU NEED TO PASS IN A PARAM OR SET THE HEALTH FACTOR AND MULTIPLY IT BY THE LOOPED INCREMENTAL SUM IN THIS FUNCTION'S IMPLEMENTATION. + function _checkLiquidity(CometInterface _compMarket) internal view virtual returns (int liquidity) { + UserBasic userBasic = _compMarket.userBasic(address(this)); // struct should be accessible via extensions/inheritance within CometMainInterface + int104 principal = userBasic.principal; + + if (principal >= 0) { + revert CompoundV3ExtraLogic__PositionIsNotABorrowPosition(address(_compMarket)); + } // EIN: this just means that it was a non-borrow position, because `principal` is a signed integer, so if it's greater than 0, it is not a borrow position. + + uint16 assetsIn = userBasic[account].assetsIn; // EIN - collateral indices + int liquidity = signedMulPrice(presentValue(principal), getPrice(baseTokenPriceFeed), uint64(baseScale)); + + numAssets = _compMarket.numAssets(); + + for (uint8 i = 0; i < numAssets; ) { + if (isInAsset(assetsIn, i)) { + if (liquidity >= 0) { + return false; + } + + AssetInfo memory asset = getAssetInfo(i); + uint newAmount = mulPrice( + userCollateral[account][asset.asset].balance, + getPrice(asset.priceFeed), + asset.scale + ); + liquidity += signed256( + mulFactor(newAmount, (asset.liquidateCollateralFactor) * (1 / minimumHealthFactor)) + ); // TODO: EIN - Thinking of having a health factor applied to this to get a buffered amount. So that means it's either a constructor specified amount in this contract, or it is a constant in this contract. Leaning towards a constructor. + } + unchecked { + i++; + } + } + + return liquidity; + } + + /// helper functions from `Comet.sol` implementation logic + + /** + * @notice Get the current price from a feed + * @param priceFeed The address of a price feed + * @return The price, scaled by `PRICE_SCALE` + */ + function getPrice(address priceFeed) public view override returns (uint256) { + (, int price, , , ) = IPriceFeed(priceFeed).latestRoundData(); + if (price <= 0) revert BadPrice(); + return uint256(price); + } + + /** + * @notice Get the i-th asset info, according to the order they were passed in originally + * @param i The index of the asset info to get + * @return The asset info object + */ + function getAssetInfo(uint8 i) public view override returns (AssetInfo memory) { + if (i >= numAssets) revert BadAsset(); + + uint256 word_a; + uint256 word_b; + + if (i == 0) { + word_a = asset00_a; + word_b = asset00_b; + } else if (i == 1) { + word_a = asset01_a; + word_b = asset01_b; + } else if (i == 2) { + word_a = asset02_a; + word_b = asset02_b; + } else if (i == 3) { + word_a = asset03_a; + word_b = asset03_b; + } else if (i == 4) { + word_a = asset04_a; + word_b = asset04_b; + } else if (i == 5) { + word_a = asset05_a; + word_b = asset05_b; + } else if (i == 6) { + word_a = asset06_a; + word_b = asset06_b; + } else if (i == 7) { + word_a = asset07_a; + word_b = asset07_b; + } else if (i == 8) { + word_a = asset08_a; + word_b = asset08_b; + } else if (i == 9) { + word_a = asset09_a; + word_b = asset09_b; + } else if (i == 10) { + word_a = asset10_a; + word_b = asset10_b; + } else if (i == 11) { + word_a = asset11_a; + word_b = asset11_b; + } else if (i == 12) { + word_a = asset12_a; + word_b = asset12_b; + } else if (i == 13) { + word_a = asset13_a; + word_b = asset13_b; + } else if (i == 14) { + word_a = asset14_a; + word_b = asset14_b; + } else { + revert Absurd(); + } + + address asset = address(uint160(word_a & type(uint160).max)); + uint64 rescale = FACTOR_SCALE / 1e4; + uint64 borrowCollateralFactor = uint64(((word_a >> 160) & type(uint16).max) * rescale); + uint64 liquidateCollateralFactor = uint64(((word_a >> 176) & type(uint16).max) * rescale); + uint64 liquidationFactor = uint64(((word_a >> 192) & type(uint16).max) * rescale); + + address priceFeed = address(uint160(word_b & type(uint160).max)); + uint8 decimals_ = uint8(((word_b >> 160) & type(uint8).max)); + uint64 scale = uint64(10 ** decimals_); + uint128 supplyCap = uint128(((word_b >> 168) & type(uint64).max) * scale); + + return + AssetInfo({ + offset: i, + asset: asset, + priceFeed: priceFeed, + scale: scale, + borrowCollateralFactor: borrowCollateralFactor, + liquidateCollateralFactor: liquidateCollateralFactor, + liquidationFactor: liquidationFactor, + supplyCap: supplyCap + }); + } + + /** + * @dev Whether user has a non-zero balance of an asset, given assetsIn flags + */ + function isInAsset(uint16 assetsIn, uint8 assetOffset) internal pure returns (bool) { + return (assetsIn & (uint16(1) << assetOffset) != 0); + } + + /** + * @dev Multiply a number by a factor + */ + function mulFactor(uint n, uint factor) internal pure returns (uint) { + return (n * factor) / FACTOR_SCALE; + } + + /** + * @dev Divide a number by an amount of base + */ + function divBaseWei(uint n, uint baseWei) internal view returns (uint) { + return (n * baseScale) / baseWei; + } + + /** + * @dev Multiply a `fromScale` quantity by a price, returning a common price quantity + */ + function mulPrice(uint n, uint price, uint64 fromScale) internal pure returns (uint) { + return (n * price) / fromScale; + } + + /** + * @dev Multiply a signed `fromScale` quantity by a price, returning a common price quantity + */ + function signedMulPrice(int n, uint price, uint64 fromScale) internal pure returns (int) { + return (n * signed256(price)) / int256(uint256(fromScale)); + } + + /** + * @dev Divide a common price quantity by a price, returning a `toScale` quantity + */ + function divPrice(uint n, uint price, uint64 toScale) internal pure returns (uint) { + return (n * toScale) / price; } }