diff --git a/.github/workflows/run-scenarios.yaml b/.github/workflows/run-scenarios.yaml index 7f2020873..5da35d811 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, mainnet-usdt, mainnet-wsteth, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, polygon-usdt, arbitrum-usdc.e, arbitrum-usdc, arbitrum-weth, arbitrum-usdt, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-aero, base-goerli, base-goerli-weth, linea-goerli, optimism-usdc, optimism-usdt, optimism-weth, scroll-goerli, scroll-usdc] + bases: [ development, mainnet, mainnet-weth, mainnet-usdt, mainnet-wsteth, mainnet-usds, goerli, goerli-weth, sepolia-usdc, sepolia-weth, fuji, mumbai, polygon, polygon-usdt, arbitrum-usdc.e, arbitrum-usdc, arbitrum-weth, arbitrum-usdt, arbitrum-goerli-usdc, arbitrum-goerli-usdc.e, base-usdbc, base-weth, base-usdc, base-aero, base-goerli, base-goerli-weth, linea-goerli, optimism-usdc, optimism-usdt, optimism-weth, scroll-goerli, scroll-usdc] name: Run scenarios env: ETHERSCAN_KEY: ${{ secrets.ETHERSCAN_KEY }} diff --git a/contracts/IERC20.sol b/contracts/IERC20.sol new file mode 100644 index 000000000..cb6edc109 --- /dev/null +++ b/contracts/IERC20.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol) + +pragma solidity ^0.8.15; + +/** + * @dev Interface of the ERC-20 standard as defined in the ERC. + */ +interface IERC20 { + /** + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); + + /** + * @dev Returns the value of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the value of tokens owned by `account`. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Moves a `value` amount of tokens from the caller's account to `to`. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transfer(address to, uint256 value) external returns (bool); + + /** + * @dev Returns the remaining number of tokens that `spender` will be + * allowed to spend on behalf of `owner` through {transferFrom}. This is + * zero by default. + * + * This value changes when {approve} or {transferFrom} are called. + */ + function allowance(address owner, address spender) external view returns (uint256); + + /** + * @dev Sets a `value` amount of tokens as the allowance of `spender` over the + * caller's tokens. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * IMPORTANT: Beware that changing an allowance with this method brings the risk + * that someone may use both the old and the new allowance by unfortunate + * transaction ordering. One possible solution to mitigate this race + * condition is to first reduce the spender's allowance to 0 and set the + * desired value afterwards: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * Emits an {Approval} event. + */ + function approve(address spender, uint256 value) external returns (bool); + + /** + * @dev Moves a `value` amount of tokens from `from` to `to` using the + * allowance mechanism. `value` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. + */ + function transferFrom(address from, address to, uint256 value) external returns (bool); +} \ No newline at end of file diff --git a/contracts/IERC20Metadata.sol b/contracts/IERC20Metadata.sol new file mode 100644 index 000000000..441d193e4 --- /dev/null +++ b/contracts/IERC20Metadata.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol) + +pragma solidity ^0.8.15; + +import {IERC20} from "./IERC20.sol"; + +/** + * @dev Interface for the optional metadata functions from the ERC-20 standard. + */ +interface IERC20Metadata is IERC20 { + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the decimals places of the token. + */ + function decimals() external view returns (uint8); +} \ No newline at end of file diff --git a/contracts/IERC4626.sol b/contracts/IERC4626.sol new file mode 100644 index 000000000..a92c33583 --- /dev/null +++ b/contracts/IERC4626.sol @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC4626.sol) + +pragma solidity ^0.8.15; + +import {IERC20} from "./IERC20.sol"; +import {IERC20Metadata} from "./IERC20Metadata.sol"; + +/** + * @dev Interface of the ERC-4626 "Tokenized Vault Standard", as defined in + * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. + */ +interface IERC4626 is IERC20, IERC20Metadata { + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + + event Withdraw( + address indexed sender, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /** + * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing. + * + * - MUST be an ERC-20 token contract. + * - MUST NOT revert. + */ + function asset() external view returns (address assetTokenAddress); + + /** + * @dev Returns the total amount of the underlying asset that is “managed” by Vault. + * + * - SHOULD include any compounding that occurs from yield. + * - MUST be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT revert. + */ + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal + * scenario where all the conditions are met. + * + * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + * - MUST NOT revert. + * + * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the + * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and + * from. + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal + * scenario where all the conditions are met. + * + * - MUST NOT be inclusive of any fees that are charged against assets in the Vault. + * - MUST NOT show any variations depending on the caller. + * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange. + * - MUST NOT revert. + * + * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the + * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and + * from. + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver, + * through a deposit call. + * + * - MUST return a limited value if receiver is subject to some deposit limit. + * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited. + * - MUST NOT revert. + */ + function maxDeposit(address receiver) external view returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given + * current on-chain conditions. + * + * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit + * call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called + * in the same transaction. + * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the + * deposit would be accepted, regardless if the user has enough tokens approved, etc. + * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by depositing. + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * deposit execution, and are accounted for during deposit. + * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call. + * - MUST return a limited value if receiver is subject to some mint limit. + * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted. + * - MUST NOT revert. + */ + function maxMint(address receiver) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given + * current on-chain conditions. + * + * - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call + * in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the + * same transaction. + * - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint + * would be accepted, regardless if the user has enough tokens approved, etc. + * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by minting. + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens. + * + * - MUST emit the Deposit event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint + * execution, and are accounted for during mint. + * - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not + * approving enough underlying tokens to the Vault contract, etc). + * + * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token. + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the + * Vault, through a withdraw call. + * + * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. + * - MUST NOT revert. + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block, + * given current on-chain conditions. + * + * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw + * call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if + * called + * in the same transaction. + * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though + * the withdrawal would be accepted, regardless if the user has enough shares, etc. + * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by depositing. + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver. + * + * - MUST emit the Withdraw event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * withdraw execution, and are accounted for during withdraw. + * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner + * not having enough shares, etc). + * + * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + * Those methods should be performed separately. + */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /** + * @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault, + * through a redeem call. + * + * - MUST return a limited value if owner is subject to some withdrawal limit or timelock. + * - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock. + * - MUST NOT revert. + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); + + /** + * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block, + * given current on-chain conditions. + * + * - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call + * in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the + * same transaction. + * - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the + * redemption would be accepted, regardless if the user has enough shares, etc. + * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees. + * - MUST NOT revert. + * + * NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in + * share price or some other type of condition, meaning the depositor will lose assets by redeeming. + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /** + * @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver. + * + * - MUST emit the Withdraw event. + * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the + * redeem execution, and are accounted for during redeem. + * - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner + * not having enough shares, etc). + * + * NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed. + * Those methods should be performed separately. + */ + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); +} \ No newline at end of file diff --git a/contracts/pricefeeds/PriceFeedWith4626Support.sol b/contracts/pricefeeds/PriceFeedWith4626Support.sol new file mode 100644 index 000000000..3103d9a7d --- /dev/null +++ b/contracts/pricefeeds/PriceFeedWith4626Support.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "../vendor/@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"; +import "../IERC4626.sol"; +import "../IPriceFeed.sol"; + +/** + * @title Price feed for ERC4626 assets + * @notice A custom price feed that calculates the price for an ERC4626 asset + * @author Compound + */ +contract PriceFeedWith4626Support is IPriceFeed { + /** Custom errors **/ + error BadDecimals(); + error InvalidInt256(); + + /// @notice Version of the price feed + uint public constant VERSION = 1; + + /// @notice Description of the price feed + string public override description; + + /// @notice Number of decimals for returned prices + uint8 public immutable override decimals; + + /// @notice Number of decimals for the 4626 rate provider + uint8 internal immutable rateProviderDecimals; + + /// @notice Number of decimals for the underlying asset + uint8 internal immutable underlyingDecimals; + + /// @notice 4626 rate provider + address public immutable rateProvider; + + /// @notice Chainlink oracle for the underlying asset + address public immutable underlyingPriceFeed; + + /// @notice Combined scale of the two underlying price feeds + int public immutable combinedScale; + + /// @notice Scale of this price feed + int public immutable priceFeedScale; + + /** + * @notice Construct a new 4626 price feed + * @param rateProvider_ The address of the 4626 rate provider + * @param underlyingPriceFeed_ The address of the underlying asset price feed to fetch prices from + * @param decimals_ The number of decimals for the returned prices + * @param description_ The description of the price feed + **/ + constructor(address rateProvider_, address underlyingPriceFeed_, uint8 decimals_, string memory description_) { + rateProvider = rateProvider_; + underlyingPriceFeed = underlyingPriceFeed_; + rateProviderDecimals = IERC4626(rateProvider_).decimals(); + underlyingDecimals = AggregatorV3Interface(underlyingPriceFeed_).decimals(); + combinedScale = signed256(10 ** (rateProviderDecimals + underlyingDecimals)); + description = description_; + + if (decimals_ > 18) revert BadDecimals(); + decimals = decimals_; + priceFeedScale = int256(10 ** decimals); + } + + /** + * @notice Get the latest price for the underlying asset + * @return roundId Round id from the underlying asset price feed + * @return answer Latest price for the underlying asset + * @return startedAt Timestamp when the round was started; passed on from the underlying asset price feed + * @return updatedAt Timestamp when the round was last updated; passed on from the underlying asset price feed + * @return answeredInRound Round id in which the answer was computed; passed on from the underlying asset price feed + **/ + function latestRoundData() override external view returns (uint80, int256, uint256, uint256, uint80) { + uint256 rate = IERC4626(rateProvider).convertToAssets(10**rateProviderDecimals); + (uint80 roundId_, int256 underlyingPrice, uint256 startedAt_, uint256 updatedAt_, uint80 answeredInRound_) = AggregatorV3Interface(underlyingPriceFeed).latestRoundData(); + + if (rate <= 0 || underlyingPrice <= 0) return (roundId_, 0, startedAt_, updatedAt_, answeredInRound_); + + int256 price = signed256(rate) * underlyingPrice * priceFeedScale / combinedScale; + return (roundId_, price, startedAt_, updatedAt_, answeredInRound_); + } + + function signed256(uint256 n) internal pure returns (int256) { + if (n > uint256(type(int256).max)) revert InvalidInt256(); + return int256(n); + } + + /** + * @notice Price for the latest round + * @return The version of the price feed contract + **/ + function version() external pure returns (uint256) { + return VERSION; + } +} \ No newline at end of file diff --git a/deployments/arbitrum/usdc/migrations/1727427904_add_wusdm_as_collateral.ts b/deployments/arbitrum/usdc/migrations/1727427904_add_wusdm_as_collateral.ts new file mode 100644 index 000000000..a3d4a0f8e --- /dev/null +++ b/deployments/arbitrum/usdc/migrations/1727427904_add_wusdm_as_collateral.ts @@ -0,0 +1,198 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { exp, proposal } from '../../../../src/deploy'; +import { applyL1ToL2Alias, estimateL2Transaction } from '../../../../scenario/utils/arbitrumUtils'; +import { ethers } from 'ethers'; + +const WUSDM_ADDRESS = '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812'; +const WUSDM_TO_USDM_PRICE_FEED_ADDRESS = '0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812'; +const USDM_TO_USD_PRICE_FEED_ADDRESS = '0x24EA2671671c33D66e9854eC06e42E5D3ac1f764'; + +let newPriceFeedAddress: string; + +export default migration('1727427904_add_wusdm_as_collateral', { + async prepare(deploymentManager: DeploymentManager) { + const _wUSDMPriceFeed = await deploymentManager.deploy( + 'wUSDM:priceFeed', + 'pricefeeds/PriceFeedWith4626Support.sol', + [ + WUSDM_TO_USDM_PRICE_FEED_ADDRESS, // wUSDM / USDM price feed + USDM_TO_USD_PRICE_FEED_ADDRESS, // USDM / USD price feed + 8, // decimals + 'wUSDM/USD price feed' // description + ], + true + ); + return { wUSDMPriceFeedAddress: _wUSDMPriceFeed.address }; + }, + + enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, { wUSDMPriceFeedAddress }) => { + const trace = deploymentManager.tracer(); + const { + bridgeReceiver, + timelock: l2Timelock, + comet, + cometAdmin, + configurator + } = await deploymentManager.getContracts(); + + const { + arbitrumInbox, + timelock, + governor + } = await govDeploymentManager.getContracts(); + + newPriceFeedAddress = wUSDMPriceFeedAddress; + + const wUSDM = await deploymentManager.existing( + 'wUSDM', + WUSDM_ADDRESS, + 'arbitrum', + 'contracts/ERC20.sol:ERC20' + ); + + const wUSDMPriceFeed = await deploymentManager.existing( + 'wUSDM:priceFeed', + wUSDMPriceFeedAddress, + 'arbitrum' + ); + + const wUSDMAssetConfig = { + asset: wUSDM.address, + priceFeed: wUSDMPriceFeed.address, + decimals: 18n, + borrowCollateralFactor: exp(0.88, 18), + liquidateCollateralFactor: exp(0.90, 18), + liquidationFactor: exp(0.95, 18), + supplyCap: exp(4_500_000, 18), + }; + + const addAssetCalldata = ethers.utils.defaultAbiCoder.encode( + ['address', 'tuple(address,address,uint8,uint64,uint64,uint64,uint128)'], + [comet.address, + [ + wUSDMAssetConfig.asset, + wUSDMAssetConfig.priceFeed, + wUSDMAssetConfig.decimals, + wUSDMAssetConfig.borrowCollateralFactor, + wUSDMAssetConfig.liquidateCollateralFactor, + wUSDMAssetConfig.liquidationFactor, + wUSDMAssetConfig.supplyCap + ] + ] + ); + + const deployAndUpgradeToCalldata = ethers.utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + + const l2ProposalData = ethers.utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, + cometAdmin.address + ], + [ + 0, + 0 + ], + [ + 'addAsset(address,(address,address,uint8,uint64,uint64,uint64,uint128))', + 'deployAndUpgradeTo(address,address)', + ], + [ + addAssetCalldata, + deployAndUpgradeToCalldata, + ] + ] + ); + + const createRetryableTicketGasParams = await estimateL2Transaction( + { + from: applyL1ToL2Alias(timelock.address), + to: bridgeReceiver.address, + data: l2ProposalData + }, + deploymentManager + ); + const refundAddress = l2Timelock.address; + + const mainnetActions = [ + // 1. Set Comet configuration and deployAndUpgradeTo USDC Comet on Arbitrum. + { + contract: arbitrumInbox, + signature: 'createRetryableTicket(address,uint256,uint256,address,address,uint256,uint256,bytes)', + args: [ + bridgeReceiver.address, // address to, + 0, // uint256 l2CallValue, + createRetryableTicketGasParams.maxSubmissionCost, // uint256 maxSubmissionCost, + refundAddress, // address excessFeeRefundAddress, + refundAddress, // address callValueRefundAddress, + createRetryableTicketGasParams.gasLimit, // uint256 gasLimit, + createRetryableTicketGasParams.maxFeePerGas, // uint256 maxFeePerGas, + l2ProposalData, // bytes calldata data + ], + value: createRetryableTicketGasParams.deposit + }, + ]; + + const description = '# Add wUSDM as collateral into cUSDCv3 on Arbitrum\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes to add wUSDM into cUSDCv3 on Arbitrum network. This proposal takes the governance steps recommended and necessary to update a Compound III USDC market on Arbitrum. 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). The new parameters include setting the risk parameters based off of the [recommendations from Gauntlet](https://www.comp.xyz/t/list-wusdm-as-a-collateral-on-usdc-usdt-markets-on-arbitrum-and-ethereum/5590/3).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/931) and [forum discussion](https://www.comp.xyz/t/list-wusdm-as-a-collateral-on-usdc-usdt-markets-on-arbitrum-and-ethereum/5590).\n\n\n## Proposal Actions\n\nThe first proposal action adds wUSDM to the USDC Comet on Arbitrum. This sends the encoded `addAsset` and `deployAndUpgradeTo` calls across the bridge to the governance receiver on Arbitrum.'; + const txn = await govDeploymentManager.retry(async () => + trace(await governor.propose(...(await proposal(mainnetActions, description)))) + ); + + const event = txn.events.find(event => event.event === 'ProposalCreated'); + + const [proposalId] = event.args; + + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(): Promise { + return false; + }, + + async verify(deploymentManager: DeploymentManager) { + const { comet, configurator } = await deploymentManager.getContracts(); + + const wUSDMAssetIndex = Number(await comet.numAssets()) - 1; + + const wUSDM = await deploymentManager.existing( + 'wUSDM', + WUSDM_ADDRESS, + 'arbitrum', + 'contracts/ERC20.sol:ERC20' + ); + + const wUSDMAssetConfig = { + asset: wUSDM.address, + priceFeed: newPriceFeedAddress, + decimals: 18n, + borrowCollateralFactor: exp(0.88, 18), + liquidateCollateralFactor: exp(0.90, 18), + liquidationFactor: exp(0.95, 18), + supplyCap: exp(4_500_000, 18), + }; + + // 1. & 2. Compare wUSDM asset config with Comet and Configurator asset info + const cometWUSDMAssetInfo = await comet.getAssetInfoByAddress(WUSDM_ADDRESS); + expect(wUSDMAssetIndex).to.be.equal(cometWUSDMAssetInfo.offset); + expect(wUSDMAssetConfig.asset).to.be.equal(cometWUSDMAssetInfo.asset); + expect(exp(1, wUSDMAssetConfig.decimals)).to.be.equal(cometWUSDMAssetInfo.scale); + expect(wUSDMAssetConfig.borrowCollateralFactor).to.be.equal(cometWUSDMAssetInfo.borrowCollateralFactor); + expect(wUSDMAssetConfig.liquidateCollateralFactor).to.be.equal(cometWUSDMAssetInfo.liquidateCollateralFactor); + expect(wUSDMAssetConfig.liquidationFactor).to.be.equal(cometWUSDMAssetInfo.liquidationFactor); + expect(wUSDMAssetConfig.supplyCap).to.be.equal(cometWUSDMAssetInfo.supplyCap); + + const configuratorWUSDMAssetConfig = (await configurator.getConfiguration(comet.address)).assetConfigs[wUSDMAssetIndex]; + expect(wUSDMAssetConfig.asset).to.be.equal(configuratorWUSDMAssetConfig.asset); + expect(wUSDMAssetConfig.decimals).to.be.equal(configuratorWUSDMAssetConfig.decimals); + expect(wUSDMAssetConfig.borrowCollateralFactor).to.be.equal(configuratorWUSDMAssetConfig.borrowCollateralFactor); + expect(wUSDMAssetConfig.liquidateCollateralFactor).to.be.equal(configuratorWUSDMAssetConfig.liquidateCollateralFactor); + expect(wUSDMAssetConfig.liquidationFactor).to.be.equal(configuratorWUSDMAssetConfig.liquidationFactor); + expect(wUSDMAssetConfig.supplyCap).to.be.equal(configuratorWUSDMAssetConfig.supplyCap); + }, +}); diff --git a/deployments/arbitrum/usdc/relations.ts b/deployments/arbitrum/usdc/relations.ts index 462bfc1a4..9a8535766 100644 --- a/deployments/arbitrum/usdc/relations.ts +++ b/deployments/arbitrum/usdc/relations.ts @@ -59,4 +59,12 @@ export default { } } }, + ERC1967Proxy: { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, }; \ No newline at end of file diff --git a/deployments/mainnet/usds/configuration.json b/deployments/mainnet/usds/configuration.json new file mode 100644 index 000000000..08c9c2de6 --- /dev/null +++ b/deployments/mainnet/usds/configuration.json @@ -0,0 +1,75 @@ +{ + "name": "Compound USDS", + "symbol": "cUSDSv3", + "baseToken": "USDS", + "baseTokenAddress": "0xdC035D45d973E3EC169d2276DDab16f1e407384F", + "borrowMin": "10e18", + "governor": "0x6d903f6003cca6255d85cca4d3b5e5146dc33925", + "pauseGuardian": "0xbbf3f1421d886e9b2c5d716b5192ac998af2012c", + "baseTokenPriceFeed": "0xfF30586cD0F29eD462364C7e81375FC0C71219b1", + "storeFrontPriceFactor": 0.6, + "targetReserves": "20_000_000e18", + "rates": { + "borrowBase": 0.015, + "borrowSlopeLow": 0.0333, + "borrowKink": 0.9, + "borrowSlopeHigh": 4.0, + "supplyBase": 0, + "supplySlopeLow": 0.039, + "supplyKink": 0.9, + "supplySlopeHigh": 3.6 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "289351851851e0", + "baseBorrowSpeed": "289351851851e0", + "baseMinForRewards": "10000e18" + }, + "rewardTokenAddress": "0xc00e94Cb662C3520282E6f5717214004A7f26888", + "assets": { + "WETH": { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "priceFeed": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", + "decimals": "18", + "borrowCF": 0.83, + "liquidateCF": 0.9, + "liquidationFactor": 0.95, + "supplyCap": "50_000e18" + }, + "USDe": { + "address": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3", + "priceFeed": "0xa569d910839Ae8865Da8F8e70FfFb0cBA869F961", + "decimals": "18", + "borrowCF": 0.7, + "liquidateCF": 0.75, + "liquidationFactor": 0.85, + "supplyCap": "50_000_000e18" + }, + "cbBTC": { + "address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + "priceFeed": "0x2665701293fCbEB223D11A08D826563EDcCE423A", + "decimals": "8", + "borrowCF": 0.8, + "liquidateCF": 0.85, + "liquidationFactor": 0.95, + "supplyCap": "150e8" + }, + "tBTC": { + "address": "0x18084fbA666a33d37592fA2633fD49a74DD93a88", + "priceFeed": "0x8350b7De6a6a2C1368E7D4Bd968190e13E354297", + "decimals": "18", + "borrowCF": 0.76, + "liquidateCF": 0.81, + "liquidationFactor": 0.9, + "supplyCap": "285e18" + }, + "wstETH": { + "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "decimals": "18", + "borrowCF": 0.82, + "liquidateCF": 0.87, + "liquidationFactor": 0.92, + "supplyCap": "10_000e18" + } + } +} \ No newline at end of file diff --git a/deployments/mainnet/usds/deploy.ts b/deployments/mainnet/usds/deploy.ts new file mode 100644 index 000000000..82bbcdbc0 --- /dev/null +++ b/deployments/mainnet/usds/deploy.ts @@ -0,0 +1,40 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet } from '../../../src/deploy'; + +export default async function deploy(deploymentManager: DeploymentManager, deploySpec: DeploySpec): Promise { + const WETH = await deploymentManager.existing('WETH', '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'); + const COMP = await deploymentManager.existing('COMP', '0xc00e94Cb662C3520282E6f5717214004A7f26888'); + const wstETH = await deploymentManager.existing('wstETH', '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'); + const cbBTC = await deploymentManager.existing('cbBTC', '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf'); + const tBTC = await deploymentManager.existing('tBTC', '0x18084fbA666a33d37592fA2633fD49a74DD93a88'); + const USDe = await deploymentManager.existing('USDe', '0x4c9EDD5852cd905f086C759E8383e09bff1E68B3'); + const USDS = await deploymentManager.existing('USDS', '0xdC035D45d973E3EC169d2276DDab16f1e407384F'); + + const wstETHtoUsdPriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'pricefeeds/WstETHPriceFeed.sol', + [ + '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419', // ETH / USD price feed + wstETH.address, // wstETH token + 8, // decimals + ], + true + ); + + // Import shared contracts from cUSDCv3 + const cometAdmin = await deploymentManager.fromDep('cometAdmin', 'mainnet', 'usdc'); + const $configuratorImpl = await deploymentManager.fromDep('configurator:implementation', 'mainnet', 'usdc'); + const configurator = await deploymentManager.fromDep('configurator', 'mainnet', 'usdc'); + const rewards = await deploymentManager.fromDep('rewards', 'mainnet', 'usdc'); + const bulker = await deploymentManager.fromDep('bulker', 'mainnet', 'weth'); + + // Deploy Comet + const deployed = await deployComet(deploymentManager, deploySpec); + + return { + ...deployed, + bulker, + rewards, + COMP + }; +} \ No newline at end of file diff --git a/deployments/mainnet/usds/migrations/1729069465_configurate_and_ens.ts b/deployments/mainnet/usds/migrations/1729069465_configurate_and_ens.ts new file mode 100644 index 000000000..0512a7db6 --- /dev/null +++ b/deployments/mainnet/usds/migrations/1729069465_configurate_and_ens.ts @@ -0,0 +1,279 @@ +import { ethers, utils } from 'ethers'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { exp, getConfigurationStruct, proposal } from '../../../../src/deploy'; +import { expect } from 'chai'; + +const ENSName = 'compound-community-licenses.eth'; +const ENSResolverAddress = '0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41'; +const ENSRegistryAddress = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; +const ENSSubdomainLabel = 'v3-additional-grants'; +const ENSSubdomain = `${ENSSubdomainLabel}.${ENSName}`; +const ENSTextRecordKey = 'v3-official-markets'; + +const USDSAmount = ethers.BigNumber.from(exp(300_000, 18)); +const cDAIAddress = '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643'; +const DaiToUsdsConverterAddress = '0x3225737a9Bbb6473CB4a45b7244ACa2BeFdB276A'; +const DAIAddress = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; + +export default migration('1729069465_configurate_and_ens', { + async prepare() { + return {}; + }, + + async enact(deploymentManager: DeploymentManager, _) { + const trace = deploymentManager.tracer(); + + const { + comet, + cometAdmin, + configurator, + rewards, + COMP, + governor + } = await deploymentManager.getContracts(); + + const configuration = await getConfigurationStruct(deploymentManager); + const cometFactory = await deploymentManager.fromDep('cometFactory', 'mainnet', 'usdt', true); + + const ENSResolver = await deploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const currentChainId = 1; + const newMarketObject = { baseSymbol: 'USDS', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse(await ENSResolver.text(subdomainHash, ENSTextRecordKey)); + + if (officialMarketsJSON[currentChainId]) { + officialMarketsJSON[currentChainId].push(newMarketObject); + } else { + officialMarketsJSON[currentChainId] = [newMarketObject]; + } + + const _reduceReservesCalldata = utils.defaultAbiCoder.encode( + ['uint256'], + [USDSAmount] + ); + + const approveCalldata = utils.defaultAbiCoder.encode( + ['address', 'uint256'], + [DaiToUsdsConverterAddress, USDSAmount] + ); + + const convertCalldata = utils.defaultAbiCoder.encode( + ['address', 'uint256'], + [comet.address, USDSAmount] + ); + + const actions = [ + // 1. Set the Comet factory in configuration + { + contract: configurator, + signature: 'setFactory(address,address)', + args: [comet.address, cometFactory.address], + }, + // 2. Set the Comet configuration + { + contract: configurator, + signature: '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)[]))', + args: [comet.address, configuration], + }, + // 3. Deploy Comet and upgrade it to the new implementation + { + contract: cometAdmin, + signature: 'deployAndUpgradeTo(address,address)', + args: [configurator.address, comet.address], + }, + // 4. Set the reward configuration + { + contract: rewards, + signature: 'setRewardConfig(address,address)', + args: [comet.address, COMP.address], + }, + + // 5. Get DAI reserves from cDAI contract + { + target: cDAIAddress, + signature: '_reduceReserves(uint256)', + calldata: _reduceReservesCalldata + }, + // 6. Approve DAI to the Comet contract + { + target: DAIAddress, + signature: 'approve(address,uint256)', + calldata: approveCalldata, + }, + // 7. Convert DAI to USDS + { + target: DaiToUsdsConverterAddress, + signature: 'daiToUsds(address,uint256)', + calldata: convertCalldata + }, + // 8. 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 cUSDSv3 on Ethereum Mainnet\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes the deployment of Compound III to the Mainnet network. This proposal takes the governance steps recommended and necessary to initialize a Compound III USDS market on Mainnet; upon execution, cUSDSv3 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). The new parameters include setting the risk parameters based off of the [recommendations from Gauntlet](https://www.comp.xyz/t/add-collateral-usds-market-on-eth-mainnet/5781/5).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/942), [deploy market GitHub action run](https://github.com/woof-software/comet/actions/runs/11392132048) and [forum discussion](https://www.comp.xyz/t/add-collateral-usds-market-on-eth-mainnet/5781).\n\n\n## wstETH price feed\n\nFor LSTs, the goal is to use full exchange rate price feeds. Thus, we treat that stETH:ETH is 1:1.\n\n## sUSDS and sUSDe collaterals\n\nGauntlet suggests having sUDSS and sUSDe collaterals. This proposal does not include them, because the price feed is under the audit. We suggest to start bootstrapping the liquidity without these collaterals and as the price feed is audited, we will add these collaterals. We discussed it with Gauntlet, and we received the approval to have such an approach.\n\n## Proposal Actions\n\nThe first proposal action sets the CometFactory for the new Comet instance in the existing Configurator.\n\nThe second action configures the Comet instance in the Configurator.\n\nThe third action deploys an instance of the newly configured factory and upgrades the Comet instance to use that implementation.\n\nThe fourth action configures the existing rewards contract for the newly deployed Comet instance.\n\nThe fifth action reduces Compound’s [cDAI](https://etherscan.io/address/0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643) reserves and transfers it to Timelock, in order to convert it to USDS to then seed the market reserves for the cUSDSv3 Comet.\n\nThe sixth action approves DAI to DAI-to-USDS native converter.\n\nThe seventh action converts DAI to USDS with 1:1 ratio and transfers USDS to cUSDSv3 Comet.\n\nThe eight 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 Ethereum Mainnet cUSDSv3 market.'; + const txn = await deploymentManager.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) { + + const { + comet, + rewards, + timelock, + WETH, + wstETH, + cbBTC, + tBTC, + USDe, + COMP + } = await deploymentManager.getContracts(); + + // 1. & 2. & 3. + const cbbtcInfo = await comet.getAssetInfoByAddress(cbBTC.address); + const tbtcInfo = await comet.getAssetInfoByAddress(tBTC.address); + const wethInfo = await comet.getAssetInfoByAddress(WETH.address); + const usdeInfo = await comet.getAssetInfoByAddress(USDe.address); + const wstETHInfo = await comet.getAssetInfoByAddress(wstETH.address); + + expect(cbbtcInfo.supplyCap).to.be.eq(exp(150, 8)); + expect(tbtcInfo.supplyCap).to.be.eq(exp(285, 18)); + expect(wethInfo.supplyCap).to.be.eq(exp(50_000, 18)); + expect(usdeInfo.supplyCap).to.be.eq(exp(50_000_000, 18)); + expect(wstETHInfo.supplyCap).to.be.eq(exp(10_000, 18)); + + expect(await comet.baseTrackingSupplySpeed()).to.be.equal(exp(25 / 86400, 15, 18)); // 289351851851 + expect(await comet.baseTrackingBorrowSpeed()).to.be.equal(exp(25 / 86400, 15, 18)); // 289351851851 + + // 4 + 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); + + expect((await comet.pauseGuardian()).toLowerCase()).to.be.eq('0xbbf3f1421d886e9b2c5d716b5192ac998af2012c'); + + // 5. & 6. + expect(await comet.getReserves()).to.be.equal(USDSAmount); + + // 7. + const ENSResolver = await deploymentManager.existing('ENSResolver', ENSResolverAddress); + const ENSRegistry = await deploymentManager.existing('ENSRegistry', ENSRegistryAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const officialMarketsJSON = await ENSResolver.text(subdomainHash, ENSTextRecordKey); + expect(await ENSRegistry.recordExists(subdomainHash)).to.be.equal(true); + expect(await ENSRegistry.owner(subdomainHash)).to.be.equal(timelock.address); + expect(await ENSRegistry.resolver(subdomainHash)).to.be.equal(ENSResolverAddress); + expect(await ENSRegistry.ttl(subdomainHash)).to.be.equal(0); + + const officialMarkets = JSON.parse(officialMarketsJSON); + expect(officialMarkets).to.deep.equal({ + 1: [ + { + baseSymbol: 'USDC', + cometAddress: '0xc3d688B66703497DAA19211EEdff47f25384cdc3', + }, + { + baseSymbol: 'WETH', + cometAddress: '0xA17581A9E3356d9A858b789D68B4d866e593aE94', + }, + { + baseSymbol: 'USDT', + cometAddress: '0x3Afdc9BCA9213A35503b077a6072F3D0d5AB0840' + }, + { + baseSymbol: 'wstETH', + cometAddress: '0x3D0bb1ccaB520A66e607822fC55BC921738fAFE3', + }, + { + baseSymbol: 'USDS', + cometAddress: comet.address, + } + ], + 10: [ + { + baseSymbol: 'USDC', + cometAddress: '0x2e44e174f7D53F0212823acC11C01A11d58c5bCB', + }, + { + baseSymbol: 'USDT', + cometAddress: '0x995E394b8B2437aC8Ce61Ee0bC610D617962B214', + }, + { + baseSymbol: 'WETH', + cometAddress: '0xE36A30D249f7761327fd973001A32010b521b6Fd' + } + ], + 137: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF25212E676D1F7F89Cd72fFEe66158f541246445', + }, + { + baseSymbol: 'USDT', + cometAddress: '0xaeB318360f27748Acb200CE616E389A6C9409a07', + }, + ], + 8453: [ + { + baseSymbol: 'USDbC', + cometAddress: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + }, + { + baseSymbol: 'WETH', + cometAddress: '0x46e6b214b524310239732D51387075E0e70970bf', + }, + { + baseSymbol: 'USDC', + cometAddress: '0xb125E6687d4313864e53df431d5425969c15Eb2F', + }, + { + baseSymbol: 'AERO', + cometAddress: '0x784efeB622244d2348d4F2522f8860B96fbEcE89' + } + ], + 42161: [ + { + baseSymbol: 'USDC.e', + cometAddress: '0xA5EDBDD9646f8dFF606d7448e414884C7d905dCA', + }, + { + baseSymbol: 'USDC', + cometAddress: '0x9c4ec768c28520B50860ea7a15bd7213a9fF58bf', + }, + { + baseSymbol: 'WETH', + cometAddress: '0x6f7D514bbD4aFf3BcD1140B7344b32f063dEe486', + }, + { + baseSymbol: 'USDT', + cometAddress: '0xd98Be00b5D27fc98112BdE293e487f8D4cA57d07', + }, + ], + 534352: [ + { + baseSymbol: 'USDC', + cometAddress: '0xB2f97c1Bd3bf02f5e74d13f02E3e26F93D77CE44', + }, + ], + }); + }, +}); \ No newline at end of file diff --git a/deployments/mainnet/usds/relations.ts b/deployments/mainnet/usds/relations.ts new file mode 100644 index 000000000..7b0cc418d --- /dev/null +++ b/deployments/mainnet/usds/relations.ts @@ -0,0 +1,25 @@ +import { RelationConfigMap } from '../../../plugins/deployment_manager/RelationConfig'; +import baseRelationConfig from '../../relations'; + +export default { + ...baseRelationConfig, + 'wstETH': { + artifact: 'contracts/bulkers/IWstETH.sol', + relations: { + stETH: { + field: async (wstETH) => wstETH.stETH() + } + } + }, + 'AppProxyUpgradeable': { + artifact: 'contracts/ERC20.sol:ERC20', + }, + 'ERC1967Proxy': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, +}; \ No newline at end of file diff --git a/deployments/mainnet/usds/roots.json b/deployments/mainnet/usds/roots.json new file mode 100644 index 000000000..565665908 --- /dev/null +++ b/deployments/mainnet/usds/roots.json @@ -0,0 +1,7 @@ +{ + "comet": "0x5D409e56D886231aDAf00c8775665AD0f9897b56", + "configurator": "0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3", + "rewards": "0x1B0e765F6224C21223AeA2af16c1C46E38885a40", + "bulker": "0xa397a8C2086C554B531c02E29f3291c9704B00c7", + "COMP": "0xc00e94Cb662C3520282E6f5717214004A7f26888" +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 411476a38..f2cf294fc 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -27,6 +27,7 @@ import mainnetRelationConfigMap from './deployments/mainnet/usdc/relations'; import mainnetWethRelationConfigMap from './deployments/mainnet/weth/relations'; import mainnetUsdtRelationConfigMap from './deployments/mainnet/usdt/relations'; import mainnetWstETHRelationConfigMap from './deployments/mainnet/wsteth/relations'; +import mainnetUsdsRelationConfigMap from './deployments/mainnet/usds/relations'; import polygonRelationConfigMap from './deployments/polygon/usdc/relations'; import polygonUsdtRelationConfigMap from './deployments/polygon/usdt/relations'; import arbitrumBridgedUsdcRelationConfigMap from './deployments/arbitrum/usdc.e/relations'; @@ -356,7 +357,8 @@ const config: HardhatUserConfig = { usdc: mainnetRelationConfigMap, weth: mainnetWethRelationConfigMap, usdt: mainnetUsdtRelationConfigMap, - wsteth: mainnetWstETHRelationConfigMap + wsteth: mainnetWstETHRelationConfigMap, + usds: mainnetUsdsRelationConfigMap, }, polygon: { usdc: polygonRelationConfigMap, @@ -422,6 +424,11 @@ const config: HardhatUserConfig = { network: 'mainnet', deployment: 'wsteth' }, + { + name: 'mainnet-usds', + network: 'mainnet', + deployment: 'usds' + }, { name: 'development', network: 'hardhat', diff --git a/scenario/LiquidationBotScenario.ts b/scenario/LiquidationBotScenario.ts index 8c504b8e1..45dd6922c 100644 --- a/scenario/LiquidationBotScenario.ts +++ b/scenario/LiquidationBotScenario.ts @@ -5,6 +5,7 @@ import { ethers, event, exp, wait } from '../test/helpers'; import CometActor from './context/CometActor'; import { CometInterface, OnChainLiquidator } from '../build/types'; import { getPoolConfig, flashLoanPools } from '../scripts/liquidation_bot/liquidateUnderwaterBorrowers'; +import { getConfigForScenario } from './utils/scenarioHelper'; interface LiquidationAddresses { balancerVault: string; @@ -537,16 +538,20 @@ for (let i = 0; i < MAX_ASSETS; i++) { scenario( `LiquidationBot > absorbs, but does not attempt to purchase collateral when value is beneath liquidationThreshold`, { - filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]) && !matchesDeployment(ctx, [{deployment: 'wsteth', network: 'mainnet'}]), - tokenBalances: { - $comet: { $base: 100000 }, - }, - cometBalances: { - albert: { - $asset0: ' == 200', - }, - betty: { $base: 1000 }, - }, + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]), + tokenBalances: async (ctx) => ( + { + $comet: { $base: getConfigForScenario(ctx).liquidationBase }, + } + ), + cometBalances: async (ctx) => ( + { + albert: { + $asset0: ` == ${getConfigForScenario(ctx).liquidationAsset}`, + }, + betty: { $base: getConfigForScenario(ctx).liquidationBase1 }, + } + ) }, async ({ comet, actors }, _context, world) => { const { albert, betty } = actors; @@ -583,7 +588,7 @@ scenario( const [initialNumAbsorbs, initialNumAbsorbed] = await comet.liquidatorPoints(betty.address); const borrowCapacity = await borrowCapacityForAsset(comet, albert, 0); - const borrowAmount = (borrowCapacity.mul(90n)).div(100n); + const borrowAmount = (borrowCapacity.mul(getConfigForScenario(_context).liquidationDenominator)).div(100n); await albert.withdrawAsset({ asset: baseToken, @@ -648,16 +653,20 @@ scenario( scenario( `LiquidationBot > absorbs, but does not attempt to purchase collateral when maxAmountToPurchase=0`, { - filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]) && !matchesDeployment(ctx, [{deployment: 'wsteth', network: 'mainnet'}]), - tokenBalances: { - $comet: { $base: 100000 }, - }, - cometBalances: { - albert: { - $asset0: ' == 200', - }, - betty: { $base: 1000 }, - }, + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]), + tokenBalances: async (ctx) => ( + { + $comet: { $base: getConfigForScenario(ctx).liquidationBase }, + } + ), + cometBalances: async (ctx) => ( + { + albert: { + $asset0: ` == ${getConfigForScenario(ctx).liquidationAsset}}`, + }, + betty: { $base: getConfigForScenario(ctx).liquidationBase1 }, + } + ) }, async ({ comet, actors }, _context, world) => { const { albert, betty } = actors; @@ -694,7 +703,7 @@ scenario( const [initialNumAbsorbs, initialNumAbsorbed] = await comet.liquidatorPoints(betty.address); const borrowCapacity = await borrowCapacityForAsset(comet, albert, 0); - const borrowAmount = (borrowCapacity.mul(90n)).div(100n); + const borrowAmount = (borrowCapacity.mul(getConfigForScenario(_context).liquidationDenominator)).div(100n); await albert.withdrawAsset({ asset: baseToken, @@ -761,7 +770,8 @@ scenario( mainnet: { usdc: 2250000, weth: 20, - usdt: 2250000 + usdt: 2250000, + usds: 225000, }, }; const assetAmounts = { @@ -769,6 +779,7 @@ scenario( usdc: ' == 5000', // COMP weth: ' == 7000', // CB_ETH usdt: ' == 5000', // COMP + usds: ' == 850', // WETH }, }; @@ -778,7 +789,7 @@ scenario( upgrade: { targetReserves: exp(20_000, 18) }, - filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }]) && !matchesDeployment(ctx, [{deployment: 'wsteth'}]), + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }]) && !matchesDeployment(ctx, [{deployment: 'wsteth'}, {deployment: 'usds'}]), tokenBalances: async (ctx) => ( { $comet: { @@ -829,7 +840,7 @@ scenario( const [initialNumAbsorbs, initialNumAbsorbed] = await comet.liquidatorPoints(betty.address); const borrowCapacity = await borrowCapacityForAsset(comet, albert, 0); - const borrowAmount = (borrowCapacity.mul(90n)).div(100n); + const borrowAmount = (borrowCapacity.mul(getConfigForScenario(_context).liquidationDenominator)).div(100n); await albert.withdrawAsset({ asset: baseToken, diff --git a/scenario/MainnetBulkerScenario.ts b/scenario/MainnetBulkerScenario.ts index 590aacaba..fdfa20a5e 100644 --- a/scenario/MainnetBulkerScenario.ts +++ b/scenario/MainnetBulkerScenario.ts @@ -52,7 +52,7 @@ scenario( const toSupplyStEth = exp(.1, 18); - await context.sourceTokens(toSupplyStEth + 2n, new CometAsset(stETH), albert); + await context.sourceTokens(toSupplyStEth + 3n, new CometAsset(stETH), albert); expect(await stETH.balanceOf(albert.address)).to.be.greaterThanOrEqual(toSupplyStEth); @@ -68,7 +68,7 @@ scenario( await albert.invoke({ actions, calldata }); - expect(await stETH.balanceOf(albert.address)).to.be.approximately(0n, 1n); + expectApproximately((await stETH.balanceOf(albert.address)).toBigInt(), 0n, 2n); expectApproximately( (await comet.collateralBalanceOf(albert.address, wstETH.address)).toBigInt(), (await wstETH.getWstETHByStETH(toSupplyStEth)).toBigInt(), diff --git a/scenario/constraints/ProposalConstraint.ts b/scenario/constraints/ProposalConstraint.ts index da4899ea3..5c507707f 100644 --- a/scenario/constraints/ProposalConstraint.ts +++ b/scenario/constraints/ProposalConstraint.ts @@ -62,15 +62,15 @@ export class ProposalConstraint implements StaticConstra ); } - // temporary hack to skip proposal 339 - if (proposal.id.eq(339)) { - console.log('Skipping proposal 339'); + // temporary hack to skip proposal 348 + if (proposal.id.eq(348)) { + console.log('Skipping proposal 348'); continue; } - // temporary hack to skip proposal 340 - if (proposal.id.eq(340)) { - console.log('Skipping proposal 340'); + // temporary hack to skip proposal 349 + if (proposal.id.eq(349)) { + console.log('Skipping proposal 349'); continue; } diff --git a/scenario/utils/scenarioHelper.ts b/scenario/utils/scenarioHelper.ts index 9ef7b4d12..5d63f4599 100644 --- a/scenario/utils/scenarioHelper.ts +++ b/scenario/utils/scenarioHelper.ts @@ -33,6 +33,7 @@ export function getConfigForScenario(ctx: CometContext) { config.transferAsset = 500; config.interestSeconds = 70; } + if (ctx.world.base.network === 'mainnet' && ctx.world.base.deployment === 'wsteth') { config.liquidationBase = 10000; config.liquidationBase1 = 1000; @@ -41,6 +42,10 @@ export function getConfigForScenario(ctx: CometContext) { config.interestSeconds = 70; } + if (ctx.world.base.network === 'mainnet' && ctx.world.base.deployment === 'usds') { + config.liquidationAsset = 100; + } + if (ctx.world.base.network === 'base' && ctx.world.base.deployment === 'aero') { config.interestSeconds = 110; } diff --git a/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts b/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts index b2d5d4e0b..29a70303e 100644 --- a/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts +++ b/scripts/liquidation_bot/liquidateUnderwaterBorrowers.ts @@ -39,6 +39,7 @@ const addresses = { WETH9: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', CB_ETH: '0xBe9895146f7AF43049ca1c1AE358B0541Ea49704', WST_ETH: '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0', + RS_ETH: '0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7', USDT: '0xdAC17F958D2ee523a2206206994597C13D831ec7' }, goerli: { @@ -94,6 +95,14 @@ export const flashLoanPools = { usdt: { tokenAddress: addresses.mainnet.DAI, poolFee: 100 + }, + wsteth: { + tokenAddress: addresses.mainnet.WETH9, + poolFee: 100 + }, + usds: { + tokenAddress: addresses.mainnet.DAI, + poolFee: 3000, } }, goerli: { @@ -189,6 +198,13 @@ export function getPoolConfig(tokenAddress: string) { uniswapPoolFee: 500 } }, + [addresses.mainnet.RS_ETH.toLowerCase()]: { + ...defaultPoolConfig, + ...{ + exchange: Exchange.Balancer, + balancerPoolId: '0x58aadfb1afac0ad7fca1148f3cde6aedf5236b6d00000000000000000000067f' + } + }, [addresses.mainnet.WST_ETH.toLowerCase()]: { ...defaultPoolConfig, ...{ diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 7381208a9..717d8a242 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -89,6 +89,8 @@ export const WHALES = { '0x2775b1c75658be0f640272ccb8c72ac986009e38', '0x1a9c8182c09f50c8318d769245bea52c32be35bc', '0x3c22ec75ea5D745c78fc84762F7F1E6D82a2c5BF', + '0x88a1493366D48225fc3cEFbdae9eBb23E323Ade3', // USDe whale + '0x43594da5d6A03b2137a04DF5685805C676dEf7cB', // rsETH whale '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b' ], polygon: [ @@ -112,7 +114,8 @@ export const WHALES = { '0xee3273f6d29ddfff08ffd9d513cff314734f01a2', // COMP whale '0x9e786a8fc88ee74b758b125071d45853356024c3', // COMP whale '0xd93f76944e870900779c09ddf1c46275f9d8bf9b', // COMP whale - '0xe68ee8a12c611fd043fb05d65e1548dc1383f2b9' // native USDC whale + '0xe68ee8a12c611fd043fb05d65e1548dc1383f2b9', // native USDC whale + '0x56CC5A9c0788e674f17F7555dC8D3e2F1C0313C0', // wUSDM whale ], base: [ '0x6D3c5a4a7aC4B1428368310E4EC3bB1350d01455', // USDbC whale