From bba7d99db28cdc04b7d90c33a5c3402b59ec0618 Mon Sep 17 00:00:00 2001 From: Mikhailo Shabodyash Date: Fri, 9 Aug 2024 23:45:26 +0300 Subject: [PATCH] feat: create comet with extended asset list --- contracts/AssetList.sol | 126 ++ contracts/AssetListFactory.sol | 17 + contracts/CometExt.sol | 5 +- contracts/CometExtendedAssetList.sol | 1225 +++++++++++++++++ contracts/CometFactoryExtendedAssetList.sol | 11 + contracts/IAssetList.sol | 12 + contracts/IAssetListFactory.sol | 13 + ...pdate_comet_to_support_more_collaterals.ts | 112 ++ scenario/LiquidationBotScenario.ts | 2 +- src/deploy/Network.ts | 4 +- test/asset-info-test-asset-list-comet.ts | 184 +++ test/constructor-test.ts | 10 +- test/helpers.ts | 29 +- test/liquidation/makeLiquidatableProtocol.ts | 9 +- 14 files changed, 1748 insertions(+), 11 deletions(-) create mode 100644 contracts/AssetList.sol create mode 100644 contracts/AssetListFactory.sol create mode 100644 contracts/CometExtendedAssetList.sol create mode 100644 contracts/CometFactoryExtendedAssetList.sol create mode 100644 contracts/IAssetList.sol create mode 100644 contracts/IAssetListFactory.sol create mode 100644 deployments/mainnet/weth/migrations/1723198576_update_comet_to_support_more_collaterals.ts create mode 100644 test/asset-info-test-asset-list-comet.ts diff --git a/contracts/AssetList.sol b/contracts/AssetList.sol new file mode 100644 index 000000000..7fc91c3bc --- /dev/null +++ b/contracts/AssetList.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "./CometConfiguration.sol"; +import "./IPriceFeed.sol"; +import "./IERC20NonStandard.sol"; +import "./CometMainInterface.sol"; +import "./CometCore.sol"; + +/** + * @title Compound's Asset List + * @author Compound + */ +contract AssetList { + /// @dev The decimals required for a price feed + uint8 internal constant PRICE_FEED_DECIMALS = 8; + + /// @dev The scale for factors + uint64 internal constant FACTOR_SCALE = 1e18; + + /// @dev The max value for a collateral factor (1) + uint64 internal constant MAX_COLLATERAL_FACTOR = FACTOR_SCALE; + + uint256[] internal assets_a; + uint256[] internal assets_b; + + /// @notice The number of assets this contract actually supports + uint8 public immutable numAssets; + + constructor(CometConfiguration.AssetConfig[] memory assetConfigs) { + numAssets = uint8(assetConfigs.length); + for (uint i = 0; i < assetConfigs.length; i++) { + (uint256 asset_a, uint256 asset_b) = getPackedAssetInternal(assetConfigs, i); + assets_a.push(asset_a); + assets_b.push(asset_b); + } + } + + /** + * @dev Checks and gets the packed asset info for storage + */ + function getPackedAssetInternal(CometConfiguration.AssetConfig[] memory assetConfigs, uint i) internal view returns (uint256, uint256) { + CometConfiguration.AssetConfig memory assetConfig; + if (i < assetConfigs.length) { + assembly { + assetConfig := mload(add(add(assetConfigs, 0x20), mul(i, 0x20))) + } + } else { + return (0, 0); + } + address asset = assetConfig.asset; + address priceFeed = assetConfig.priceFeed; + uint8 decimals_ = assetConfig.decimals; + + // Short-circuit if asset is nil + if (asset == address(0)) { + return (0, 0); + } + + // Sanity check price feed and asset decimals + if (IPriceFeed(priceFeed).decimals() != PRICE_FEED_DECIMALS) revert CometMainInterface.BadDecimals(); + if (IERC20NonStandard(asset).decimals() != decimals_) revert CometMainInterface.BadDecimals(); + + // Ensure collateral factors are within range + if (assetConfig.borrowCollateralFactor >= assetConfig.liquidateCollateralFactor) revert CometMainInterface.BorrowCFTooLarge(); + if (assetConfig.liquidateCollateralFactor > MAX_COLLATERAL_FACTOR) revert CometMainInterface.LiquidateCFTooLarge(); + + unchecked { + // Keep 4 decimals for each factor + uint64 descale = FACTOR_SCALE / 1e4; + uint16 borrowCollateralFactor = uint16(assetConfig.borrowCollateralFactor / descale); + uint16 liquidateCollateralFactor = uint16(assetConfig.liquidateCollateralFactor / descale); + uint16 liquidationFactor = uint16(assetConfig.liquidationFactor / descale); + + // Be nice and check descaled values are still within range + if (borrowCollateralFactor >= liquidateCollateralFactor) revert CometMainInterface.BorrowCFTooLarge(); + + // Keep whole units of asset for supply cap + uint64 supplyCap = uint64(assetConfig.supplyCap / (10 ** decimals_)); + + uint256 word_a = (uint160(asset) << 0 | + uint256(borrowCollateralFactor) << 160 | + uint256(liquidateCollateralFactor) << 176 | + uint256(liquidationFactor) << 192); + uint256 word_b = (uint160(priceFeed) << 0 | + uint256(decimals_) << 160 | + uint256(supplyCap) << 168); + + return (word_a, word_b); + } + } + + /** + * @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 returns (CometCore.AssetInfo memory) { + if (i >= numAssets) revert CometMainInterface.BadAsset(); + + uint256 word_a = assets_a[i]; + uint256 word_b = assets_b[i]; + + 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 CometCore.AssetInfo({ + offset: i, + asset: asset, + priceFeed: priceFeed, + scale: scale, + borrowCollateralFactor: borrowCollateralFactor, + liquidateCollateralFactor: liquidateCollateralFactor, + liquidationFactor: liquidationFactor, + supplyCap: supplyCap + }); + } +} \ No newline at end of file diff --git a/contracts/AssetListFactory.sol b/contracts/AssetListFactory.sol new file mode 100644 index 000000000..52c190adc --- /dev/null +++ b/contracts/AssetListFactory.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "./AssetList.sol"; + +/** + * @title Compound's Asset List Factory + * @author Compound + */ +contract AssetListFactory { + event AssetListCreated(address indexed assetList, CometCore.AssetConfig[] assetConfigs); + + function createAssetList(CometCore.AssetConfig[] memory assetConfigs) external returns (address assetList) { + assetList = address(new AssetList(assetConfigs)); + emit AssetListCreated(assetList, assetConfigs); + } +} \ No newline at end of file diff --git a/contracts/CometExt.sol b/contracts/CometExt.sol index 46962b3ad..67b7645e7 100644 --- a/contracts/CometExt.sol +++ b/contracts/CometExt.sol @@ -29,13 +29,16 @@ contract CometExt is CometExtInterface { /// @dev The ERC20 symbol for wrapped base token bytes32 internal immutable symbol32; + address immutable public assetListFactory; + /** * @notice Construct a new protocol instance * @param config The mapping of initial/constant parameters **/ - constructor(ExtConfiguration memory config) { + constructor(ExtConfiguration memory config, address assetListFactoryAddress) { name32 = config.name32; symbol32 = config.symbol32; + assetListFactory = assetListFactoryAddress; } /** External getters for internal constants **/ diff --git a/contracts/CometExtendedAssetList.sol b/contracts/CometExtendedAssetList.sol new file mode 100644 index 000000000..57d9d6c09 --- /dev/null +++ b/contracts/CometExtendedAssetList.sol @@ -0,0 +1,1225 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "./CometMainInterface.sol"; +import "./IERC20NonStandard.sol"; +import "./IPriceFeed.sol"; +import "./IAssetListFactory.sol"; +import "./IAssetList.sol"; + +/** + * @title Compound's Comet Contract + * @notice An efficient monolithic money market protocol + * @author Compound + */ +contract CometExtendedAssetList is CometMainInterface { + /** General configuration constants **/ + + /// @notice The admin of the protocol + address public override immutable governor; + + /// @notice The account which may trigger pauses + address public override immutable pauseGuardian; + + /// @notice The address of the base token contract + address public override immutable baseToken; + + /// @notice The address of the price feed for the base token + address public override immutable baseTokenPriceFeed; + + /// @notice The address of the extension contract delegate + address public override immutable extensionDelegate; + + /// @notice The point in the supply rates separating the low interest rate slope and the high interest rate slope (factor) + /// @dev uint64 + uint public override immutable supplyKink; + + /// @notice Per second supply interest rate slope applied when utilization is below kink (factor) + /// @dev uint64 + uint public override immutable supplyPerSecondInterestRateSlopeLow; + + /// @notice Per second supply interest rate slope applied when utilization is above kink (factor) + /// @dev uint64 + uint public override immutable supplyPerSecondInterestRateSlopeHigh; + + /// @notice Per second supply base interest rate (factor) + /// @dev uint64 + uint public override immutable supplyPerSecondInterestRateBase; + + /// @notice The point in the borrow rate separating the low interest rate slope and the high interest rate slope (factor) + /// @dev uint64 + uint public override immutable borrowKink; + + /// @notice Per second borrow interest rate slope applied when utilization is below kink (factor) + /// @dev uint64 + uint public override immutable borrowPerSecondInterestRateSlopeLow; + + /// @notice Per second borrow interest rate slope applied when utilization is above kink (factor) + /// @dev uint64 + uint public override immutable borrowPerSecondInterestRateSlopeHigh; + + /// @notice Per second borrow base interest rate (factor) + /// @dev uint64 + uint public override immutable borrowPerSecondInterestRateBase; + + /// @notice The fraction of the liquidation penalty that goes to buyers of collateral instead of the protocol + /// @dev uint64 + uint public override immutable storeFrontPriceFactor; + + /// @notice The scale for base token (must be less than 18 decimals) + /// @dev uint64 + uint public override immutable baseScale; + + /// @notice The scale for reward tracking + /// @dev uint64 + uint public override immutable trackingIndexScale; + + /// @notice The speed at which supply rewards are tracked (in trackingIndexScale) + /// @dev uint64 + uint public override immutable baseTrackingSupplySpeed; + + /// @notice The speed at which borrow rewards are tracked (in trackingIndexScale) + /// @dev uint64 + uint public override immutable baseTrackingBorrowSpeed; + + /// @notice The minimum amount of base principal wei for rewards to accrue + /// @dev This must be large enough so as to prevent division by base wei from overflowing the 64 bit indices + /// @dev uint104 + uint public override immutable baseMinForRewards; + + /// @notice The minimum base amount required to initiate a borrow + uint public override immutable baseBorrowMin; + + /// @notice The minimum base token reserves which must be held before collateral is hodled + uint public override immutable targetReserves; + + /// @notice The number of decimals for wrapped base token + uint8 public override immutable decimals; + + /// @notice The number of assets this contract actually supports + uint8 public override immutable numAssets; + + /// @notice Factor to divide by when accruing rewards in order to preserve 6 decimals (i.e. baseScale / 1e6) + uint internal immutable accrualDescaleFactor; + + /// @notice The address of the asset list + address immutable public assetList; + + /** + * @notice Construct a new protocol instance + * @param config The mapping of initial/constant parameters + **/ + constructor(Configuration memory config) { + // Sanity checks + uint8 decimals_ = IERC20NonStandard(config.baseToken).decimals(); + if (decimals_ > MAX_BASE_DECIMALS) revert BadDecimals(); + if (config.storeFrontPriceFactor > FACTOR_SCALE) revert BadDiscount(); + if (config.baseMinForRewards == 0) revert BadMinimum(); + if (IPriceFeed(config.baseTokenPriceFeed).decimals() != PRICE_FEED_DECIMALS) revert BadDecimals(); + + // Copy configuration + unchecked { + governor = config.governor; + pauseGuardian = config.pauseGuardian; + baseToken = config.baseToken; + baseTokenPriceFeed = config.baseTokenPriceFeed; + extensionDelegate = config.extensionDelegate; + storeFrontPriceFactor = config.storeFrontPriceFactor; + + decimals = decimals_; + baseScale = uint64(10 ** decimals_); + trackingIndexScale = config.trackingIndexScale; + if (baseScale < BASE_ACCRUAL_SCALE) revert BadDecimals(); + accrualDescaleFactor = baseScale / BASE_ACCRUAL_SCALE; + + baseMinForRewards = config.baseMinForRewards; + baseTrackingSupplySpeed = config.baseTrackingSupplySpeed; + baseTrackingBorrowSpeed = config.baseTrackingBorrowSpeed; + + baseBorrowMin = config.baseBorrowMin; + targetReserves = config.targetReserves; + } + + // Set interest rate model configs + unchecked { + supplyKink = config.supplyKink; + supplyPerSecondInterestRateSlopeLow = config.supplyPerYearInterestRateSlopeLow / SECONDS_PER_YEAR; + supplyPerSecondInterestRateSlopeHigh = config.supplyPerYearInterestRateSlopeHigh / SECONDS_PER_YEAR; + supplyPerSecondInterestRateBase = config.supplyPerYearInterestRateBase / SECONDS_PER_YEAR; + borrowKink = config.borrowKink; + borrowPerSecondInterestRateSlopeLow = config.borrowPerYearInterestRateSlopeLow / SECONDS_PER_YEAR; + borrowPerSecondInterestRateSlopeHigh = config.borrowPerYearInterestRateSlopeHigh / SECONDS_PER_YEAR; + borrowPerSecondInterestRateBase = config.borrowPerYearInterestRateBase / SECONDS_PER_YEAR; + } + + // Set asset info + numAssets = uint8(config.assetConfigs.length); + + assetList = IAssetListFactory(IAssetListFactory(extensionDelegate).assetListFactory()).createAssetList(config.assetConfigs); + } + + /** + * @dev Prevents marked functions from being reentered + * Note: this restrict contracts from calling comet functions in their hooks. + * Doing so will cause the transaction to revert. + */ + modifier nonReentrant() { + nonReentrantBefore(); + _; + nonReentrantAfter(); + } + + /** + * @dev Checks that the reentrancy flag is not set and then sets the flag + */ + function nonReentrantBefore() internal { + bytes32 slot = REENTRANCY_GUARD_FLAG_SLOT; + uint256 status; + assembly ("memory-safe") { + status := sload(slot) + } + + if (status == REENTRANCY_GUARD_ENTERED) revert ReentrantCallBlocked(); + assembly ("memory-safe") { + sstore(slot, REENTRANCY_GUARD_ENTERED) + } + } + + /** + * @dev Unsets the reentrancy flag + */ + function nonReentrantAfter() internal { + bytes32 slot = REENTRANCY_GUARD_FLAG_SLOT; + uint256 status; + assembly ("memory-safe") { + sstore(slot, REENTRANCY_GUARD_NOT_ENTERED) + } + } + + /** + * @notice Initialize storage for the contract + * @dev Can be used from constructor or proxy + */ + function initializeStorage() override external { + if (lastAccrualTime != 0) revert AlreadyInitialized(); + + // Initialize aggregates + lastAccrualTime = getNowInternal(); + baseSupplyIndex = BASE_INDEX_SCALE; + baseBorrowIndex = BASE_INDEX_SCALE; + + // Implicit initialization (not worth increasing contract size) + // trackingSupplyIndex = 0; + // trackingBorrowIndex = 0; + } + + /** + * @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) override public view returns (AssetInfo memory) { + return IAssetList(assetList).getAssetInfo(i); + } + + /** + * @dev Determine index of asset that matches given address + */ + function getAssetInfoByAddress(address asset) override public view returns (AssetInfo memory) { + for (uint8 i = 0; i < numAssets; ) { + AssetInfo memory assetInfo = getAssetInfo(i); + if (assetInfo.asset == asset) { + return assetInfo; + } + unchecked { i++; } + } + revert BadAsset(); + } + + /** + * @return The current timestamp + **/ + function getNowInternal() virtual internal view returns (uint40) { + if (block.timestamp >= 2**40) revert TimestampTooLarge(); + return uint40(block.timestamp); + } + + /** + * @dev Calculate accrued interest indices for base token supply and borrows + **/ + function accruedInterestIndices(uint timeElapsed) internal view returns (uint64, uint64) { + uint64 baseSupplyIndex_ = baseSupplyIndex; + uint64 baseBorrowIndex_ = baseBorrowIndex; + if (timeElapsed > 0) { + uint utilization = getUtilization(); + uint supplyRate = getSupplyRate(utilization); + uint borrowRate = getBorrowRate(utilization); + baseSupplyIndex_ += safe64(mulFactor(baseSupplyIndex_, supplyRate * timeElapsed)); + baseBorrowIndex_ += safe64(mulFactor(baseBorrowIndex_, borrowRate * timeElapsed)); + } + return (baseSupplyIndex_, baseBorrowIndex_); + } + + /** + * @dev Accrue interest (and rewards) in base token supply and borrows + **/ + function accrueInternal() internal { + uint40 now_ = getNowInternal(); + uint timeElapsed = uint256(now_ - lastAccrualTime); + if (timeElapsed > 0) { + (baseSupplyIndex, baseBorrowIndex) = accruedInterestIndices(timeElapsed); + if (totalSupplyBase >= baseMinForRewards) { + trackingSupplyIndex += safe64(divBaseWei(baseTrackingSupplySpeed * timeElapsed, totalSupplyBase)); + } + if (totalBorrowBase >= baseMinForRewards) { + trackingBorrowIndex += safe64(divBaseWei(baseTrackingBorrowSpeed * timeElapsed, totalBorrowBase)); + } + lastAccrualTime = now_; + } + } + + /** + * @notice Accrue interest and rewards for an account + **/ + function accrueAccount(address account) override external { + accrueInternal(); + + UserBasic memory basic = userBasic[account]; + updateBasePrincipal(account, basic, basic.principal); + } + + /** + * @dev Note: Does not accrue interest first + * @param utilization The utilization to check the supply rate for + * @return The per second supply rate at `utilization` + */ + function getSupplyRate(uint utilization) override public view returns (uint64) { + if (utilization <= supplyKink) { + // interestRateBase + interestRateSlopeLow * utilization + return safe64(supplyPerSecondInterestRateBase + mulFactor(supplyPerSecondInterestRateSlopeLow, utilization)); + } else { + // interestRateBase + interestRateSlopeLow * kink + interestRateSlopeHigh * (utilization - kink) + return safe64(supplyPerSecondInterestRateBase + mulFactor(supplyPerSecondInterestRateSlopeLow, supplyKink) + mulFactor(supplyPerSecondInterestRateSlopeHigh, (utilization - supplyKink))); + } + } + + /** + * @dev Note: Does not accrue interest first + * @param utilization The utilization to check the borrow rate for + * @return The per second borrow rate at `utilization` + */ + function getBorrowRate(uint utilization) override public view returns (uint64) { + if (utilization <= borrowKink) { + // interestRateBase + interestRateSlopeLow * utilization + return safe64(borrowPerSecondInterestRateBase + mulFactor(borrowPerSecondInterestRateSlopeLow, utilization)); + } else { + // interestRateBase + interestRateSlopeLow * kink + interestRateSlopeHigh * (utilization - kink) + return safe64(borrowPerSecondInterestRateBase + mulFactor(borrowPerSecondInterestRateSlopeLow, borrowKink) + mulFactor(borrowPerSecondInterestRateSlopeHigh, (utilization - borrowKink))); + } + } + + /** + * @dev Note: Does not accrue interest first + * @return The utilization rate of the base asset + */ + function getUtilization() override public view returns (uint) { + uint totalSupply_ = presentValueSupply(baseSupplyIndex, totalSupplyBase); + uint totalBorrow_ = presentValueBorrow(baseBorrowIndex, totalBorrowBase); + if (totalSupply_ == 0) { + return 0; + } else { + return totalBorrow_ * FACTOR_SCALE / totalSupply_; + } + } + + /** + * @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) override public view returns (uint256) { + (, int price, , , ) = IPriceFeed(priceFeed).latestRoundData(); + if (price <= 0) revert BadPrice(); + return uint256(price); + } + + /** + * @notice Gets the total balance of protocol collateral reserves for an asset + * @dev Note: Reverts if collateral reserves are somehow negative, which should not be possible + * @param asset The collateral asset + */ + function getCollateralReserves(address asset) override public view returns (uint) { + return IERC20NonStandard(asset).balanceOf(address(this)) - totalsCollateral[asset].totalSupplyAsset; + } + + /** + * @notice Gets the total amount of protocol reserves of the base asset + */ + function getReserves() override public view returns (int) { + (uint64 baseSupplyIndex_, uint64 baseBorrowIndex_) = accruedInterestIndices(getNowInternal() - lastAccrualTime); + uint balance = IERC20NonStandard(baseToken).balanceOf(address(this)); + uint totalSupply_ = presentValueSupply(baseSupplyIndex_, totalSupplyBase); + uint totalBorrow_ = presentValueBorrow(baseBorrowIndex_, totalBorrowBase); + return signed256(balance) - signed256(totalSupply_) + signed256(totalBorrow_); + } + + /** + * @notice Check whether an account has enough collateral to borrow + * @param account The address to check + * @return Whether the account is minimally collateralized enough to borrow + */ + function isBorrowCollateralized(address account) override public view returns (bool) { + int104 principal = userBasic[account].principal; + + if (principal >= 0) { + return true; + } + + uint16 assetsIn = userBasic[account].assetsIn; + int liquidity = signedMulPrice( + presentValue(principal), + getPrice(baseTokenPriceFeed), + uint64(baseScale) + ); + + for (uint8 i = 0; i < numAssets; ) { + if (isInAsset(assetsIn, i)) { + if (liquidity >= 0) { + return true; + } + + AssetInfo memory asset = getAssetInfo(i); + uint newAmount = mulPrice( + userCollateral[account][asset.asset].balance, + getPrice(asset.priceFeed), + asset.scale + ); + liquidity += signed256(mulFactor( + newAmount, + asset.borrowCollateralFactor + )); + } + unchecked { i++; } + } + + return liquidity >= 0; + } + + /** + * @notice Check whether an account has enough collateral to not be liquidated + * @param account The address to check + * @return Whether the account is minimally collateralized enough to not be liquidated + */ + function isLiquidatable(address account) override public view returns (bool) { + int104 principal = userBasic[account].principal; + + if (principal >= 0) { + return false; + } + + uint16 assetsIn = userBasic[account].assetsIn; + int liquidity = signedMulPrice( + presentValue(principal), + getPrice(baseTokenPriceFeed), + uint64(baseScale) + ); + + 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 + )); + } + unchecked { i++; } + } + + return liquidity < 0; + } + + /** + * @dev The change in principal broken into repay and supply amounts + */ + function repayAndSupplyAmount(int104 oldPrincipal, int104 newPrincipal) internal pure returns (uint104, uint104) { + // If the new principal is less than the old principal, then no amount has been repaid or supplied + if (newPrincipal < oldPrincipal) return (0, 0); + + if (newPrincipal <= 0) { + return (uint104(newPrincipal - oldPrincipal), 0); + } else if (oldPrincipal >= 0) { + return (0, uint104(newPrincipal - oldPrincipal)); + } else { + return (uint104(-oldPrincipal), uint104(newPrincipal)); + } + } + + /** + * @dev The change in principal broken into withdraw and borrow amounts + */ + function withdrawAndBorrowAmount(int104 oldPrincipal, int104 newPrincipal) internal pure returns (uint104, uint104) { + // If the new principal is greater than the old principal, then no amount has been withdrawn or borrowed + if (newPrincipal > oldPrincipal) return (0, 0); + + if (newPrincipal >= 0) { + return (uint104(oldPrincipal - newPrincipal), 0); + } else if (oldPrincipal <= 0) { + return (0, uint104(oldPrincipal - newPrincipal)); + } else { + return (uint104(oldPrincipal), uint104(-newPrincipal)); + } + } + + /** + * @notice Pauses different actions within Comet + * @param supplyPaused Boolean for pausing supply actions + * @param transferPaused Boolean for pausing transfer actions + * @param withdrawPaused Boolean for pausing withdraw actions + * @param absorbPaused Boolean for pausing absorb actions + * @param buyPaused Boolean for pausing buy actions + */ + function pause( + bool supplyPaused, + bool transferPaused, + bool withdrawPaused, + bool absorbPaused, + bool buyPaused + ) override external { + if (msg.sender != governor && msg.sender != pauseGuardian) revert Unauthorized(); + + pauseFlags = + uint8(0) | + (toUInt8(supplyPaused) << PAUSE_SUPPLY_OFFSET) | + (toUInt8(transferPaused) << PAUSE_TRANSFER_OFFSET) | + (toUInt8(withdrawPaused) << PAUSE_WITHDRAW_OFFSET) | + (toUInt8(absorbPaused) << PAUSE_ABSORB_OFFSET) | + (toUInt8(buyPaused) << PAUSE_BUY_OFFSET); + + emit PauseAction(supplyPaused, transferPaused, withdrawPaused, absorbPaused, buyPaused); + } + + /** + * @return Whether or not supply actions are paused + */ + function isSupplyPaused() override public view returns (bool) { + return toBool(pauseFlags & (uint8(1) << PAUSE_SUPPLY_OFFSET)); + } + + /** + * @return Whether or not transfer actions are paused + */ + function isTransferPaused() override public view returns (bool) { + return toBool(pauseFlags & (uint8(1) << PAUSE_TRANSFER_OFFSET)); + } + + /** + * @return Whether or not withdraw actions are paused + */ + function isWithdrawPaused() override public view returns (bool) { + return toBool(pauseFlags & (uint8(1) << PAUSE_WITHDRAW_OFFSET)); + } + + /** + * @return Whether or not absorb actions are paused + */ + function isAbsorbPaused() override public view returns (bool) { + return toBool(pauseFlags & (uint8(1) << PAUSE_ABSORB_OFFSET)); + } + + /** + * @return Whether or not buy actions are paused + */ + function isBuyPaused() override public view returns (bool) { + return toBool(pauseFlags & (uint8(1) << PAUSE_BUY_OFFSET)); + } + + /** + * @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; + } + + /** + * @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 Update assetsIn bit vector if user has entered or exited an asset + */ + function updateAssetsIn( + address account, + AssetInfo memory assetInfo, + uint128 initialUserBalance, + uint128 finalUserBalance + ) internal { + if (initialUserBalance == 0 && finalUserBalance != 0) { + // set bit for asset + userBasic[account].assetsIn |= (uint16(1) << assetInfo.offset); + } else if (initialUserBalance != 0 && finalUserBalance == 0) { + // clear bit for asset + userBasic[account].assetsIn &= ~(uint16(1) << assetInfo.offset); + } + } + + /** + * @dev Write updated principal to store and tracking participation + */ + function updateBasePrincipal(address account, UserBasic memory basic, int104 principalNew) internal { + int104 principal = basic.principal; + basic.principal = principalNew; + + if (principal >= 0) { + uint indexDelta = uint256(trackingSupplyIndex - basic.baseTrackingIndex); + basic.baseTrackingAccrued += safe64(uint104(principal) * indexDelta / trackingIndexScale / accrualDescaleFactor); + } else { + uint indexDelta = uint256(trackingBorrowIndex - basic.baseTrackingIndex); + basic.baseTrackingAccrued += safe64(uint104(-principal) * indexDelta / trackingIndexScale / accrualDescaleFactor); + } + + if (principalNew >= 0) { + basic.baseTrackingIndex = trackingSupplyIndex; + } else { + basic.baseTrackingIndex = trackingBorrowIndex; + } + + userBasic[account] = basic; + } + + /** + * @dev Safe ERC20 transfer in and returns the final amount transferred (taking into account any fees) + * @dev Note: Safely handles non-standard ERC-20 tokens that do not return a value. See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca + */ + function doTransferIn(address asset, address from, uint amount) internal returns (uint) { + uint256 preTransferBalance = IERC20NonStandard(asset).balanceOf(address(this)); + IERC20NonStandard(asset).transferFrom(from, address(this), amount); + bool success; + assembly ("memory-safe") { + switch returndatasize() + case 0 { // This is a non-standard ERC-20 + success := not(0) // set success to true + } + case 32 { // This is a compliant ERC-20 + returndatacopy(0, 0, 32) + success := mload(0) // Set `success = returndata` of override external call + } + default { // This is an excessively non-compliant ERC-20, revert. + revert(0, 0) + } + } + if (!success) revert TransferInFailed(); + return IERC20NonStandard(asset).balanceOf(address(this)) - preTransferBalance; + } + + /** + * @dev Safe ERC20 transfer out + * @dev Note: Safely handles non-standard ERC-20 tokens that do not return a value. See here: https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca + */ + function doTransferOut(address asset, address to, uint amount) internal { + IERC20NonStandard(asset).transfer(to, amount); + bool success; + assembly ("memory-safe") { + switch returndatasize() + case 0 { // This is a non-standard ERC-20 + success := not(0) // set success to true + } + case 32 { // This is a compliant ERC-20 + returndatacopy(0, 0, 32) + success := mload(0) // Set `success = returndata` of override external call + } + default { // This is an excessively non-compliant ERC-20, revert. + revert(0, 0) + } + } + if (!success) revert TransferOutFailed(); + } + + /** + * @notice Supply an amount of asset to the protocol + * @param asset The asset to supply + * @param amount The quantity to supply + */ + function supply(address asset, uint amount) override external { + return supplyInternal(msg.sender, msg.sender, msg.sender, asset, amount); + } + + /** + * @notice Supply an amount of asset to dst + * @param dst The address which will hold the balance + * @param asset The asset to supply + * @param amount The quantity to supply + */ + function supplyTo(address dst, address asset, uint amount) override external { + return supplyInternal(msg.sender, msg.sender, dst, asset, amount); + } + + /** + * @notice Supply an amount of asset from `from` to dst, if allowed + * @param from The supplier address + * @param dst The address which will hold the balance + * @param asset The asset to supply + * @param amount The quantity to supply + */ + function supplyFrom(address from, address dst, address asset, uint amount) override external { + return supplyInternal(msg.sender, from, dst, asset, amount); + } + + /** + * @dev Supply either collateral or base asset, depending on the asset, if operator is allowed + * @dev Note: Specifying an `amount` of uint256.max will repay all of `dst`'s accrued base borrow balance + */ + function supplyInternal(address operator, address from, address dst, address asset, uint amount) internal nonReentrant { + if (isSupplyPaused()) revert Paused(); + if (!hasPermission(from, operator)) revert Unauthorized(); + + if (asset == baseToken) { + if (amount == type(uint256).max) { + amount = borrowBalanceOf(dst); + } + return supplyBase(from, dst, amount); + } else { + return supplyCollateral(from, dst, asset, safe128(amount)); + } + } + + /** + * @dev Supply an amount of base asset from `from` to dst + */ + function supplyBase(address from, address dst, uint256 amount) internal { + amount = doTransferIn(baseToken, from, amount); + + accrueInternal(); + + UserBasic memory dstUser = userBasic[dst]; + int104 dstPrincipal = dstUser.principal; + int256 dstBalance = presentValue(dstPrincipal) + signed256(amount); + int104 dstPrincipalNew = principalValue(dstBalance); + + (uint104 repayAmount, uint104 supplyAmount) = repayAndSupplyAmount(dstPrincipal, dstPrincipalNew); + + totalSupplyBase += supplyAmount; + totalBorrowBase -= repayAmount; + + updateBasePrincipal(dst, dstUser, dstPrincipalNew); + + emit Supply(from, dst, amount); + + if (supplyAmount > 0) { + emit Transfer(address(0), dst, presentValueSupply(baseSupplyIndex, supplyAmount)); + } + } + + /** + * @dev Supply an amount of collateral asset from `from` to dst + */ + function supplyCollateral(address from, address dst, address asset, uint128 amount) internal { + amount = safe128(doTransferIn(asset, from, amount)); + + AssetInfo memory assetInfo = getAssetInfoByAddress(asset); + TotalsCollateral memory totals = totalsCollateral[asset]; + totals.totalSupplyAsset += amount; + if (totals.totalSupplyAsset > assetInfo.supplyCap) revert SupplyCapExceeded(); + + uint128 dstCollateral = userCollateral[dst][asset].balance; + uint128 dstCollateralNew = dstCollateral + amount; + + totalsCollateral[asset] = totals; + userCollateral[dst][asset].balance = dstCollateralNew; + + updateAssetsIn(dst, assetInfo, dstCollateral, dstCollateralNew); + + emit SupplyCollateral(from, dst, asset, amount); + } + + /** + * @notice ERC20 transfer an amount of base token to dst + * @param dst The recipient address + * @param amount The quantity to transfer + * @return true + */ + function transfer(address dst, uint amount) override external returns (bool) { + transferInternal(msg.sender, msg.sender, dst, baseToken, amount); + return true; + } + + /** + * @notice ERC20 transfer an amount of base token from src to dst, if allowed + * @param src The sender address + * @param dst The recipient address + * @param amount The quantity to transfer + * @return true + */ + function transferFrom(address src, address dst, uint amount) override external returns (bool) { + transferInternal(msg.sender, src, dst, baseToken, amount); + return true; + } + + /** + * @notice Transfer an amount of asset to dst + * @param dst The recipient address + * @param asset The asset to transfer + * @param amount The quantity to transfer + */ + function transferAsset(address dst, address asset, uint amount) override external { + return transferInternal(msg.sender, msg.sender, dst, asset, amount); + } + + /** + * @notice Transfer an amount of asset from src to dst, if allowed + * @param src The sender address + * @param dst The recipient address + * @param asset The asset to transfer + * @param amount The quantity to transfer + */ + function transferAssetFrom(address src, address dst, address asset, uint amount) override external { + return transferInternal(msg.sender, src, dst, asset, amount); + } + + /** + * @dev Transfer either collateral or base asset, depending on the asset, if operator is allowed + * @dev Note: Specifying an `amount` of uint256.max will transfer all of `src`'s accrued base balance + */ + function transferInternal(address operator, address src, address dst, address asset, uint amount) internal nonReentrant { + if (isTransferPaused()) revert Paused(); + if (!hasPermission(src, operator)) revert Unauthorized(); + if (src == dst) revert NoSelfTransfer(); + + if (asset == baseToken) { + if (amount == type(uint256).max) { + amount = balanceOf(src); + } + return transferBase(src, dst, amount); + } else { + return transferCollateral(src, dst, asset, safe128(amount)); + } + } + + /** + * @dev Transfer an amount of base asset from src to dst, borrowing if possible/necessary + */ + function transferBase(address src, address dst, uint256 amount) internal { + accrueInternal(); + + UserBasic memory srcUser = userBasic[src]; + UserBasic memory dstUser = userBasic[dst]; + + int104 srcPrincipal = srcUser.principal; + int104 dstPrincipal = dstUser.principal; + int256 srcBalance = presentValue(srcPrincipal) - signed256(amount); + int256 dstBalance = presentValue(dstPrincipal) + signed256(amount); + int104 srcPrincipalNew = principalValue(srcBalance); + int104 dstPrincipalNew = principalValue(dstBalance); + + (uint104 withdrawAmount, uint104 borrowAmount) = withdrawAndBorrowAmount(srcPrincipal, srcPrincipalNew); + (uint104 repayAmount, uint104 supplyAmount) = repayAndSupplyAmount(dstPrincipal, dstPrincipalNew); + + // Note: Instead of `total += addAmount - subAmount` to avoid underflow errors. + totalSupplyBase = totalSupplyBase + supplyAmount - withdrawAmount; + totalBorrowBase = totalBorrowBase + borrowAmount - repayAmount; + + updateBasePrincipal(src, srcUser, srcPrincipalNew); + updateBasePrincipal(dst, dstUser, dstPrincipalNew); + + if (srcBalance < 0) { + if (uint256(-srcBalance) < baseBorrowMin) revert BorrowTooSmall(); + if (!isBorrowCollateralized(src)) revert NotCollateralized(); + } + + if (withdrawAmount > 0) { + emit Transfer(src, address(0), presentValueSupply(baseSupplyIndex, withdrawAmount)); + } + + if (supplyAmount > 0) { + emit Transfer(address(0), dst, presentValueSupply(baseSupplyIndex, supplyAmount)); + } + } + + /** + * @dev Transfer an amount of collateral asset from src to dst + */ + function transferCollateral(address src, address dst, address asset, uint128 amount) internal { + uint128 srcCollateral = userCollateral[src][asset].balance; + uint128 dstCollateral = userCollateral[dst][asset].balance; + uint128 srcCollateralNew = srcCollateral - amount; + uint128 dstCollateralNew = dstCollateral + amount; + + userCollateral[src][asset].balance = srcCollateralNew; + userCollateral[dst][asset].balance = dstCollateralNew; + + AssetInfo memory assetInfo = getAssetInfoByAddress(asset); + updateAssetsIn(src, assetInfo, srcCollateral, srcCollateralNew); + updateAssetsIn(dst, assetInfo, dstCollateral, dstCollateralNew); + + // Note: no accrue interest, BorrowCF < LiquidationCF covers small changes + if (!isBorrowCollateralized(src)) revert NotCollateralized(); + + emit TransferCollateral(src, dst, asset, amount); + } + + /** + * @notice Withdraw an amount of asset from the protocol + * @param asset The asset to withdraw + * @param amount The quantity to withdraw + */ + function withdraw(address asset, uint amount) override external { + return withdrawInternal(msg.sender, msg.sender, msg.sender, asset, amount); + } + + /** + * @notice Withdraw an amount of asset to `to` + * @param to The recipient address + * @param asset The asset to withdraw + * @param amount The quantity to withdraw + */ + function withdrawTo(address to, address asset, uint amount) override external { + return withdrawInternal(msg.sender, msg.sender, to, asset, amount); + } + + /** + * @notice Withdraw an amount of asset from src to `to`, if allowed + * @param src The sender address + * @param to The recipient address + * @param asset The asset to withdraw + * @param amount The quantity to withdraw + */ + function withdrawFrom(address src, address to, address asset, uint amount) override external { + return withdrawInternal(msg.sender, src, to, asset, amount); + } + + /** + * @dev Withdraw either collateral or base asset, depending on the asset, if operator is allowed + * @dev Note: Specifying an `amount` of uint256.max will withdraw all of `src`'s accrued base balance + */ + function withdrawInternal(address operator, address src, address to, address asset, uint amount) internal nonReentrant { + if (isWithdrawPaused()) revert Paused(); + if (!hasPermission(src, operator)) revert Unauthorized(); + + if (asset == baseToken) { + if (amount == type(uint256).max) { + amount = balanceOf(src); + } + return withdrawBase(src, to, amount); + } else { + return withdrawCollateral(src, to, asset, safe128(amount)); + } + } + + /** + * @dev Withdraw an amount of base asset from src to `to`, borrowing if possible/necessary + */ + function withdrawBase(address src, address to, uint256 amount) internal { + accrueInternal(); + + UserBasic memory srcUser = userBasic[src]; + int104 srcPrincipal = srcUser.principal; + int256 srcBalance = presentValue(srcPrincipal) - signed256(amount); + int104 srcPrincipalNew = principalValue(srcBalance); + + (uint104 withdrawAmount, uint104 borrowAmount) = withdrawAndBorrowAmount(srcPrincipal, srcPrincipalNew); + + totalSupplyBase -= withdrawAmount; + totalBorrowBase += borrowAmount; + + updateBasePrincipal(src, srcUser, srcPrincipalNew); + + if (srcBalance < 0) { + if (uint256(-srcBalance) < baseBorrowMin) revert BorrowTooSmall(); + if (!isBorrowCollateralized(src)) revert NotCollateralized(); + } + + doTransferOut(baseToken, to, amount); + + emit Withdraw(src, to, amount); + + if (withdrawAmount > 0) { + emit Transfer(src, address(0), presentValueSupply(baseSupplyIndex, withdrawAmount)); + } + } + + /** + * @dev Withdraw an amount of collateral asset from src to `to` + */ + function withdrawCollateral(address src, address to, address asset, uint128 amount) internal { + uint128 srcCollateral = userCollateral[src][asset].balance; + uint128 srcCollateralNew = srcCollateral - amount; + + totalsCollateral[asset].totalSupplyAsset -= amount; + userCollateral[src][asset].balance = srcCollateralNew; + + AssetInfo memory assetInfo = getAssetInfoByAddress(asset); + updateAssetsIn(src, assetInfo, srcCollateral, srcCollateralNew); + + // Note: no accrue interest, BorrowCF < LiquidationCF covers small changes + if (!isBorrowCollateralized(src)) revert NotCollateralized(); + + doTransferOut(asset, to, amount); + + emit WithdrawCollateral(src, to, asset, amount); + } + + /** + * @notice Absorb a list of underwater accounts onto the protocol balance sheet + * @param absorber The recipient of the incentive paid to the caller of absorb + * @param accounts The list of underwater accounts to absorb + */ + function absorb(address absorber, address[] calldata accounts) override external { + if (isAbsorbPaused()) revert Paused(); + + uint startGas = gasleft(); + accrueInternal(); + for (uint i = 0; i < accounts.length; ) { + absorbInternal(absorber, accounts[i]); + unchecked { i++; } + } + uint gasUsed = startGas - gasleft(); + + // Note: liquidator points are an imperfect tool for governance, + // to be used while evaluating strategies for incentivizing absorption. + // Using gas price instead of base fee would more accurately reflect spend, + // but is also subject to abuse if refunds were to be given automatically. + LiquidatorPoints memory points = liquidatorPoints[absorber]; + points.numAbsorbs++; + points.numAbsorbed += safe64(accounts.length); + points.approxSpend += safe128(gasUsed * block.basefee); + liquidatorPoints[absorber] = points; + } + + /** + * @dev Transfer user's collateral and debt to the protocol itself. + */ + function absorbInternal(address absorber, address account) internal { + if (!isLiquidatable(account)) revert NotLiquidatable(); + + UserBasic memory accountUser = userBasic[account]; + int104 oldPrincipal = accountUser.principal; + int256 oldBalance = presentValue(oldPrincipal); + uint16 assetsIn = accountUser.assetsIn; + + uint256 basePrice = getPrice(baseTokenPriceFeed); + uint256 deltaValue = 0; + + for (uint8 i = 0; i < numAssets; ) { + if (isInAsset(assetsIn, i)) { + AssetInfo memory assetInfo = getAssetInfo(i); + address asset = assetInfo.asset; + uint128 seizeAmount = userCollateral[account][asset].balance; + userCollateral[account][asset].balance = 0; + totalsCollateral[asset].totalSupplyAsset -= seizeAmount; + + uint256 value = mulPrice(seizeAmount, getPrice(assetInfo.priceFeed), assetInfo.scale); + deltaValue += mulFactor(value, assetInfo.liquidationFactor); + + emit AbsorbCollateral(absorber, account, asset, seizeAmount, value); + } + unchecked { i++; } + } + + uint256 deltaBalance = divPrice(deltaValue, basePrice, uint64(baseScale)); + int256 newBalance = oldBalance + signed256(deltaBalance); + // New balance will not be negative, all excess debt absorbed by reserves + if (newBalance < 0) { + newBalance = 0; + } + + int104 newPrincipal = principalValue(newBalance); + updateBasePrincipal(account, accountUser, newPrincipal); + + // reset assetsIn + userBasic[account].assetsIn = 0; + + (uint104 repayAmount, uint104 supplyAmount) = repayAndSupplyAmount(oldPrincipal, newPrincipal); + + // Reserves are decreased by increasing total supply and decreasing borrows + // the amount of debt repaid by reserves is `newBalance - oldBalance` + totalSupplyBase += supplyAmount; + totalBorrowBase -= repayAmount; + + uint256 basePaidOut = unsigned256(newBalance - oldBalance); + uint256 valueOfBasePaidOut = mulPrice(basePaidOut, basePrice, uint64(baseScale)); + emit AbsorbDebt(absorber, account, basePaidOut, valueOfBasePaidOut); + + if (newPrincipal > 0) { + emit Transfer(address(0), account, presentValueSupply(baseSupplyIndex, unsigned104(newPrincipal))); + } + } + + /** + * @notice Buy collateral from the protocol using base tokens, increasing protocol reserves + A minimum collateral amount should be specified to indicate the maximum slippage acceptable for the buyer. + * @param asset The asset to buy + * @param minAmount The minimum amount of collateral tokens that should be received by the buyer + * @param baseAmount The amount of base tokens used to buy the collateral + * @param recipient The recipient address + */ + function buyCollateral(address asset, uint minAmount, uint baseAmount, address recipient) override external nonReentrant { + if (isBuyPaused()) revert Paused(); + + int reserves = getReserves(); + if (reserves >= 0 && uint(reserves) >= targetReserves) revert NotForSale(); + + // Note: Re-entrancy can skip the reserves check above on a second buyCollateral call. + baseAmount = doTransferIn(baseToken, msg.sender, baseAmount); + + uint collateralAmount = quoteCollateral(asset, baseAmount); + if (collateralAmount < minAmount) revert TooMuchSlippage(); + if (collateralAmount > getCollateralReserves(asset)) revert InsufficientReserves(); + + // Note: Pre-transfer hook can re-enter buyCollateral with a stale collateral ERC20 balance. + // Assets should not be listed which allow re-entry from pre-transfer now, as too much collateral could be bought. + // This is also a problem if quoteCollateral derives its discount from the collateral ERC20 balance. + doTransferOut(asset, recipient, safe128(collateralAmount)); + + emit BuyCollateral(msg.sender, asset, baseAmount, collateralAmount); + } + + /** + * @notice Gets the quote for a collateral asset in exchange for an amount of base asset + * @param asset The collateral asset to get the quote for + * @param baseAmount The amount of the base asset to get the quote for + * @return The quote in terms of the collateral asset + */ + function quoteCollateral(address asset, uint baseAmount) override public view returns (uint) { + AssetInfo memory assetInfo = getAssetInfoByAddress(asset); + uint256 assetPrice = getPrice(assetInfo.priceFeed); + // Store front discount is derived from the collateral asset's liquidationFactor and storeFrontPriceFactor + // discount = storeFrontPriceFactor * (1e18 - liquidationFactor) + uint256 discountFactor = mulFactor(storeFrontPriceFactor, FACTOR_SCALE - assetInfo.liquidationFactor); + uint256 assetPriceDiscounted = mulFactor(assetPrice, FACTOR_SCALE - discountFactor); + uint256 basePrice = getPrice(baseTokenPriceFeed); + // # of collateral assets + // = (TotalValueOfBaseAmount / DiscountedPriceOfCollateralAsset) * assetScale + // = ((basePrice * baseAmount / baseScale) / assetPriceDiscounted) * assetScale + return basePrice * baseAmount * assetInfo.scale / assetPriceDiscounted / baseScale; + } + + /** + * @notice Withdraws base token reserves if called by the governor + * @param to An address of the receiver of withdrawn reserves + * @param amount The amount of reserves to be withdrawn from the protocol + */ + function withdrawReserves(address to, uint amount) override external { + if (msg.sender != governor) revert Unauthorized(); + + int reserves = getReserves(); + if (reserves < 0 || amount > unsigned256(reserves)) revert InsufficientReserves(); + + doTransferOut(baseToken, to, amount); + + emit WithdrawReserves(to, amount); + } + + /** + * @notice Sets Comet's ERC20 allowance of an asset for a manager + * @dev Only callable by governor + * @dev Note: Setting the `asset` as Comet's address will allow the manager + * to withdraw from Comet's Comet balance + * @dev Note: For USDT, if there is non-zero prior allowance, it must be reset to 0 first before setting a new value in proposal + * @param asset The asset that the manager will gain approval of + * @param manager The account which will be allowed or disallowed + * @param amount The amount of an asset to approve + */ + function approveThis(address manager, address asset, uint amount) override external { + if (msg.sender != governor) revert Unauthorized(); + + IERC20NonStandard(asset).approve(manager, amount); + } + + /** + * @notice Get the total number of tokens in circulation + * @dev Note: uses updated interest indices to calculate + * @return The supply of tokens + **/ + function totalSupply() override external view returns (uint256) { + (uint64 baseSupplyIndex_, ) = accruedInterestIndices(getNowInternal() - lastAccrualTime); + return presentValueSupply(baseSupplyIndex_, totalSupplyBase); + } + + /** + * @notice Get the total amount of debt + * @dev Note: uses updated interest indices to calculate + * @return The amount of debt + **/ + function totalBorrow() override external view returns (uint256) { + (, uint64 baseBorrowIndex_) = accruedInterestIndices(getNowInternal() - lastAccrualTime); + return presentValueBorrow(baseBorrowIndex_, totalBorrowBase); + } + + /** + * @notice Query the current positive base balance of an account or zero + * @dev Note: uses updated interest indices to calculate + * @param account The account whose balance to query + * @return The present day base balance magnitude of the account, if positive + */ + function balanceOf(address account) override public view returns (uint256) { + (uint64 baseSupplyIndex_, ) = accruedInterestIndices(getNowInternal() - lastAccrualTime); + int104 principal = userBasic[account].principal; + return principal > 0 ? presentValueSupply(baseSupplyIndex_, unsigned104(principal)) : 0; + } + + /** + * @notice Query the current negative base balance of an account or zero + * @dev Note: uses updated interest indices to calculate + * @param account The account whose balance to query + * @return The present day base balance magnitude of the account, if negative + */ + function borrowBalanceOf(address account) override public view returns (uint256) { + (, uint64 baseBorrowIndex_) = accruedInterestIndices(getNowInternal() - lastAccrualTime); + int104 principal = userBasic[account].principal; + return principal < 0 ? presentValueBorrow(baseBorrowIndex_, unsigned104(-principal)) : 0; + } + + /** + * @notice Fallback to calling the extension delegate for everything else + */ + fallback() external payable { + address delegate = extensionDelegate; + assembly { + calldatacopy(0, 0, calldatasize()) + let result := delegatecall(gas(), delegate, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch result + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } +} \ No newline at end of file diff --git a/contracts/CometFactoryExtendedAssetList.sol b/contracts/CometFactoryExtendedAssetList.sol new file mode 100644 index 000000000..5d30705f7 --- /dev/null +++ b/contracts/CometFactoryExtendedAssetList.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "./CometExtendedAssetList.sol"; +import "./CometConfiguration.sol"; + +contract CometFactoryExtendedAssetList is CometConfiguration { + function clone(Configuration calldata config) external returns (address) { + return address(new CometExtendedAssetList(config)); + } +} \ No newline at end of file diff --git a/contracts/IAssetList.sol b/contracts/IAssetList.sol new file mode 100644 index 000000000..584e6cc2b --- /dev/null +++ b/contracts/IAssetList.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "./CometCore.sol"; + +/** + * @title Compound's Asset List + * @author Compound + */ +interface IAssetList { + function getAssetInfo(uint8 i) external view returns (CometCore.AssetInfo memory); +} \ No newline at end of file diff --git a/contracts/IAssetListFactory.sol b/contracts/IAssetListFactory.sol new file mode 100644 index 000000000..e1cd91288 --- /dev/null +++ b/contracts/IAssetListFactory.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; +import "./CometCore.sol"; + +/** + * @title Compound's Asset List Factory + * @author Compound + */ +interface IAssetListFactory { + function createAssetList(CometCore.AssetConfig[] memory assetConfigs) external returns (address assetList); + // add it here to save space in Comet + function assetListFactory() external view returns (address); +} \ No newline at end of file diff --git a/deployments/mainnet/weth/migrations/1723198576_update_comet_to_support_more_collaterals.ts b/deployments/mainnet/weth/migrations/1723198576_update_comet_to_support_more_collaterals.ts new file mode 100644 index 000000000..1d971dbfa --- /dev/null +++ b/deployments/mainnet/weth/migrations/1723198576_update_comet_to_support_more_collaterals.ts @@ -0,0 +1,112 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { proposal } from '../../../../src/deploy'; +import { ethers } from 'ethers'; +import { Contract } from 'ethers'; + +let newCometExtAddress: string; + +export default migration('1723198576_update_comet_to_support_more_collaterals', { + async prepare(deploymentManager: DeploymentManager) { + const _assetListFactory = await deploymentManager.deploy( + 'assetListFactory', + 'AssetListFactory.sol', + [] + ); + + const cometFactoryExtendedAssetList = await deploymentManager.deploy( + 'cometFactoryExtendedAssetList', + 'CometFactoryExtendedAssetList.sol', + [] + ); + + const _newCometExt = await deploymentManager.deploy( + 'CometExt', + 'CometExt.sol', + [ + { + name32: ethers.utils.formatBytes32String('Compound WETH'), + symbol32: ethers.utils.formatBytes32String('cWETHv3') + }, + _assetListFactory.address + ] + ); + return { + cometFactoryExtendedAssetList: cometFactoryExtendedAssetList.address, + newCometExt: _newCometExt.address + }; + }, + + async enact(deploymentManager: DeploymentManager, _, { + cometFactoryExtendedAssetList, + newCometExt, + }) { + + const trace = deploymentManager.tracer(); + const { + governor, + comet, + cometAdmin, + configurator, + } = await deploymentManager.getContracts(); + + newCometExtAddress = newCometExt; + + const mainnetActions = [ + // 1. Set the factory in the Configurator + { + contract: configurator, + signature: 'setFactory(address,address)', + args: [comet.address, cometFactoryExtendedAssetList], + }, + // 2. Set new CometExt as the extension delegate + { + contract: configurator, + signature: 'setExtensionDelegate(address,address)', + args: [comet.address, newCometExt], + }, + // 3. Deploy and upgrade to a new version of Comet + { + contract: cometAdmin, + signature: 'deployAndUpgradeTo(address,address)', + args: [configurator.address, comet.address], + }, + ]; + + const description = 'DESCRIPTION'; + const txn = await deploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ) + ); + + const event = txn.events.find( + (event) => event.event === 'ProposalCreated' + ); + const [proposalId] = event.args; + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(): Promise { + return false; + }, + + async verify(deploymentManager: DeploymentManager) { + const { comet } = await deploymentManager.getContracts(); + + const cometNew = new Contract( + comet.address, + [ + 'function assetList() external view returns (address)', + ], + deploymentManager.hre.ethers.provider + ); + + const assetListAddress = await cometNew.assetList(); + + expect(assetListAddress).to.not.be.equal(ethers.constants.AddressZero); + + expect(await comet.extensionDelegate()).to.be.equal(newCometExtAddress); + }, +}); diff --git a/scenario/LiquidationBotScenario.ts b/scenario/LiquidationBotScenario.ts index bf21963b7..77dbfe95c 100644 --- a/scenario/LiquidationBotScenario.ts +++ b/scenario/LiquidationBotScenario.ts @@ -767,7 +767,7 @@ scenario( const assetAmounts = { mainnet: { usdc: ' == 5000', // COMP - weth: ' == 7000', // CB_ETH + weth: ' == 3400', // CB_ETH usdt: ' == 5000', // COMP }, }; diff --git a/src/deploy/Network.ts b/src/deploy/Network.ts index 9f6f027d1..aebcc9d18 100644 --- a/src/deploy/Network.ts +++ b/src/deploy/Network.ts @@ -119,6 +119,8 @@ export async function deployNetworkComet( rewardTokenAddress } = await getConfiguration(deploymentManager, configOverrides); + const assetListFactory = await deploymentManager.deploy('assetListFactory', 'AssetListFactory.sol', [], maybeForce()); + /* Deploy contracts */ const cometAdmin = await deploymentManager.deploy( @@ -135,7 +137,7 @@ export async function deployNetworkComet( const cometExt = await deploymentManager.deploy( 'comet:implementation:implementation', 'CometExt.sol', - [extConfiguration], + [extConfiguration, assetListFactory.address], maybeForce(deploySpec.cometExt) ); diff --git a/test/asset-info-test-asset-list-comet.ts b/test/asset-info-test-asset-list-comet.ts new file mode 100644 index 000000000..dd1388074 --- /dev/null +++ b/test/asset-info-test-asset-list-comet.ts @@ -0,0 +1,184 @@ +import { expect, exp, makeConfigurator, ONE } from './helpers'; +import { SimplePriceFeed__factory, FaucetToken__factory, CometExt__factory, CometExtendedAssetList__factory, AssetListFactory__factory } from '../build/types'; + +import { ethers } from 'hardhat'; +describe('asset info', function () { + it('initializes protocol', async () => { + const { cometExtendedAssetList: comet, tokens } = await makeConfigurator({ + assets: { + USDC: {}, + ASSET1: {}, + ASSET2: {}, + ASSET3: {}, + }, + reward: 'ASSET1', + }); + + const cometNumAssets = await comet.numAssets(); + expect(cometNumAssets).to.be.equal(3); + + const assetInfo00 = await comet.getAssetInfo(0); + expect(assetInfo00.asset).to.be.equal(tokens['ASSET1'].address); + expect(assetInfo00.borrowCollateralFactor).to.equal(ONE - exp(1, 14)); + expect(assetInfo00.liquidateCollateralFactor).to.equal(ONE); + + const assetInfo01 = await comet.getAssetInfo(1); + expect(assetInfo01.asset).to.be.equal(tokens['ASSET2'].address); + expect(assetInfo01.borrowCollateralFactor).to.equal(ONE - exp(1, 14)); + expect(assetInfo01.liquidateCollateralFactor).to.equal(ONE); + + const assetInfo02 = await comet.getAssetInfo(2); + expect(assetInfo02.asset).to.be.equal(tokens['ASSET3'].address); + expect(assetInfo02.borrowCollateralFactor).to.equal(ONE - exp(1, 14)); + expect(assetInfo02.liquidateCollateralFactor).to.equal(ONE); + }); + + it('do NOT reverts if too many assets are passed', async () => { + const signers = await ethers.getSigners(); + + const assets = { + USDC: {}, + ASSET1: {}, + ASSET2: {}, + ASSET3: {}, + ASSET4: {}, + ASSET5: {}, + ASSET6: {}, + ASSET7: {}, + ASSET8: {}, + ASSET9: {}, + ASSET10: {}, + ASSET11: {}, + ASSET12: {}, + ASSET13: {}, + ASSET14: {}, + ASSET15: {}, + ASSET16: {}, + ASSET17: {}, + ASSET18: {}, + ASSET19: {}, + ASSET20: {}, + }; + let priceFeeds = {}; + const PriceFeedFactory = (await ethers.getContractFactory('SimplePriceFeed')) as SimplePriceFeed__factory; + for (const asset in assets) { + const initialPrice = exp(assets[asset].initialPrice || 1, 8); + const priceFeedDecimals = assets[asset].priceFeedDecimals || 8; + const priceFeed = await PriceFeedFactory.deploy(initialPrice, priceFeedDecimals); + await priceFeed.deployed(); + priceFeeds[asset] = priceFeed; + } + + const name32 = ethers.utils.formatBytes32String(('Compound Comet')); + const symbol32 = ethers.utils.formatBytes32String(('📈BASE')); + const governor = signers[0]; + const pauseGuardian = signers[1]; + const base = 'USDC'; + const supplyKink = exp(0.8, 18); + const supplyPerYearInterestRateBase = exp(0.0, 18); + const supplyPerYearInterestRateSlopeLow = exp(0.05, 18); + const supplyPerYearInterestRateSlopeHigh = exp(2, 18); + const borrowKink = exp(0.8, 18); + const borrowPerYearInterestRateBase = exp(0.005, 18); + const borrowPerYearInterestRateSlopeLow = exp(0.1, 18); + const borrowPerYearInterestRateSlopeHigh = exp(3, 18); + const storeFrontPriceFactor = ONE; + const trackingIndexScale = exp(1, 15); + const baseTrackingSupplySpeed = trackingIndexScale; + const baseTrackingBorrowSpeed = trackingIndexScale; + const baseMinForRewards = exp(1, 18); + const baseBorrowMin = exp(1, 18); + const targetReserves = 0; + + const FaucetFactory = (await ethers.getContractFactory('FaucetToken')) as FaucetToken__factory; + const tokens = {}; + for (const symbol in assets) { + const config = assets[symbol]; + const decimals = config.decimals || 18; + const initial = config.initial || 1e6; + const name = config.name || symbol; + const factory = config.factory || FaucetFactory; + let token = (tokens[symbol] = await factory.deploy(initial, name, decimals, symbol)); + await token.deployed(); + } + const AssetListFactory = (await ethers.getContractFactory('AssetListFactory')) as AssetListFactory__factory; + const assetListFactory = await AssetListFactory.deploy(); + await assetListFactory.deployed(); + + const CometExtFactory = (await ethers.getContractFactory('CometExt')) as CometExt__factory; + const extensionDelegate = await CometExtFactory.deploy({ name32, symbol32 }, assetListFactory.address); + await extensionDelegate.deployed(); + + + const config = { + governor: governor.address, + pauseGuardian: pauseGuardian.address, + extensionDelegate: extensionDelegate.address, + baseToken: tokens[base].address, + baseTokenPriceFeed: priceFeeds[base].address, + supplyKink, + supplyPerYearInterestRateBase, + supplyPerYearInterestRateSlopeLow, + supplyPerYearInterestRateSlopeHigh, + borrowKink, + borrowPerYearInterestRateBase, + borrowPerYearInterestRateSlopeLow, + borrowPerYearInterestRateSlopeHigh, + storeFrontPriceFactor, + trackingIndexScale, + baseTrackingSupplySpeed, + baseTrackingBorrowSpeed, + baseMinForRewards, + baseBorrowMin, + targetReserves, + assetConfigs: Object.entries(assets).reduce((acc, [symbol], _i) => { + if (symbol != base) { + acc.push({ + asset: tokens[symbol].address, + priceFeed: priceFeeds[symbol].address, + decimals: 18, + borrowCollateralFactor: ONE - 1n, + liquidateCollateralFactor: ONE, + liquidationFactor: ONE, + supplyCap: exp(100, 18), + }); + } + return acc; + }, []), + }; + const CometExtendedAssetList = (await ethers.getContractFactory('CometExtendedAssetList')) as CometExtendedAssetList__factory; + await expect(CometExtendedAssetList.deploy(config)).to.not.be.reverted; + }); + + it('reverts if index is greater than numAssets', async () => { + const { cometExtendedAssetList } = await makeConfigurator(); + await expect(cometExtendedAssetList.getAssetInfo(3)).to.be.revertedWith("custom error 'BadAsset()'"); + }); + + it('reverts if collateral factors are out of range', async () => { + await expect(makeConfigurator({ + assets: { + USDC: {}, + ASSET1: {borrowCF: exp(0.9, 18), liquidateCF: exp(0.9, 18)}, + ASSET2: {}, + }, + })).to.be.revertedWith("custom error 'BorrowCFTooLarge()'"); + + // check descaled factors + await expect(makeConfigurator({ + assets: { + USDC: {}, + ASSET1: {borrowCF: exp(0.9, 18), liquidateCF: exp(0.9, 18) + 1n}, + ASSET2: {}, + }, + })).to.be.revertedWith("custom error 'BorrowCFTooLarge()'"); + + await expect(makeConfigurator({ + assets: { + USDC: {}, + ASSET1: {borrowCF: exp(0.99, 18), liquidateCF: exp(1.1, 18)}, + ASSET2: {}, + }, + })).to.be.revertedWith("custom error 'LiquidateCFTooLarge()'"); + }); +}); diff --git a/test/constructor-test.ts b/test/constructor-test.ts index 3adfc8d89..8c070d70d 100644 --- a/test/constructor-test.ts +++ b/test/constructor-test.ts @@ -4,6 +4,7 @@ import { CometHarness__factory, FaucetToken__factory, SimplePriceFeed__factory, + AssetListFactory__factory, } from '../build/types'; describe('constructor', function () { @@ -16,13 +17,16 @@ describe('constructor', function () { it('verifies asset scales', async function () { const [governor, pauseGuardian] = await ethers.getSigners(); - + const AssetListFactory = (await ethers.getContractFactory('AssetListFactory')) as AssetListFactory__factory; + const assetListFactory = await AssetListFactory.deploy(); + await assetListFactory.deployed(); + // extension delegate const CometExtFactory = (await ethers.getContractFactory('CometExt')) as CometExt__factory; const extensionDelegate = await CometExtFactory.deploy({ name32: ethers.utils.formatBytes32String('Compound Comet'), - symbol32: ethers.utils.formatBytes32String('📈BASE') - }); + symbol32: ethers.utils.formatBytes32String('📈BASE'), + }, assetListFactory.address); await extensionDelegate.deployed(); // tokens diff --git a/test/helpers.ts b/test/helpers.ts index 58acf19af..01df1b12b 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -32,6 +32,10 @@ import { CometInterface, NonStandardFaucetFeeToken, NonStandardFaucetFeeToken__factory, + CometExtendedAssetList, + CometExtendedAssetList__factory, + AssetListFactory, + AssetListFactory__factory, } from '../build/types'; import { BigNumber } from 'ethers'; import { TransactionReceipt, TransactionResponse } from '@ethersproject/abstract-provider'; @@ -98,6 +102,8 @@ export type Protocol = { base: string; reward: string; comet: Comet; + cometExtendedAssetList: CometExtendedAssetList; + assetListFactory: AssetListFactory; tokens: { [symbol: string]: FaucetToken | NonStandardFaucetFeeToken; }; @@ -270,15 +276,19 @@ export async function makeProtocol(opts: ProtocolOpts = {}): Promise { const unsupportedToken = await FaucetFactory.deploy(1e6, 'Unsupported Token', 6, 'USUP'); + const AssetListFactory = (await ethers.getContractFactory('AssetListFactory')) as AssetListFactory__factory; + const assetListFactory = await AssetListFactory.deploy(); + await assetListFactory.deployed(); + let extensionDelegate = opts.extensionDelegate; if (extensionDelegate === undefined) { const CometExtFactory = (await ethers.getContractFactory('CometExt')) as CometExt__factory; - extensionDelegate = await CometExtFactory.deploy({ name32, symbol32 }); + extensionDelegate = await CometExtFactory.deploy({ name32, symbol32 }, assetListFactory.address); await extensionDelegate.deployed(); } const CometFactory = (await ethers.getContractFactory('CometHarness')) as CometHarness__factory; - const comet = await CometFactory.deploy({ + const config = { governor: governor.address, pauseGuardian: pauseGuardian.address, extensionDelegate: extensionDelegate.address, @@ -313,12 +323,19 @@ export async function makeProtocol(opts: ProtocolOpts = {}): Promise { } return acc; }, []), - }); + }; + const comet = await CometFactory.deploy(config); await comet.deployed(); + const CometExtendedAssetList = (await ethers.getContractFactory('CometExtendedAssetList')) as CometExtendedAssetList__factory; + const cometExtendedAssetList = await CometExtendedAssetList.deploy(config); + await cometExtendedAssetList.deployed(); + if (opts.start) await ethers.provider.send('evm_setNextBlockTimestamp', [opts.start]); await comet.initializeStorage(); + await cometExtendedAssetList.initializeStorage(); + const baseTokenBalance = opts.baseTokenBalance; if (baseTokenBalance) { const baseToken = tokens[base]; @@ -334,6 +351,8 @@ export async function makeProtocol(opts: ProtocolOpts = {}): Promise { base, reward, comet: await ethers.getContractAt('CometHarnessInterface', comet.address) as Comet, + cometExtendedAssetList: cometExtendedAssetList, + assetListFactory: assetListFactory, tokens, unsupportedToken, priceFeeds, @@ -352,6 +371,8 @@ export async function makeConfigurator(opts: ProtocolOpts = {}): Promise