From f68439d1b0b3d98b4a87c6d344a0b4694b055a77 Mon Sep 17 00:00:00 2001 From: MishaShWoof Date: Fri, 16 Aug 2024 22:34:39 +0300 Subject: [PATCH] Add wstETH as collateral to USDC market on Mainnet (#884) Co-authored-by: dmitriy-woof-software --- .../1720623615_add_wsteth_as_collateral.ts | 142 ++++++++++++++++++ deployments/mainnet/usdc/relations.ts | 11 ++ scenario/MainnetBulkerScenario.ts | 107 ++++++++----- 3 files changed, 224 insertions(+), 36 deletions(-) create mode 100644 deployments/mainnet/usdc/migrations/1720623615_add_wsteth_as_collateral.ts diff --git a/deployments/mainnet/usdc/migrations/1720623615_add_wsteth_as_collateral.ts b/deployments/mainnet/usdc/migrations/1720623615_add_wsteth_as_collateral.ts new file mode 100644 index 000000000..b258c7f23 --- /dev/null +++ b/deployments/mainnet/usdc/migrations/1720623615_add_wsteth_as_collateral.ts @@ -0,0 +1,142 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { exp, proposal } from '../../../../src/deploy'; + +const WSTETH_ADDRESS = '0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0'; + +let priceFeedAddress: string; + +export default migration('1720623615_add_wsteth_as_collateral', { + async prepare() { + return {}; + }, + + enact: async (deploymentManager: DeploymentManager) => { + const trace = deploymentManager.tracer(); + + const wstETH = await deploymentManager.existing( + 'wstETH', + WSTETH_ADDRESS, + 'mainnet', + 'contracts/ERC20.sol:ERC20' + ); + const wstETHPricefeed = await deploymentManager.fromDep('wstETH:priceFeed', 'mainnet', 'usdt'); + priceFeedAddress = wstETHPricefeed.address; + const { + governor, + comet, + cometAdmin, + configurator + } = await deploymentManager.getContracts(); + + const newAssetConfig = { + asset: wstETH.address, + priceFeed: wstETHPricefeed.address, + decimals: await wstETH.decimals(), + borrowCollateralFactor: exp(0.82, 18), + liquidateCollateralFactor: exp(0.87, 18), + liquidationFactor: exp(0.92, 18), + supplyCap: exp(15_000, 18), + }; + + const mainnetActions = [ + // 1. Add weETH as asset + { + contract: configurator, + signature: 'addAsset(address,(address,address,uint8,uint64,uint64,uint64,uint128))', + args: [comet.address, newAssetConfig], + }, + // 2. Deploy and upgrade to a new version of Comet + { + contract: cometAdmin, + signature: 'deployAndUpgradeTo(address,address)', + args: [configurator.address, comet.address], + }, + ]; + + const description = '# Add wstETH as collateral into cUSDCv3 on Ethereum\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes to add wstETH into cUSDCv3 on Ethereum network. This proposal takes the governance steps recommended and necessary to update a Compound III USDC market on Ethereum. 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/gauntlet-wsteth-and-ezeth-asset-listing/5404/1).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/884) and [forum discussion](https://www.comp.xyz/t/gauntlet-wsteth-and-ezeth-asset-listing/5404).\n\n\n## Proposal Actions\n\nThe first proposal action adds wstETH asset as collateral with corresponding configurations.\n\nThe second action deploys and upgrades Comet to a new version.'; + const txn = await deploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ) + ); + + const event = txn.events.find( + (event) => event.event === 'ProposalCreated' + ); + const [proposalId] = event.args; + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(): Promise { + return true; + }, + + async verify(deploymentManager: DeploymentManager) { + const { comet, configurator } = await deploymentManager.getContracts(); + + const wstETHAssetIndex = Number(await comet.numAssets()) - 1; + + const wstETHAssetConfig = { + asset: WSTETH_ADDRESS, + priceFeed: priceFeedAddress, + decimals: 18, + borrowCollateralFactor: exp(0.82, 18), + liquidateCollateralFactor: exp(0.87, 18), + liquidationFactor: exp(0.92, 18), + supplyCap: exp(15_000, 18), + }; + + // 1. Compare proposed asset config with Comet asset info + const wstETHAssetInfo = await comet.getAssetInfoByAddress( + WSTETH_ADDRESS + ); + expect(wstETHAssetIndex).to.be.equal(wstETHAssetInfo.offset); + expect(wstETHAssetConfig.asset).to.be.equal(wstETHAssetInfo.asset); + expect(wstETHAssetConfig.priceFeed).to.be.equal( + wstETHAssetInfo.priceFeed + ); + expect(exp(1, wstETHAssetConfig.decimals)).to.be.equal( + wstETHAssetInfo.scale + ); + expect(wstETHAssetConfig.borrowCollateralFactor).to.be.equal( + wstETHAssetInfo.borrowCollateralFactor + ); + expect(wstETHAssetConfig.liquidateCollateralFactor).to.be.equal( + wstETHAssetInfo.liquidateCollateralFactor + ); + expect(wstETHAssetConfig.liquidationFactor).to.be.equal( + wstETHAssetInfo.liquidationFactor + ); + expect(wstETHAssetConfig.supplyCap).to.be.equal( + wstETHAssetInfo.supplyCap + ); + + // 2. Compare proposed asset config with Configurator asset config + const configuratorWstETHAssetConfig = ( + await configurator.getConfiguration(comet.address) + ).assetConfigs[wstETHAssetIndex]; + expect(wstETHAssetConfig.asset).to.be.equal( + configuratorWstETHAssetConfig.asset + ); + expect(wstETHAssetConfig.priceFeed).to.be.equal( + configuratorWstETHAssetConfig.priceFeed + ); + expect(wstETHAssetConfig.decimals).to.be.equal( + configuratorWstETHAssetConfig.decimals + ); + expect(wstETHAssetConfig.borrowCollateralFactor).to.be.equal( + configuratorWstETHAssetConfig.borrowCollateralFactor + ); + expect(wstETHAssetConfig.liquidateCollateralFactor).to.be.equal( + configuratorWstETHAssetConfig.liquidateCollateralFactor + ); + expect(wstETHAssetConfig.liquidationFactor).to.be.equal( + configuratorWstETHAssetConfig.liquidationFactor + ); + expect(wstETHAssetConfig.supplyCap).to.be.equal( + configuratorWstETHAssetConfig.supplyCap + ); + }, +}); diff --git a/deployments/mainnet/usdc/relations.ts b/deployments/mainnet/usdc/relations.ts index 4aa0946b3..4f242ef3d 100644 --- a/deployments/mainnet/usdc/relations.ts +++ b/deployments/mainnet/usdc/relations.ts @@ -3,6 +3,17 @@ 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', + }, fxRoot: { relations: { stateSender: { diff --git a/scenario/MainnetBulkerScenario.ts b/scenario/MainnetBulkerScenario.ts index 127581aaf..997600cb2 100644 --- a/scenario/MainnetBulkerScenario.ts +++ b/scenario/MainnetBulkerScenario.ts @@ -10,22 +10,45 @@ import { import { exp } from '../test/helpers'; import { expectApproximately, isBulkerSupported, matchesDeployment } from './utils'; +const MAINNET_WSTETH_ADDRESS = '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'; +const MAINNET_STETH_ADDRESS = '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84'; + +async function getWstETHIndex(context: any): Promise { + const comet = await context.getComet(); + const totalAssets = await comet.numAssets(); + for (let i = 0; i < totalAssets; i++) { + const asset = await comet.getAssetInfo(i); + if (asset.asset.toLowerCase() === MAINNET_WSTETH_ADDRESS) { + return i; + } + } + return -1; +} + +async function hasWstETH(context: any): Promise { + return (await getWstETHIndex(context) > -1); +} + scenario( 'MainnetBulker > wraps stETH before supplying', { - filter: async (ctx) => await isBulkerSupported(ctx) && matchesDeployment(ctx, [{network: 'mainnet', deployment: 'weth'}]), - supplyCaps: { - $asset1: 1, - }, - tokenBalances: { - albert: { $asset1: '== 0' }, - }, + filter: async (ctx) => await hasWstETH(ctx) && await isBulkerSupported(ctx) && matchesDeployment(ctx, [{ network: 'mainnet' }]), + supplyCaps: async (ctx) => ( + { + [`$asset${await getWstETHIndex(ctx)}`]: 1, + } + ), + tokenBalances: async (ctx) => ( + { + albert: { [`$asset${await getWstETHIndex(ctx)}`]: '== 0' }, + } + ), }, async ({ comet, actors, bulker }, context) => { const { albert } = actors; - const stETH = await context.world.deploymentManager.contract('stETH') as ERC20; - const wstETH = await context.world.deploymentManager.contract('wstETH') as IWstETH; + const stETH = await context.world.deploymentManager.hre.ethers.getContractAt('ERC20', MAINNET_STETH_ADDRESS) as ERC20; + const wstETH = await context.world.deploymentManager.hre.ethers.getContractAt('IWstETH', MAINNET_WSTETH_ADDRESS) as IWstETH; const toSupplyStEth = exp(.1, 18); @@ -57,23 +80,29 @@ scenario( scenario( 'MainnetBulker > unwraps wstETH before withdrawing', { - filter: async (ctx) => await isBulkerSupported(ctx) && matchesDeployment(ctx, [{network: 'mainnet', deployment: 'weth'}]), - supplyCaps: { - $asset1: 2, - }, - tokenBalances: { - albert: { $asset1: 2 }, - $comet: { $asset1: 5 }, - }, - cometBalances: { - albert: { $asset1: 1 } - } + filter: async (ctx) => await hasWstETH(ctx) && await isBulkerSupported(ctx) && matchesDeployment(ctx, [{ network: 'mainnet' }]), + supplyCaps: async (ctx) => ( + { + [`$asset${await getWstETHIndex(ctx)}`]: 2, + } + ), + tokenBalances: async (ctx) => ( + { + albert: { [`$asset${await getWstETHIndex(ctx)}`]: 2 }, + $comet: { [`$asset${await getWstETHIndex(ctx)}`]: 5 }, + } + ), + cometBalances: async (ctx) => ( + { + albert: { [`$asset${await getWstETHIndex(ctx)}`]: 1 } + } + ) }, async ({ comet, actors, bulker }, context) => { const { albert } = actors; - const stETH = await context.world.deploymentManager.getContractOrThrow('stETH'); - const wstETH = await context.world.deploymentManager.getContractOrThrow('wstETH'); + const stETH = await context.world.deploymentManager.hre.ethers.getContractAt('ERC20', MAINNET_STETH_ADDRESS) as ERC20; + const wstETH = await context.world.deploymentManager.hre.ethers.getContractAt('IWstETH', MAINNET_WSTETH_ADDRESS) as IWstETH; await albert.allow(bulker.address, true); @@ -105,23 +134,29 @@ scenario( scenario( 'MainnetBulker > withdraw max stETH leaves no dust', { - filter: async (ctx) => await isBulkerSupported(ctx) && matchesDeployment(ctx, [{network: 'mainnet', deployment: 'weth'}]), - supplyCaps: { - $asset1: 2, - }, - tokenBalances: { - albert: { $asset1: 2 }, - $comet: { $asset1: 5 }, - }, - cometBalances: { - albert: { $asset1: 1 } - } + filter: async (ctx) => await hasWstETH(ctx) && await isBulkerSupported(ctx) && matchesDeployment(ctx, [{ network: 'mainnet' }]), + supplyCaps: async (ctx) => ( + { + [`$asset${await getWstETHIndex(ctx)}`]: 2, + } + ), + tokenBalances: async (ctx) => ( + { + albert: { [`$asset${await getWstETHIndex(ctx)}`]: 2 }, + $comet: { [`$asset${await getWstETHIndex(ctx)}`]: 5 }, + } + ), + cometBalances: async (ctx) => ( + { + albert: { [`$asset${await getWstETHIndex(ctx)}`]: 1 } + } + ) }, async ({ comet, actors, bulker }, context) => { const { albert } = actors; - const stETH = await context.world.deploymentManager.contract('stETH') as ERC20; - const wstETH = await context.world.deploymentManager.contract('wstETH') as IWstETH; + const stETH = await context.world.deploymentManager.hre.ethers.getContractAt('ERC20', MAINNET_STETH_ADDRESS) as ERC20; + const wstETH = await context.world.deploymentManager.hre.ethers.getContractAt('IWstETH', MAINNET_WSTETH_ADDRESS) as IWstETH; await albert.allow(bulker.address, true); @@ -147,7 +182,7 @@ scenario( scenario( 'MainnetBulker > it reverts when passed an action that does not exist', { - filter: async (ctx) => await isBulkerSupported(ctx) && matchesDeployment(ctx, [{network: 'mainnet', deployment: 'weth'}]), + filter: async (ctx) => await hasWstETH(ctx) && await isBulkerSupported(ctx) && matchesDeployment(ctx, [{ network: 'mainnet' }]), }, async ({ comet, actors }) => { const { betty } = actors;