diff --git a/.eslintrc.js b/.eslintrc.js index 0fed4397c..a0d2140f2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,6 +28,7 @@ module.exports = { 'unix' ], 'no-constant-condition': ['error', { checkLoops: false }], + 'no-inner-declarations': 'off', 'no-prototype-builtins': 'off', 'no-unused-vars': 'off', 'prefer-const': 'off', diff --git a/.github/workflows/deploy-market.yaml b/.github/workflows/deploy-market.yaml index 58bcf7409..6889852d6 100644 --- a/.github/workflows/deploy-market.yaml +++ b/.github/workflows/deploy-market.yaml @@ -17,6 +17,8 @@ on: - base - base-goerli - linea-goerli + - scroll-goerli + - scroll deployment: description: Deployment Name (e.g. "usdc") required: true @@ -44,7 +46,7 @@ jobs: with: wallet_connect_project_id: ${{ secrets.WALLET_CONNECT_PROJECT_ID }} requested_network: "${{ inputs.network }}" - ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"sepolia\":\"https://sepolia.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\",\"base\":\"https://clean-spring-wind.base-mainnet.discover.quiknode.pro/$QUICKNODE_KEY\",\"base-goerli\":\"https://base-goerli.infura.io/v3/$INFURA_KEY\",\"linea-goerli\":\"https://linea-goerli.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"sepolia\":\"https://sepolia.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\",\"base\":\"https://clean-spring-wind.base-mainnet.discover.quiknode.pro/$QUICKNODE_KEY\",\"base-goerli\":\"https://base-goerli.infura.io/v3/$INFURA_KEY\",\"linea-goerli\":\"https://linea-goerli.infura.io/v3/$INFURA_KEY\",\"scroll-goerli\":\"https://alpha-rpc.scroll.io/l2\",\"scroll\":\"https://rpc.scroll.io\"}')[inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' diff --git a/.github/workflows/enact-migration.yaml b/.github/workflows/enact-migration.yaml index 14461c63f..2ecc8c8ec 100644 --- a/.github/workflows/enact-migration.yaml +++ b/.github/workflows/enact-migration.yaml @@ -17,6 +17,8 @@ on: - base - base-goerli - linea-goerli + - scroll-goerli + - scroll deployment: description: Deployment Name (e.g. "usdc") required: true @@ -52,7 +54,7 @@ jobs: case ${{ github.event.inputs.network }} in polygon | arbitrum | base) echo "GOV_NETWORK=mainnet" >> $GITHUB_ENV ;; - mumbai | arbitrum-goerli | base-goerli | linea-goerli) + mumbai | arbitrum-goerli | base-goerli | linea-goerli | scroll-goerli | scroll) echo "GOV_NETWORK=goerli" >> $GITHUB_ENV ;; *) echo "No governance network for selected network" ;; @@ -63,7 +65,7 @@ jobs: with: wallet_connect_project_id: ${{ secrets.WALLET_CONNECT_PROJECT_ID }} requested_network: "${{ inputs.network }}" - ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"sepolia\":\"https://sepolia.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\",\"base\":\"https://clean-spring-wind.base-mainnet.discover.quiknode.pro/$QUICKNODE_KEY\",\"base-goerli\":\"https://base-goerli.infura.io/v3/$INFURA_KEY\",\"linea-goerli\":\"https://linea-goerli.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"sepolia\":\"https://sepolia.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\",\"base\":\"https://clean-spring-wind.base-mainnet.discover.quiknode.pro/$QUICKNODE_KEY\",\"base-goerli\":\"https://base-goerli.infura.io/v3/$INFURA_KEY\",\"linea-goerli\":\"https://linea-goerli.infura.io/v3/$INFURA_KEY\",\"scroll-goerli\":\"https://alpha-rpc.scroll.io/l2\",\"scroll\":\"https://rpc.scroll.io\"}')[inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' diff --git a/.github/workflows/prepare-migration.yaml b/.github/workflows/prepare-migration.yaml index 4f734e2de..3701da08d 100644 --- a/.github/workflows/prepare-migration.yaml +++ b/.github/workflows/prepare-migration.yaml @@ -46,7 +46,7 @@ jobs: with: wallet_connect_project_id: ${{ secrets.WALLET_CONNECT_PROJECT_ID }} requested_network: "${{ inputs.network }}" - ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"sepolia\":\"https://sepolia.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\",\"base\":\"https://clean-spring-wind.base-mainnet.discover.quiknode.pro/$QUICKNODE_KEY\",\"base-goerli\":\"https://base-goerli.infura.io/v3/$INFURA_KEY\",\"linea-goerli\":\"https://linea-goerli.infura.io/v3/$INFURA_KEY\"}')[inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"https://mainnet.infura.io/v3/$INFURA_KEY\",\"goerli\":\"https://goerli.infura.io/v3/$INFURA_KEY\",\"sepolia\":\"https://sepolia.infura.io/v3/$INFURA_KEY\",\"mumbai\":\"https://polygon-mumbai.infura.io/v3/$INFURA_KEY\",\"polygon\":\"https://polygon-mainnet.infura.io/v3/$INFURA_KEY\",\"arbitrum-goerli\":\"https://arbitrum-goerli.infura.io/v3/$INFURA_KEY\",\"arbitrum\":\"https://arbitrum-mainnet.infura.io/v3/$INFURA_KEY\",\"base\":\"https://clean-spring-wind.base-mainnet.discover.quiknode.pro/$QUICKNODE_KEY\",\"base-goerli\":\"https://base-goerli.infura.io/v3/$INFURA_KEY\",\"linea-goerli\":\"https://linea-goerli.infura.io/v3/$INFURA_KEY\",\"scroll-goerli\":\"https://alpha-rpc.scroll.io/l2\",\"scroll\":\"https://rpc.scroll.io\"}')[inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' diff --git a/.github/workflows/run-scenarios.yaml b/.github/workflows/run-scenarios.yaml index 97237fe13..8acd648f5 100644 --- a/.github/workflows/run-scenarios.yaml +++ b/.github/workflows/run-scenarios.yaml @@ -7,7 +7,7 @@ jobs: strategy: fail-fast: false matrix: - bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, arbitrum-usdc.e, arbitrum-usdc, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-goerli, base-goerli-weth, linea-goerli] + bases: [ development, mainnet, mainnet-weth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, arbitrum-usdc.e, arbitrum-usdc, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-goerli, base-goerli-weth, linea-goerli, scroll-goerli, scroll-usdc] name: Run scenarios env: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }} diff --git a/contracts/ScrollComet.sol b/contracts/ScrollComet.sol new file mode 100644 index 000000000..26cafd500 --- /dev/null +++ b/contracts/ScrollComet.sol @@ -0,0 +1,1324 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "./CometMainInterface.sol"; +import "./ERC20.sol"; +import "./IPriceFeed.sol"; + +/** + * @title Compound's Comet Contract on Scroll + * @notice An efficient monolithic money market protocol + * @author Compound + */ +contract ScrollComet 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; + + /** Collateral asset configuration (packed) **/ + + uint256 internal immutable asset00_a; + uint256 internal immutable asset00_b; + uint256 internal immutable asset01_a; + uint256 internal immutable asset01_b; + uint256 internal immutable asset02_a; + uint256 internal immutable asset02_b; + uint256 internal immutable asset03_a; + uint256 internal immutable asset03_b; + uint256 internal immutable asset04_a; + uint256 internal immutable asset04_b; + uint256 internal immutable asset05_a; + uint256 internal immutable asset05_b; + uint256 internal immutable asset06_a; + uint256 internal immutable asset06_b; + uint256 internal immutable asset07_a; + uint256 internal immutable asset07_b; + uint256 internal immutable asset08_a; + uint256 internal immutable asset08_b; + uint256 internal immutable asset09_a; + uint256 internal immutable asset09_b; + uint256 internal immutable asset10_a; + uint256 internal immutable asset10_b; + uint256 internal immutable asset11_a; + uint256 internal immutable asset11_b; + uint256 internal immutable asset12_a; + uint256 internal immutable asset12_b; + uint256 internal immutable asset13_a; + uint256 internal immutable asset13_b; + uint256 internal immutable asset14_a; + uint256 internal immutable asset14_b; + + /** + * @notice Construct a new protocol instance + * @param config The mapping of initial/constant parameters + **/ + constructor(Configuration memory config) { + // Sanity checks + uint8 decimals_ = ERC20(config.baseToken).decimals(); + if (decimals_ > MAX_BASE_DECIMALS) revert BadDecimals(); + if (config.storeFrontPriceFactor > FACTOR_SCALE) revert BadDiscount(); + if (config.assetConfigs.length > MAX_ASSETS) revert TooManyAssets(); + 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); + + (asset00_a, asset00_b) = getPackedAssetInternal(config.assetConfigs, 0); + (asset01_a, asset01_b) = getPackedAssetInternal(config.assetConfigs, 1); + (asset02_a, asset02_b) = getPackedAssetInternal(config.assetConfigs, 2); + (asset03_a, asset03_b) = getPackedAssetInternal(config.assetConfigs, 3); + (asset04_a, asset04_b) = getPackedAssetInternal(config.assetConfigs, 4); + (asset05_a, asset05_b) = getPackedAssetInternal(config.assetConfigs, 5); + (asset06_a, asset06_b) = getPackedAssetInternal(config.assetConfigs, 6); + (asset07_a, asset07_b) = getPackedAssetInternal(config.assetConfigs, 7); + (asset08_a, asset08_b) = getPackedAssetInternal(config.assetConfigs, 8); + (asset09_a, asset09_b) = getPackedAssetInternal(config.assetConfigs, 9); + (asset10_a, asset10_b) = getPackedAssetInternal(config.assetConfigs, 10); + (asset11_a, asset11_b) = getPackedAssetInternal(config.assetConfigs, 11); + (asset12_a, asset12_b) = getPackedAssetInternal(config.assetConfigs, 12); + (asset13_a, asset13_b) = getPackedAssetInternal(config.assetConfigs, 13); + (asset14_a, asset14_b) = getPackedAssetInternal(config.assetConfigs, 14); + } + + /** + * @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; + } + + /** + * @dev Checks and gets the packed asset info for storage + */ + function getPackedAssetInternal(AssetConfig[] memory assetConfigs, uint i) internal view returns (uint256, uint256) { + 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 BadDecimals(); + if (ERC20(asset).decimals() != decimals_) revert BadDecimals(); + + // Ensure collateral factors are within range + if (assetConfig.borrowCollateralFactor >= assetConfig.liquidateCollateralFactor) revert BorrowCFTooLarge(); + if (assetConfig.liquidateCollateralFactor > MAX_COLLATERAL_FACTOR) revert 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 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) override public view 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 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 ERC20(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 = ERC20(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, assumes no fee is charged and amount is transferred + */ + function doTransferIn(address asset, address from, uint amount) internal { + bool success = ERC20(asset).transferFrom(from, address(this), amount); + if (!success) revert TransferInFailed(); + } + + /** + * @dev Safe ERC20 transfer out + */ + function doTransferOut(address asset, address to, uint amount) internal { + bool success = ERC20(asset).transfer(to, amount); + 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 { + 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 { + 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 { + 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 { + 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 { + 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); + 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 { + 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. + 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 + * @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(); + + ERC20(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()) } + } + } +} diff --git a/contracts/bridges/scroll/IScrollMessenger.sol b/contracts/bridges/scroll/IScrollMessenger.sol new file mode 100644 index 000000000..e88260f9f --- /dev/null +++ b/contracts/bridges/scroll/IScrollMessenger.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +/// @notice IScrollMessenger is the interface for Scroll's messenger contract +interface IScrollMessenger { + function xDomainMessageSender() external view returns (address); +} diff --git a/contracts/bridges/scroll/ScrollBridgeReceiver.sol b/contracts/bridges/scroll/ScrollBridgeReceiver.sol new file mode 100644 index 000000000..6cfc5b11d --- /dev/null +++ b/contracts/bridges/scroll/ScrollBridgeReceiver.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "../SweepableBridgeReceiver.sol"; +import "./IScrollMessenger.sol"; + +/// @title Scroll Bridge Receiver +/// @notice Contract that processes messages passed from Compound governance using the Scroll bridge +contract ScrollBridgeReceiver is SweepableBridgeReceiver { + error InvalidL2Messenger(); + + event NewL2Messenger(address indexed oldL2Messenger, address indexed newL2Messenger); + + /// @notice Address of Scroll L2 Messenger contract + address public l2Messenger; + + /// @notice Construct a new ScrollBridgeReceiver instance + /// @param l2Messenger_ Address of Scroll L2 Messenger contract + constructor(address l2Messenger_) { + l2Messenger = l2Messenger_; + emit NewL2Messenger(address(0), l2Messenger_); + } + + /// @notice Update the L2 Messenger address + /// @param newL2Messenger New address for L2 Messenger contract + function changeL2Messenger(address newL2Messenger) public { + if (msg.sender != localTimelock) revert Unauthorized(); + address oldL2Messenger = l2Messenger; + l2Messenger = newL2Messenger; + emit NewL2Messenger(oldL2Messenger, newL2Messenger); + } + + /// @notice Fallback function to handle messages + fallback() external payable { + if (msg.sender != l2Messenger) revert InvalidL2Messenger(); + address messageSender = IScrollMessenger(msg.sender).xDomainMessageSender(); + processMessage(messageSender, msg.data); + } +} \ No newline at end of file diff --git a/deployments/goerli/usdc/relations.ts b/deployments/goerli/usdc/relations.ts index f9edfd59b..d9277c42c 100644 --- a/deployments/goerli/usdc/relations.ts +++ b/deployments/goerli/usdc/relations.ts @@ -63,5 +63,19 @@ export default { slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' } } + }, + scrollMessenger: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + scrollL1TokenBridge: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } } }; diff --git a/deployments/goerli/usdc/roots.json b/deployments/goerli/usdc/roots.json index f693adeb1..02a9e9b69 100644 --- a/deployments/goerli/usdc/roots.json +++ b/deployments/goerli/usdc/roots.json @@ -14,5 +14,7 @@ "lineaL1TokenBridge": "0xaA012D038E6440535Ec66eDf2DA592F4F8398133", "lineaL1usdcBridge": "0x9c556D2cCfb6157E4A6305aa9963EdD6ca5047cB", "CCTPTokenMessenger": "0xd0c3da58f55358142b8d3e06c1c30c5c6114efe8", - "CCTPMessageTransmitter": "0x26413e8157cd32011e726065a5462e97dd4d03d9" + "CCTPMessageTransmitter": "0x26413e8157cd32011e726065a5462e97dd4d03d9", + "scrollMessenger": "0x5260e38080BFe97e6C4925d9209eCc5f964373b6", + "scrollL1TokenBridge": "0xe5E30E7c24e4dFcb281A682562E53154C15D3332" } \ No newline at end of file diff --git a/deployments/mainnet/usdc/relations.ts b/deployments/mainnet/usdc/relations.ts index a076adcaa..a15c9370d 100644 --- a/deployments/mainnet/usdc/relations.ts +++ b/deployments/mainnet/usdc/relations.ts @@ -41,5 +41,19 @@ export default { slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' } } + }, + scrollMessenger: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + scrollL1USDCGateway: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } } }; \ No newline at end of file diff --git a/deployments/mainnet/usdc/roots.json b/deployments/mainnet/usdc/roots.json index 0aa64b6cc..996d96beb 100644 --- a/deployments/mainnet/usdc/roots.json +++ b/deployments/mainnet/usdc/roots.json @@ -10,5 +10,7 @@ "CCTPTokenMessenger": "0xbd3fa81b58ba92a82136038b25adec7066af3155", "CCTPMessageTransmitter": "0x0a992d191deec32afe36203ad87d7d289a738f81", "baseL1CrossDomainMessenger": "0x866E82a600A1414e583f7F13623F1aC5d58b0Afa", - "baseL1StandardBridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35" + "baseL1StandardBridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", + "scrollMessenger": "0x6774Bcbd5ceCeF1336b5300fb5186a12DDD8b367", + "scrollL1USDCGateway": "0xf1AF3b23DE0A5Ca3CAb7261cb0061C0D779A5c7B" } \ No newline at end of file diff --git a/deployments/scroll-goerli/usdc/configuration.json b/deployments/scroll-goerli/usdc/configuration.json new file mode 100644 index 000000000..8d370324f --- /dev/null +++ b/deployments/scroll-goerli/usdc/configuration.json @@ -0,0 +1,37 @@ +{ + "name": "Compound USDC", + "symbol": "cUSDCv3", + "baseToken": "USDC", + "baseTokenAddress": "0x67aE69Fd63b4fc8809ADc224A9b82Be976039509", + "baseTokenPriceFeed": "0xbbb06bDcf221Ae565f458b990DE83Cbb3D27C2aA", + "borrowMin": "0.001e6", + "storeFrontPriceFactor": 0.5, + "targetReserves": "1000000e6", + "rates": { + "supplyKink": 0.8, + "supplySlopeLow": 0.0325, + "supplySlopeHigh": 0.4, + "supplyBase": 0, + "borrowKink": 0.8, + "borrowSlopeLow": 0.035, + "borrowSlopeHigh": 0.25, + "borrowBase": 0.015 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "0.000402083333333e15", + "baseBorrowSpeed": "0.000402083333333e15", + "baseMinForRewards": "1000e6" + }, + "assets": { + "WETH": { + "address": "0xa1EA0B2354F5A344110af2b6AD68e75545009a03", + "priceFeed": "0x2287BAF672879935cB944a2C050971515E73da65", + "decimals": "18", + "borrowCF": 0.775, + "liquidateCF": 0.825, + "liquidationFactor": 0.95, + "supplyCap": "10000000e18" + } + } +} diff --git a/deployments/scroll-goerli/usdc/deploy.ts b/deployments/scroll-goerli/usdc/deploy.ts new file mode 100644 index 000000000..93fb99f9b --- /dev/null +++ b/deployments/scroll-goerli/usdc/deploy.ts @@ -0,0 +1,98 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet, exp, wait } from '../../../src/deploy'; + +const secondsPerDay = 24 * 60 * 60; + +const GOERLI_TIMELOCK = '0x8Fa336EB4bF58Cfc508dEA1B0aeC7336f55B1399'; // L1 contract + +export default async function deploy( + deploymentManager: DeploymentManager, + deploySpec: DeploySpec +): Promise { + const trace = deploymentManager.tracer(); + const ethers = deploymentManager.hre.ethers; + + // Pull in existing assets + const WETH = await deploymentManager.existing( + 'WETH', + '0xa1EA0B2354F5A344110af2b6AD68e75545009a03', + 'scroll-goerli' + ); + + const l2Messenger = await deploymentManager.existing( + 'l2Messenger', + '0xb75d7e84517e1504C151B270255B087Fd746D34C', + 'scroll-goerli' + ); + + const l2ERC20Gateway = await deploymentManager.existing( + 'l2ERC20Gateway', + '0xB878F37BB278bf0e4974856fFe86f5e6F66BD725', + 'scroll-goerli' + ); + + const l2ETHGateway = await deploymentManager.existing( + 'l2ETHGateway', + '0x32139B5C8838E94fFcD83E60dff95Daa7F0bA14c', + 'scroll-goerli' + ); + + const l2WETHGateway = await deploymentManager.existing( + 'l2WETHGateway', + '0xBb88bF582F2BBa46702621dae5CB9271057bC85b', + 'scroll-goerli' + ); + + // Deploy ScrollBridgeReceiver + const bridgeReceiver = await deploymentManager.deploy( + 'bridgeReceiver', + 'bridges/scroll/ScrollBridgeReceiver.sol', + [l2Messenger.address] + ); + + // Deploy Local Timelock + const localTimelock = await deploymentManager.deploy('timelock', 'vendor/Timelock.sol', [ + bridgeReceiver.address, // admin + 10 * 60, // delay + 14 * secondsPerDay, // grace period + 10 * 60, // minimum delay + 30 * secondsPerDay // maximum delay + ]); + + // Initialize BridgeReceiver + await deploymentManager.idempotent( + async () => !(await bridgeReceiver.initialized()), + async () => { + trace(`Initializing BridgeReceiver`); + await bridgeReceiver.initialize( + GOERLI_TIMELOCK, // govTimelock + localTimelock.address // localTimelock + ); + trace(`BridgeReceiver initialized`); + } + ); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + const { comet } = deployed; + + // Deploy Bulker + const bulker = await deploymentManager.deploy('bulker', 'bulkers/BaseBulker.sol', [ + await comet.governor(), // admin + WETH.address // weth + ]); + + // Deploy fauceteer + const fauceteer = await deploymentManager.deploy('fauceteer', 'test/Fauceteer.sol', []); + + return { + ...deployed, + bridgeReceiver, + l2Messenger, + l2ERC20Gateway, + l2ETHGateway, + l2WETHGateway, + bulker, + fauceteer, + }; +} diff --git a/deployments/scroll-goerli/usdc/migrations/1694594095_configurate_and_ens.ts b/deployments/scroll-goerli/usdc/migrations/1694594095_configurate_and_ens.ts new file mode 100644 index 000000000..33e2fdfb3 --- /dev/null +++ b/deployments/scroll-goerli/usdc/migrations/1694594095_configurate_and_ens.ts @@ -0,0 +1,229 @@ +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { diffState, getCometConfig } from '../../../../plugins/deployment_manager/DiffState'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, getConfigurationStruct, proposal } from '../../../../src/deploy'; +import { expect } from 'chai'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x19c2d5D0f035563344dBB7bE5fD09c8dad62b001'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; +const scrollCOMPAddress = '0xE90a006650cda1F8390f95f45132B36bA9038bdF'; +const scrollL1StandardERC20GatewayAddress = "0xeF37207c1A1efF6D6a9d7BfF3cF4270e406d319b" + +export default migration('1694594095_configurate_and_ens', { + prepare: async (deploymentManager: DeploymentManager) => { + return {}; + }, + + enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager) => { + const trace = deploymentManager.tracer(); + const ethers = deploymentManager.hre.ethers; + const { utils } = ethers; + + const { + bridgeReceiver, + comet, + cometAdmin, + configurator, + rewards + } = await deploymentManager.getContracts(); + + const { + scrollMessenger, + scrollL1TokenBridge, + governor, + COMP, + } = await govDeploymentManager.getContracts(); + + // ENS Setup + // See also: https://docs.ens.domains/contract-api-reference/name-processing + const ENSResolver = await govDeploymentManager.existing( + 'ENSResolver', + ENSResolverAddress, + 'goerli' + ); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const scrollGoerliChainId = ( + await deploymentManager.hre.ethers.provider.getNetwork() + ).chainId.toString(); + const newMarketObject = { baseSymbol: 'USDC', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse(await ENSResolver.text(subdomainHash, ENSTextRecordKey)); + if (officialMarketsJSON[scrollGoerliChainId]) { + officialMarketsJSON[scrollGoerliChainId].push(newMarketObject); + } else { + officialMarketsJSON[scrollGoerliChainId] = [newMarketObject]; + } + + const configuration = await getConfigurationStruct(deploymentManager); + + const setConfigurationCalldata = await calldata( + configurator.populateTransaction.setConfiguration(comet.address, configuration) + ); + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + const setRewardConfigCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [comet.address, scrollCOMPAddress] + ); + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, cometAdmin.address, rewards.address], + [0, 0, 0], + [ + 'setConfiguration(address,(address,address,address,address,address,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint104,uint104,uint104,(address,address,uint8,uint64,uint64,uint64,uint128)[]))', + 'deployAndUpgradeTo(address,address)', + 'setRewardConfig(address,address)' + ], + [setConfigurationCalldata, deployAndUpgradeToCalldata, setRewardConfigCalldata] + ] + ); + + const COMPAmountToBridge = exp(10_000, 18); + + const goerliActions = [ + // 1. Set Comet configuration + deployAndUpgradeTo new Comet and set reward config on Scroll Alpha. + { + contract: scrollMessenger, + signature: 'sendMessage(address,uint256,bytes,uint256)', + args: [bridgeReceiver.address, 0, l2ProposalData, 600_000] + }, + + // 2. Approve Goerli's StandardERC20Gateway to take Timelock's COMP (for bridging) + { + contract: COMP, + signature: 'approve(address,uint256)', + args: [scrollL1StandardERC20GatewayAddress, COMPAmountToBridge] + }, + // 3. Bridge COMP from Goerli to Scroll Alpha Comet using L1GatewayRouter + { + contract: scrollL1TokenBridge, + signature: 'depositERC20(address,address,uint256,uint256)', + args: [COMP.address, rewards.address, COMPAmountToBridge, 300_000] + }, + // 4. Update the list of official markets + { + target: ENSResolverAddress, + signature: 'setText(bytes32,string,string)', + calldata: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'string', 'string'], + [subdomainHash, ENSTextRecordKey, JSON.stringify(officialMarketsJSON)] + ) + } + ]; + + const description = + '# Configurate Scroll Alpha cUSDCv3 market, set reward config, bridge over USDC and COMP, and update ENS text record.'; + const txn = await govDeploymentManager.retry(async () => + trace(await governor.propose(...(await proposal(goerliActions, description)))) + ); + + const event = txn.events.find(event => event.event === 'ProposalCreated'); + const [proposalId] = event.args; + + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify( + deploymentManager: DeploymentManager, + govDeploymentManager: DeploymentManager, + preMigrationBlockNumber: number + ) { + const ethers = deploymentManager.hre.ethers; + await deploymentManager.spider(); // We spider here to pull in Scroll COMP now that reward config has been set + + const { comet, rewards, COMP, USDC } = await deploymentManager.getContracts(); + + // 1. + const stateChanges = await diffState(comet, getCometConfig, preMigrationBlockNumber); + expect(stateChanges).to.deep.equal({ + baseTrackingSupplySpeed: exp(34.74 / 86400, 15, 18), + baseTrackingBorrowSpeed: exp(34.74 / 86400, 15, 18), + baseMinForRewards: exp(1000, 6), + WETH: { + borrowCollateralFactor: exp(0.775, 18), + liquidationFactor: exp(0.95, 18), + supplyCap: exp(1000, 18) + }, + }); + + const config = await rewards.rewardConfig(comet.address); + expect(config.token).to.be.equal(COMP.address); + expect(config.rescaleFactor).to.be.equal(exp(1, 12)); + expect(config.shouldUpscale).to.be.equal(true); + + // 2. & 3. + expect(await COMP.balanceOf(rewards.address)).to.be.equal(exp(1_000, 18)); + + // 4 + const ENSResolver = await govDeploymentManager.existing( + 'ENSResolver', + ENSResolverAddress, + 'goerli' + ); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + const officialMarkets = JSON.parse(officialMarketsJSON); + expect(officialMarkets).to.deep.equal({ + 5: [ + { + baseSymbol: 'USDC', + cometAddress: '0x3EE77595A8459e93C2888b13aDB354017B198188' + }, + { + baseSymbol: 'WETH', + cometAddress: '0x9A539EEc489AAA03D588212a164d0abdB5F08F5F' + } + ], + 80001: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF09F0369aB0a875254fB565E52226c88f10Bc839' + } + ], + 420: [ + { + baseSymbol: 'USDC', + cometAddress: '0xb8F2f9C84ceD7bBCcc1Db6FB7bb1F19A9a4adfF4' + } + ], + 421613: [ + { + baseSymbol: 'USDC', + cometAddress: '0x1d573274E19174260c5aCE3f2251598959d24456' + } + ], + 84531: [ + { + baseSymbol: 'USDC', + cometAddress: '0xe78Fc55c884704F9485EDa042fb91BfE16fD55c1' + }, + { + baseSymbol: 'WETH', + cometAddress: '0xED94f3052638620fE226a9661ead6a39C2a265bE' + } + ], + 59140: [ + { + baseSymbol: 'USDC', + cometAddress: "0xa84b24A43ba1890A165f94Ad13d0196E5fD1023a" + } + ], + 534353: [ + { + baseSymbol: 'USDC', + cometAddress: comet.address + } + ] + }); + } + +}); diff --git a/deployments/scroll-goerli/usdc/relations.ts b/deployments/scroll-goerli/usdc/relations.ts new file mode 100644 index 000000000..767191e98 --- /dev/null +++ b/deployments/scroll-goerli/usdc/relations.ts @@ -0,0 +1,42 @@ +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + governor: { + artifact: 'contracts/bridges/scroll/ScrollBridgeReceiver.sol:ScrollBridgeReceiver' + }, + l2Messenger: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + l2ERC20Gateway: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + l2ETHGateway: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + l2WETHGateway: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + '0x477df03cb8c83ec241df01302a9d13102676eee4': { + artifact: 'contracts/Configurator.sol:Configurator', + }, + '0x3effaacd82fa5a76d539b7d9cee0250f972f115f': { + artifact: 'contracts/CometFactory.sol:CometFactory', + } +}; diff --git a/deployments/scroll-goerli/usdc/roots.json b/deployments/scroll-goerli/usdc/roots.json new file mode 100644 index 000000000..cf9aee3fd --- /dev/null +++ b/deployments/scroll-goerli/usdc/roots.json @@ -0,0 +1,12 @@ +{ + "comet": "0x149B7023781d1D37689d447A565a1bf5854a8e3d", + "configurator": "0x6f5010E26581DC625802005EC82b30067460610a", + "rewards": "0xB2dbE5c648D8B4329499af8929Ee1d81b577ed5E", + "bridgeReceiver": "0x1ad7e375CF875bf9145fFa511d9D3c3A0d9DD0f8", + "l2Messenger": "0xb75d7e84517e1504C151B270255B087Fd746D34C", + "l2ERC20Gateway": "0xB878F37BB278bf0e4974856fFe86f5e6F66BD725", + "l2ETHGateway": "0x32139B5C8838E94fFcD83E60dff95Daa7F0bA14c", + "l2WETHGateway": "0xBb88bF582F2BBa46702621dae5CB9271057bC85b", + "bulker": "0x055FFa809c8817A1a0967B40027e48856C9D7945", + "fauceteer": "0x4a499858975e824f4ee2B95633B6B975Ab6d029E" +} \ No newline at end of file diff --git a/deployments/scroll/usdc/configuration.json b/deployments/scroll/usdc/configuration.json new file mode 100644 index 000000000..db3819878 --- /dev/null +++ b/deployments/scroll/usdc/configuration.json @@ -0,0 +1,46 @@ +{ + "name": "Compound USDC", + "symbol": "cUSDCv3", + "baseToken": "USDC", + "baseTokenAddress": "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4", + "baseTokenPriceFeed": "0x43d12Fb3AfCAd5347fA764EeAB105478337b7200", + "borrowMin": "0.001e6", + "pauseGuardian": "0x0747a435b8a60070A7a111D015046d765098e4cc", + "storeFrontPriceFactor": 0.6, + "targetReserves": "1000000e6", + "rates": { + "supplyKink": 0.85, + "supplySlopeLow": 0.048, + "supplySlopeHigh": 1.6, + "supplyBase": 0, + "borrowKink": 0.85, + "borrowSlopeLow": 0.053, + "borrowSlopeHigh": 1.8, + "borrowBase": 0.015 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "0e15", + "baseBorrowSpeed": "0e15", + "baseMinForRewards": "1000e6" + }, + "assets": { + "WETH": { + "address": "0x5300000000000000000000000000000000000004", + "priceFeed": "0x6bF14CB0A831078629D993FDeBcB182b21A8774C", + "decimals": "18", + "borrowCF": 0.8, + "liquidateCF": 0.85, + "liquidationFactor": 0.9, + "supplyCap": "300e18" + }, + "wstETH": { + "address": "0xf610A9dfB7C89644979b4A0f27063E9e7d7Cda32", + "decimals": "18", + "borrowCF": 0.75, + "liquidateCF": 0.8, + "liquidationFactor": 0.85, + "supplyCap": "50e18" + } + } +} diff --git a/deployments/scroll/usdc/deploy.ts b/deployments/scroll/usdc/deploy.ts new file mode 100644 index 000000000..786e773a8 --- /dev/null +++ b/deployments/scroll/usdc/deploy.ts @@ -0,0 +1,87 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet, exp, wait } from '../../../src/deploy'; + +const HOUR = 60 * 60; +const DAY = 24 * HOUR; + +const MAINNET_TIMELOCK = '0x6d903f6003cca6255d85cca4d3b5e5146dc33925'; // L1 contract + +export default async function deploy( + deploymentManager: DeploymentManager, + deploySpec: DeploySpec +): Promise { + const trace = deploymentManager.tracer(); + const ethers = deploymentManager.hre.ethers; + + // Pull in existing assets + const WETH = await deploymentManager.existing('WETH','0x5300000000000000000000000000000000000004','scroll'); + const wstETH = await deploymentManager.existing('wstETH', '0xf610A9dfB7C89644979b4A0f27063E9e7d7Cda32', 'scroll'); + + const l2Messenger = await deploymentManager.existing('l2Messenger','0x781e90f1c8Fc4611c9b7497C3B47F99Ef6969CbC','scroll'); + const l2ERC20Gateway = await deploymentManager.existing('l2ERC20Gateway','0xE2b4795039517653c5Ae8C2A9BFdd783b48f447A','scroll'); + const l2ETHGateway = await deploymentManager.existing('l2ETHGateway', '0x6EA73e05AdC79974B931123675ea8F78FfdacDF0', 'scroll'); + const l2WETHGateway = await deploymentManager.existing('l2WETHGateway','0x7003E7B7186f0E6601203b99F7B8DECBfA391cf9','scroll'); + const l2WstETHGateway = await deploymentManager.existing('l2WstETHGateway', '0x8aE8f22226B9d789A36AC81474e633f8bE2856c9', 'scroll'); + + // Deploy ScrollBridgeReceiver + const bridgeReceiver = await deploymentManager.deploy( + 'bridgeReceiver', + 'bridges/scroll/ScrollBridgeReceiver.sol', + [l2Messenger.address] + ); + + // Deploy Local Timelock + const localTimelock = await deploymentManager.deploy('timelock', 'vendor/Timelock.sol', [ + bridgeReceiver.address, // admin + 1 * DAY, // delay + 14 * DAY, // grace period + 12 * HOUR, // minimum delay + 30 * DAY // maxiumum delay + ]); + + // Deploy multiplicative price feed for wstETH / USD + const wstETHMultiplicativePriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'pricefeeds/MultiplicativePriceFeed.sol', + [ + '0xE61Da4C909F7d86797a0D06Db63c34f76c9bCBDC', // wstETH-stETH price feed + '0x6bF14CB0A831078629D993FDeBcB182b21A8774C', // ETH / USD price feed + 8, // decimals + 'wstETH / USD price feed' // description + ] + ); + + // Initialize BridgeReceiver + await deploymentManager.idempotent( + async () => !(await bridgeReceiver.initialized()), + async () => { + trace(`Initializing BridgeReceiver`); + await bridgeReceiver.initialize( + MAINNET_TIMELOCK, // govTimelock + localTimelock.address // localTimelock + ); + trace(`BridgeReceiver initialized`); + } + ); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + const { comet } = deployed; + + // Deploy Bulker + const bulker = await deploymentManager.deploy('bulker','bulkers/BaseBulker.sol', [ + await comet.governor(), // admin_ + WETH.address, // weth_ + ]); + + return { + ...deployed, + bridgeReceiver, + l2Messenger, + l2ERC20Gateway, + l2ETHGateway, + l2WETHGateway, + l2WstETHGateway, + bulker, + }; +} diff --git a/deployments/scroll/usdc/migrations/1706149385_configurate_and_ens.ts b/deployments/scroll/usdc/migrations/1706149385_configurate_and_ens.ts new file mode 100644 index 000000000..d5c4dd59d --- /dev/null +++ b/deployments/scroll/usdc/migrations/1706149385_configurate_and_ens.ts @@ -0,0 +1,228 @@ +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { diffState, getCometConfig } from '../../../../plugins/deployment_manager/DiffState'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, getConfigurationStruct, proposal } from '../../../../src/deploy'; +import { expect } from 'chai'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; +const scrollCOMPAddress = '0x643e160a3C3E2B7eae198f0beB1BfD2441450e86'; + +export default migration('1706149385_configurate_and_ens', { + prepare: async (deploymentManager: DeploymentManager) => { + return {}; + }, + + enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager) => { + const trace = deploymentManager.tracer(); + const ethers = deploymentManager.hre.ethers; + const { utils } = ethers; + + const { + bridgeReceiver, + comet, + cometAdmin, + configurator, + rewards + } = await deploymentManager.getContracts(); + + const { + scrollMessenger, + scrollL1USDCGateway, + governor, + USDC + } = await govDeploymentManager.getContracts(); + + // ENS Setup + // See also: https://docs.ens.domains/contract-api-reference/name-processing + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const scrollChainId = ( + await deploymentManager.hre.ethers.provider.getNetwork() + ).chainId.toString(); + const newMarketObject = { baseSymbol: 'USDC', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse(await ENSResolver.text(subdomainHash, ENSTextRecordKey)); + if (officialMarketsJSON[scrollChainId]) { + officialMarketsJSON[scrollChainId].push(newMarketObject); + } else { + officialMarketsJSON[scrollChainId] = [newMarketObject]; + } + + const configuration = await getConfigurationStruct(deploymentManager); + + const setConfigurationCalldata = await calldata( + configurator.populateTransaction.setConfiguration(comet.address, configuration) + ); + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + const setRewardConfigCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [comet.address, scrollCOMPAddress] + ); + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, cometAdmin.address, rewards.address], + [0, 0, 0], + [ + 'setConfiguration(address,(address,address,address,address,address,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint104,uint104,uint104,(address,address,uint8,uint64,uint64,uint64,uint128)[]))', + 'deployAndUpgradeTo(address,address)', + 'setRewardConfig(address,address)' + ], + [setConfigurationCalldata, deployAndUpgradeToCalldata, setRewardConfigCalldata] + ] + ); + + const USDCAmountToBridge = exp(10_000, 6); + + const actions = [ + // 1. Set Comet configuration + deployAndUpgradeTo new Comet and set reward config on Scroll + { + contract: scrollMessenger, + signature: 'sendMessage(address,uint256,bytes,uint256)', + args: [bridgeReceiver.address, 0, l2ProposalData, 600_000], + value: exp(0.1, 18) + }, + + // 2. Approve Scroll's L1 USDC Gateway to take Timelock's USDC (for bridging) + { + contract: USDC, + signature: 'approve(address,uint256)', + args: [scrollL1USDCGateway.address, USDCAmountToBridge] + }, + // 3. Bridge USDC from Ethereum to Scroll Comet using L1 USDC Gateway + { + contract: scrollL1USDCGateway, + signature: 'depositERC20(address,address,uint256,uint256)', + args: [USDC.address, comet.address, USDCAmountToBridge, 300_000], + value: exp(0.1, 18) + }, + // 4. Update the list of official markets + { + target: ENSResolverAddress, + signature: 'setText(bytes32,string,string)', + calldata: ethers.utils.defaultAbiCoder.encode( + ['bytes32', 'string', 'string'], + [subdomainHash, ENSTextRecordKey, JSON.stringify(officialMarketsJSON)] + ) + } + ]; + + const description = + "# Initialize cUSDCv3 on Scroll\n\nThis proposal takes the governance steps recommended and necessary to initialize a Compound III USDC market on Scroll; upon execution, cUSDCv3 will be ready for use. Simulations have confirmed the market’s readiness, as much as possible, using the [Comet scenario suite](https://github.com/compound-finance/comet/tree/main/scenario). Although real tests have also been run over the Goerli/Scroll Alpha bridge, this will be the first proposal to actually bridge from Ethereum mainnet to Scroll mainnet, and therefore includes risks not present in previous proposals.\n\nAlthough the proposal sets the entire configuration in the Configurator, with parameters based off of the [recommendations from Gauntlet](https://www.comp.xyz/t/deploy-compound-iii-on-scroll/4917/3).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/824) and [forum discussion](https://www.comp.xyz/t/deploy-compound-iii-on-scroll/4917).\n\n\n## Proposal Actions\n\nThe first proposal action sets the Comet configuration and deploys a new Comet implementation on Scroll. This sends the encoded `setConfiguration` and `deployAndUpgradeTo` calls across the bridge to the governance receiver on Scroll. It also calls `setRewardConfig` on the Scroll rewards contract to establish Scroll’s bridged version of COMP as the reward token for the deployment (note that rewards speeds have been set to 0, as Gauntlet has recommended to hold off on including rewards in the comet deployment for now).\n\nThe second action approves Scroll’s [L1USDCGateway](https://etherscan.io/address/0xf1AF3b23DE0A5Ca3CAb7261cb0061C0D779A5c7B) to take Timelock's USDC, in order to seed the market reserves through the bridge.\n\nThe third action deposits 10K USDC from mainnet to the Scroll L1USDCGateway contract to bridge to Comet.\n\nThe fourth action updates the ENS TXT record `v3-official-markets` on `v3-additional-grants.compound-community-licenses.eth`, updating the official markets JSON to include the new Scroll cUSDCv3 market."; + const txn = await govDeploymentManager.retry(async () => + trace(await governor.propose(...(await proposal(actions, description)))) + ); + + const event = txn.events.find(event => event.event === 'ProposalCreated'); + const [proposalId] = event.args; + + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify( + deploymentManager: DeploymentManager, + govDeploymentManager: DeploymentManager, + preMigrationBlockNumber: number + ) { + const ethers = deploymentManager.hre.ethers; + await deploymentManager.spider(); // We spider here to pull in Scroll COMP now that reward config has been set + + const { comet, rewards, COMP, USDC } = await deploymentManager.getContracts(); + + // 1. + const stateChanges = await diffState(comet, getCometConfig, preMigrationBlockNumber); + expect(stateChanges).to.deep.equal({ + baseTrackingSupplySpeed: exp(34.74 / 86400, 15, 18), + baseTrackingBorrowSpeed: exp(34.74 / 86400, 15, 18), + baseMinForRewards: exp(1000, 6), + WETH: { + borrowCollateralFactor: exp(0.775, 18), + liquidationFactor: exp(0.95, 18), + supplyCap: exp(1000, 18) + }, + }); + + const config = await rewards.rewardConfig(comet.address); + expect(config.token).to.be.equal(COMP.address); + expect(config.rescaleFactor).to.be.equal(exp(1, 12)); + expect(config.shouldUpscale).to.be.equal(true); + + // 2. & 3. + expect(await COMP.balanceOf(rewards.address)).to.be.equal(exp(1_000, 18)); + + // 4 + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + const officialMarkets = JSON.parse(officialMarketsJSON); + expect(officialMarkets).to.deep.equal({ + 5: [ + { + baseSymbol: 'USDC', + cometAddress: '0x3EE77595A8459e93C2888b13aDB354017B198188' + }, + { + baseSymbol: 'WETH', + cometAddress: '0x9A539EEc489AAA03D588212a164d0abdB5F08F5F' + } + ], + 80001: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF09F0369aB0a875254fB565E52226c88f10Bc839' + } + ], + 420: [ + { + baseSymbol: 'USDC', + cometAddress: '0xb8F2f9C84ceD7bBCcc1Db6FB7bb1F19A9a4adfF4' + } + ], + 421613: [ + { + baseSymbol: 'USDC', + cometAddress: '0x1d573274E19174260c5aCE3f2251598959d24456' + } + ], + 84531: [ + { + baseSymbol: 'USDC', + cometAddress: '0xe78Fc55c884704F9485EDa042fb91BfE16fD55c1' + }, + { + baseSymbol: 'WETH', + cometAddress: '0xED94f3052638620fE226a9661ead6a39C2a265bE' + } + ], + 59140: [ + { + baseSymbol: 'USDC', + cometAddress: "0xa84b24A43ba1890A165f94Ad13d0196E5fD1023a" + } + ], + 534353: [ + { + baseSymbol: 'USDC', + cometAddress: '0x149B7023781d1D37689d447A565a1bf5854a8e3d' + } + ], + 534352: [ + { + baseSymbol: 'USDC', + cometAddress: comet.address + } + ] + }); + } + +}); diff --git a/deployments/scroll/usdc/relations.ts b/deployments/scroll/usdc/relations.ts new file mode 100644 index 000000000..40baae4e7 --- /dev/null +++ b/deployments/scroll/usdc/relations.ts @@ -0,0 +1,52 @@ +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + governor: { + artifact: 'contracts/bridges/scroll/ScrollBridgeReceiver.sol:ScrollBridgeReceiver' + }, + l2Messenger: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + l2ERC20Gateway: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + l2ETHGateway: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + l2WETHGateway: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + l2WstETHGateway: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + // wstETH + '0xf610a9dfb7c89644979b4a0f27063e9e7d7cda32': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, +}; diff --git a/deployments/scroll/usdc/roots.json b/deployments/scroll/usdc/roots.json new file mode 100644 index 000000000..1960e352a --- /dev/null +++ b/deployments/scroll/usdc/roots.json @@ -0,0 +1,12 @@ +{ + "comet": "0xB2f97c1Bd3bf02f5e74d13f02E3e26F93D77CE44", + "configurator": "0xECAB0bEEa3e5DEa0c35d3E69468EAC20098032D7", + "rewards": "0x70167D30964cbFDc315ECAe02441Af747bE0c5Ee", + "bridgeReceiver": "0xC6bf5A64896D679Cf89843DbeC6c0f5d3C9b610D", + "l2Messenger": "0x781e90f1c8Fc4611c9b7497C3B47F99Ef6969CbC", + "l2ERC20Gateway": "0xE2b4795039517653c5Ae8C2A9BFdd783b48f447A", + "l2ETHGateway": "0x6EA73e05AdC79974B931123675ea8F78FfdacDF0", + "l2WETHGateway": "0x7003E7B7186f0E6601203b99F7B8DECBfA391cf9", + "l2WstETHGateway": "0x8aE8f22226B9d789A36AC81474e633f8bE2856c9", + "bulker": "0x53C6D04e3EC7031105bAeA05B36cBc3C987C56fA" +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 41900ebdd..3b1799434 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -36,6 +36,8 @@ import baseUsdcRelationConfigMap from './deployments/base/usdc/relations'; import baseGoerliRelationConfigMap from './deployments/base-goerli/usdc/relations'; import baseGoerliWethRelationConfigMap from './deployments/base-goerli/weth/relations'; import lineaGoerliRelationConfigMap from './deployments/linea-goerli/usdc/relations'; +import scrollGoerliRelationConfigMap from './deployments/scroll-goerli/usdc/relations'; +import scrollRelationConfigMap from './deployments/scroll/usdc/relations'; task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => { @@ -145,6 +147,16 @@ const networkConfigs: NetworkConfig[] = [ chainId: 59140, url: `https://linea-goerli.infura.io/v3/${INFURA_KEY}`, }, + { + network: 'scroll-goerli', + chainId: 534353, + url: 'https://alpha-rpc.scroll.io/l2', + }, + { + network: 'scroll', + chainId: 534352, + url: 'https://rpc.scroll.io', + } ]; function getDefaultProviderURL(network: string) { @@ -198,9 +210,9 @@ const config: HardhatUserConfig = { hardhat: { chainId: 1337, loggingEnabled: !!process.env['LOGGING'], - gas: 12000000, + gas: 120000000, gasPrice: 'auto', - blockGasLimit: 12000000, + blockGasLimit: 120000000, accounts: ETH_PK ? [...deriveAccounts(ETH_PK)].map(privateKey => ({ privateKey, balance: (10n ** 36n).toString() })) : { mnemonic: MNEMONIC, accountsBalance: (10n ** 36n).toString() }, @@ -235,6 +247,10 @@ const config: HardhatUserConfig = { 'base-goerli': BASESCAN_KEY, // Linea 'linea-goerli': LINEASCAN_KEY, + // Scroll Testnet + 'scroll-goerli': ETHERSCAN_KEY, + // Scroll + 'scroll': ETHERSCAN_KEY, }, customChains: [ { @@ -280,6 +296,22 @@ const config: HardhatUserConfig = { apiURL: 'https://api-goerli.lineascan.build/api', browserURL: 'https://goerli.lineascan.build/' } + }, + { + network: 'scroll-goerli', + chainId: 534353, + urls: { + apiURL: 'https://alpha-blockscout.scroll.io/api', + browserURL: 'https://alpha-blockscout.scroll.io/' + } + }, + { + network: 'scroll', + chainId: 534352, + urls: { + apiURL: 'https://api.scrollscan.com/api', + browserURL: 'https://scrollscan.com/' + } } ] }, @@ -329,6 +361,12 @@ const config: HardhatUserConfig = { }, 'linea-goerli': { usdc: lineaGoerliRelationConfigMap + }, + 'scroll-goerli': { + usdc: scrollGoerliRelationConfigMap + }, + 'scroll': { + usdc: scrollRelationConfigMap } }, }, @@ -447,6 +485,18 @@ const config: HardhatUserConfig = { network: 'linea-goerli', deployment: 'usdc', auxiliaryBase: 'goerli' + }, + { + name: 'scroll-goerli', + network: 'scroll-goerli', + deployment: 'usdc', + auxiliaryBase: 'goerli' + }, + { + name: 'scroll-usdc', + network: 'scroll', + deployment: 'usdc', + auxiliaryBase: 'mainnet' } ], }, diff --git a/plugins/import/etherscan.ts b/plugins/import/etherscan.ts index 573d57b9e..81b500b0a 100644 --- a/plugins/import/etherscan.ts +++ b/plugins/import/etherscan.ts @@ -21,7 +21,9 @@ export function getEtherscanApiUrl(network: string): string { 'arbitrum-goerli': 'api-goerli.arbiscan.io', base: 'api.basescan.org', 'base-goerli': 'api-goerli.basescan.org', - 'linea-goerli': 'api-goerli.lineascan.build' + 'linea-goerli': 'api-goerli.lineascan.build', + 'scroll-goerli': 'alpha-blockscout.scroll.io', + scroll: 'api.scrollscan.com' }[network]; if (!host) { @@ -46,7 +48,9 @@ export function getEtherscanUrl(network: string): string { 'arbitrum-goerli': 'goerli.arbiscan.io', base: 'basescan.org', 'base-goerli': 'goerli.basescan.org', - 'linea-goerli': 'goerli.lineascan.build' + 'linea-goerli': 'goerli.lineascan.build', + 'scroll-goerli': 'alpha-blockscout.scroll.io', + scroll: 'scrollscan.com' }[network]; if (!host) { @@ -71,7 +75,9 @@ export function getEtherscanApiKey(network: string): string { 'arbitrum-goerli': process.env.ARBISCAN_KEY, base: process.env.BASESCAN_KEY, 'base-goerli': process.env.BASESCAN_KEY, - 'linea-goerli': process.env.LINEASCAN_KEY + 'linea-goerli': process.env.LINEASCAN_KEY, + 'scroll-goerli': process.env.ETHERSCAN_KEY, + scroll: process.env.ETHERSCAN_KEY }[network]; if (!apiKey) { diff --git a/scenario/CrossChainGovernanceScenario.ts b/scenario/CrossChainGovernanceScenario.ts index b20bf4b29..cc25ab765 100644 --- a/scenario/CrossChainGovernanceScenario.ts +++ b/scenario/CrossChainGovernanceScenario.ts @@ -1,7 +1,7 @@ import { scenario } from './context/CometContext'; import { expect } from 'chai'; import { utils } from 'ethers'; -import { BaseBridgeReceiver, LineaBridgeReceiver } from '../build/types'; +import { BaseBridgeReceiver, LineaBridgeReceiver, ScrollBridgeReceiver } from '../build/types'; import { calldata } from '../src/deploy'; import { isBridgedDeployment, matchesDeployment, createCrossChainProposal } from './utils'; import { ArbitrumBridgeReceiver } from '../build/types'; @@ -374,6 +374,127 @@ scenario( await createCrossChainProposal(context, l2ProposalData, newBridgeReceiver); + expect(await newLocalTimelock.delay()).to.eq(newTimelockDelay); + expect(await comet.isAbsorbPaused()).to.eq(true); + expect(await comet.isBuyPaused()).to.eq(true); + expect(await comet.isSupplyPaused()).to.eq(true); + expect(await comet.isTransferPaused()).to.eq(true); + expect(await comet.isWithdrawPaused()).to.eq(true); + } +); + +scenario( + 'upgrade Scroll governance contracts and ensure they work properly', + { + filter: async ctx => matchesDeployment(ctx, [{ network: 'scroll-goerli' }, {network: 'scroll'}]) + }, + async ( + { + comet, + configurator, + proxyAdmin, + timelock: oldLocalTimelock, + bridgeReceiver: oldBridgeReceiver + }, + context, + world + ) => { + const dm = world.deploymentManager; + const governanceDeploymentManager = world.auxiliaryDeploymentManager; + if (!governanceDeploymentManager) { + throw new Error('cannot execute governance without governance deployment manager'); + } + + const l2Messenger = await dm.getContractOrThrow('l2Messenger'); + + // Deploy new ScrollBridgeReceiver + const newBridgeReceiver = await dm.deploy( + 'newBridgeReceiver', + 'bridges/scroll/ScrollBridgeReceiver.sol', + [l2Messenger.address] + ); + + // Deploy new local Timelock + const secondsPerDay = 24 * 60 * 60; + const newLocalTimelock = await dm.deploy('newTimelock', 'vendor/Timelock.sol', [ + newBridgeReceiver.address, // admin + 2 * secondsPerDay, // delay + 14 * secondsPerDay, // grace period + 2 * secondsPerDay, // minimum delay + 30 * secondsPerDay // maxiumum delay + ]); + + // Initialize new ScrollBridgeReceiver + const mainnetTimelock = (await governanceDeploymentManager.getContractOrThrow('timelock')) + .address; + await newBridgeReceiver.initialize( + mainnetTimelock, // govTimelock + newLocalTimelock.address // localTimelock + ); + + // Process for upgrading L2 governance contracts (order matters): + // 1. Update the admin of Comet in Configurator to be the new Timelock + // 2. Update the admin of CometProxyAdmin to be the new Timelock + const transferOwnershipCalldata = utils.defaultAbiCoder.encode( + ['address'], + [newLocalTimelock.address] + ); + const setGovernorCalldata = await calldata( + configurator.populateTransaction.setGovernor(comet.address, newLocalTimelock.address) + ); + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + const upgradeL2GovContractsProposal = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, proxyAdmin.address, proxyAdmin.address], + [0, 0, 0], + [ + 'setGovernor(address,address)', + 'deployAndUpgradeTo(address,address)', + 'transferOwnership(address)' + ], + [setGovernorCalldata, deployAndUpgradeToCalldata, transferOwnershipCalldata] + ] + ); + + expect(await proxyAdmin.owner()).to.eq(oldLocalTimelock.address); + expect(await comet.governor()).to.eq(oldLocalTimelock.address); + + await createCrossChainProposal(context, upgradeL2GovContractsProposal, oldBridgeReceiver); + + expect(await proxyAdmin.owner()).to.eq(newLocalTimelock.address); + expect(await comet.governor()).to.eq(newLocalTimelock.address); + + // Update aliases now that the new Timelock and BridgeReceiver are official + await dm.putAlias('timelock', newLocalTimelock); + await dm.putAlias('bridgeReceiver', newBridgeReceiver); + + // Now, test that the new L2 governance contracts are working properly via another cross-chain proposal + const currentTimelockDelay = await newLocalTimelock.delay(); + const newTimelockDelay = currentTimelockDelay.mul(2); + + const setDelayCalldata = utils.defaultAbiCoder.encode(['uint'], [newTimelockDelay]); + const pauseCalldata = await calldata( + comet.populateTransaction.pause(true, true, true, true, true) + ); + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [newLocalTimelock.address, comet.address], + [0, 0], + ['setDelay(uint256)', 'pause(bool,bool,bool,bool,bool)'], + [setDelayCalldata, pauseCalldata] + ] + ); + + expect(await newLocalTimelock.delay()).to.eq(currentTimelockDelay); + expect(currentTimelockDelay).to.not.eq(newTimelockDelay); + + await createCrossChainProposal(context, l2ProposalData, newBridgeReceiver); + expect(await newLocalTimelock.delay()).to.eq(newTimelockDelay); expect(await comet.isAbsorbPaused()).to.eq(true); expect(await comet.isBuyPaused()).to.eq(true); diff --git a/scenario/utils/index.ts b/scenario/utils/index.ts index 2e03715ed..061c5f7e7 100644 --- a/scenario/utils/index.ts +++ b/scenario/utils/index.ts @@ -429,7 +429,7 @@ export async function createCrossChainProposal(context: CometContext, l2Proposal const proposer = await context.getProposer(); const bridgeNetwork = bridgeDeploymentManager.network; const targets: string[] = []; - const values: number[] = []; + const values: BigNumberish[] = []; const signatures: string[] = []; const calldata: string[] = []; @@ -504,6 +504,21 @@ export async function createCrossChainProposal(context: CometContext, l2Proposal calldata.push(sendMessageCalldata); break; } + case 'scroll': + case 'scroll-goerli': { + const sendMessageCalldata = utils.defaultAbiCoder.encode( + ['address', 'uint256', 'bytes', 'uint256'], + [bridgeReceiver.address, 0, l2ProposalData, 1_000_000] // XXX find a reliable way to estimate the gasLimit + ); + const scrollMessenger = await govDeploymentManager.getContractOrThrow( + 'scrollMessenger' + ); + targets.push(scrollMessenger.address); + values.push(exp(1, 18)); // XXX fees are paid via msg.value + signatures.push('sendMessage(address,uint256,bytes,uint256)'); + calldata.push(sendMessageCalldata); + break; + } default: throw new Error( `No cross-chain proposal constructor implementation for ${govDeploymentManager.network} -> ${bridgeNetwork}` diff --git a/scenario/utils/isBridgeProposal.ts b/scenario/utils/isBridgeProposal.ts index b14bbd32d..03ec57b26 100644 --- a/scenario/utils/isBridgeProposal.ts +++ b/scenario/utils/isBridgeProposal.ts @@ -52,6 +52,15 @@ export async function isBridgeProposal( const { targets } = await governor.getActions(openProposal.id); return targets.includes(lineaMessageService.address); } + case 'scroll': + case 'scroll-goerli': { + const governor = await governanceDeploymentManager.getContractOrThrow('governor'); + const scrollMessenger = await governanceDeploymentManager.getContractOrThrow( + 'scrollMessenger' + ); + const { targets } = await governor.getActions(openProposal.id); + return targets.includes(scrollMessenger.address); + } default: { const tag = `[${bridgeNetwork} -> ${governanceDeploymentManager.network}]`; throw new Error(`${tag} Unable to determine whether to relay Proposal ${openProposal.id}`); diff --git a/scenario/utils/relayMessage.ts b/scenario/utils/relayMessage.ts index 0848cb1ba..25ad63849 100644 --- a/scenario/utils/relayMessage.ts +++ b/scenario/utils/relayMessage.ts @@ -3,6 +3,7 @@ import relayPolygonMessage from './relayPolygonMessage'; import { relayArbitrumMessage, relayCCTPMint } from './relayArbitrumMessage'; import relayBaseMessage from './relayBaseMessage'; import relayLineaMessage from './relayLineaMessage'; +import relayScrollMessage from './relayScrollMessage'; export default async function relayMessage( governanceDeploymentManager: DeploymentManager, @@ -47,6 +48,14 @@ export default async function relayMessage( startingBlockNumber ); break; + case 'scroll': + case 'scroll-goerli': + await relayScrollMessage( + governanceDeploymentManager, + bridgeDeploymentManager, + startingBlockNumber + ); + break; default: throw new Error( `No message relay implementation from ${bridgeNetwork} -> ${governanceDeploymentManager.network}` diff --git a/scenario/utils/relayScrollMessage.ts b/scenario/utils/relayScrollMessage.ts new file mode 100644 index 000000000..e6feb53d9 --- /dev/null +++ b/scenario/utils/relayScrollMessage.ts @@ -0,0 +1,155 @@ +import { DeploymentManager } from '../../plugins/deployment_manager'; +import { setNextBaseFeeToZero, setNextBlockTimestamp } from './hreUtils'; +import { Log } from '@ethersproject/abstract-provider'; +import { impersonateAddress } from '../../plugins/scenario/utils'; +import { OpenBridgedProposal } from '../context/Gov'; +import { BigNumber, ethers } from 'ethers'; + +/* +The Scroll relayer applies an offset to the message sender. + +applyL1ToL2Alias mimics the AddressAliasHelper.applyL1ToL2Alias fn that converts +an L1 address to its offset, L2 equivalent. +*/ +function applyL1ToL2Alias(address: string) { + const offset = BigInt('0x1111000000000000000000000000000000001111'); + return `0x${(BigInt(address) + offset).toString(16)}`; +} + +export default async function relayScrollMessage( + governanceDeploymentManager: DeploymentManager, + bridgeDeploymentManager: DeploymentManager, + startingBlockNumber: number +) { + const scrollMessenger = await governanceDeploymentManager.getContractOrThrow( + 'scrollMessenger' + ); + const bridgeReceiver = await bridgeDeploymentManager.getContractOrThrow('bridgeReceiver'); + const l2Messenger = await bridgeDeploymentManager.getContractOrThrow('l2Messenger'); + const l2ERC20Gateway = await bridgeDeploymentManager.getContractOrThrow('l2ERC20Gateway'); + const l2ETHGateway = await bridgeDeploymentManager.getContractOrThrow('l2ETHGateway'); + const l2WETHGateway = await bridgeDeploymentManager.getContractOrThrow('l2WETHGateway'); + const l2WstETHGateway = await bridgeDeploymentManager.getContractOrThrow('l2WstETHGateway'); + + const openBridgedProposals: OpenBridgedProposal[] = []; + + // Grab all events on the L1CrossDomainMessenger contract since the `startingBlockNumber` + const filter = scrollMessenger.filters.SentMessage(); + const messageSentEvents: Log[] = await governanceDeploymentManager.hre.ethers.provider.getLogs({ + fromBlock: startingBlockNumber, + toBlock: 'latest', + address: scrollMessenger.address, + topics: filter.topics! + }); + for (let messageSentEvent of messageSentEvents) { + const { + args: { sender, target, value, messageNonce, gasLimit, message } + } = scrollMessenger.interface.parseLog(messageSentEvent); + + await setNextBaseFeeToZero(bridgeDeploymentManager); + + let aliasAccount; + if (bridgeDeploymentManager.network == 'scroll-goerli'){ + aliasAccount = await impersonateAddress( + bridgeDeploymentManager, + '0xD69c917c7F1C0a724A51c189B4A8F4F8C8E8cA0a' + ); + } else { + aliasAccount = await impersonateAddress( + bridgeDeploymentManager, + applyL1ToL2Alias(scrollMessenger.address) + ); + } + + const relayMessageTxn = await ( + await l2Messenger.connect(aliasAccount).relayMessage( + sender, + target, + value, + messageNonce, + message, + { gasPrice: 0, gasLimit } + ) + ).wait(); + + const messageWithoutPrefix = message.slice(2); // strip out the 0x prefix + const messageWithoutSigHash = '0x' + messageWithoutPrefix.slice(8); + + // Try to decode the SentMessage data to determine what type of cross-chain activity this is. So far, + // there are two types: + // 1. Bridging ERC20 token or ETH + // 2. Cross-chain message passing + if (target === l2ERC20Gateway.address) { + // 1a. Bridging ERC20 token + const { l1Token, _l2Token, _from, to, amount, _data } = ethers.utils.defaultAbiCoder.decode( + ['address _l1Token', 'address _l2Token','address _from', 'address _to','uint256 _amount', 'bytes _data'], + messageWithoutSigHash + ); + + console.log( + `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Bridged over ${amount} of ${l1Token} to user ${to}` + ); + } else if (target === l2ETHGateway.address){ + // 1a. Bridging ETH + const { _from, to, amount, _data } = ethers.utils.defaultAbiCoder.decode( + ['address _from', 'address _to', 'uint256 _amount', 'bytes _data'], + messageWithoutSigHash + ); + + const oldBalance = await bridgeDeploymentManager.hre.ethers.provider.getBalance(to); + const newBalance = oldBalance.add(BigNumber.from(amount)); + // This is our best attempt to mimic the deposit transaction type (not supported in Hardhat) that Optimism uses to deposit ETH to an L2 address + await bridgeDeploymentManager.hre.ethers.provider.send('hardhat_setBalance', [ + to, + ethers.utils.hexStripZeros(newBalance.toHexString()), + ]); + + console.log( + `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Bridged over ${amount} of ETH to user ${to}` + ); + }else if (target === l2WETHGateway.address){ + // 1c. Bridging WETH + const { _l1Token, _l2Token, _from, to, amount, _data } = ethers.utils.defaultAbiCoder.decode( + ['address _l1Token', 'address _l2Token','address _from', 'address _to','uint256 _amount', 'bytes _data'], + messageWithoutSigHash + ); + + console.log( + `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Bridged over ${amount} of WETH to user ${to}` + ); + } else if (target === l2WstETHGateway.address){ + // 1d. Bridging WstETH + const { _l1Token, _l2Token, _from, to, amount, _data } = ethers.utils.defaultAbiCoder.decode( + ['address _l1Token', 'address _l2Token','address _from', 'address _to','uint256 _amount', 'bytes _data'], + messageWithoutSigHash + ); + + console.log( + `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Bridged over ${amount} of WstETH to user ${to}` + ); + } else if (target === bridgeReceiver.address) { + // Cross-chain message passing + const proposalCreatedEvent = relayMessageTxn.events.find(event => event.address === bridgeReceiver.address); + const { args: { id, eta } } = bridgeReceiver.interface.parseLog(proposalCreatedEvent); + + // Add the proposal to the list of open bridged proposals to be executed after all the messages have been relayed + openBridgedProposals.push({ id, eta }); + } else { + throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); + } + } + + // Execute open bridged proposals now that all messages have been bridged + for (let proposal of openBridgedProposals) { + const { eta, id } = proposal; + // Fast forward l2 time + await setNextBlockTimestamp(bridgeDeploymentManager, eta.toNumber() + 1); + + // Execute queued proposal + await setNextBaseFeeToZero(bridgeDeploymentManager); + await bridgeReceiver.executeProposal(id, { gasPrice: 0 }); + console.log( + `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Executed bridged proposal ${id}` + ); + } +} diff --git a/src/deploy/index.ts b/src/deploy/index.ts index bbcbf8f54..be2d38860 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -103,6 +103,9 @@ export const WHALES = { '0x6D3c5a4a7aC4B1428368310E4EC3bB1350d01455', // USDbC whale '0x07CFA5Df24fB17486AF0CBf6C910F24253a674D3' // cbETH whale TODO: need to update this whale, not enough ], + scroll: [ + '0xaaaaAAAACB71BF2C8CaE522EA5fa455571A74106' // USDC whale + ], 'arbitrum-goerli': [ '0x4984cbfa5b199e5920995883d345bbe14b005db7', // USDC whale '0xbbfe34e868343e6f4f5e8b5308de980d7bd88c46', // LINK whale