diff --git a/.github/workflows/enact-migration.yaml b/.github/workflows/enact-migration.yaml index f3a5e4524..67fc6ecad 100644 --- a/.github/workflows/enact-migration.yaml +++ b/.github/workflows/enact-migration.yaml @@ -40,6 +40,11 @@ on: description: Impersonate Account required: false default: '' + with_deploy: + type: boolean + description: Deploy Market + required: false + default: false jobs: enact-migration: name: Enact Migration @@ -108,6 +113,17 @@ jobs: path: deployments/${{ github.event.inputs.network }}/${{ github.event.inputs.deployment }}/artifacts/ if: github.event.inputs.run_id != '' + - name: Run Deploy Market and Enact Migration (impersonate) + run: | + yarn hardhat deploy_and_migrate --network ${{ github.event.inputs.network }} --deployment ${{ github.event.inputs.deployment }} --enact --overwrite ${{ fromJSON('["", "--simulate"]')[github.event.inputs.simulate == 'true'] }} ${{ fromJSON('["", "--no-enacted"]')[github.event.inputs.no_enacted == 'true'] }} ${{ github.event.inputs.migration }} --impersonate ${{ github.event.inputs.impersonateAccount }} + env: + DEBUG: true + ETH_PK: "${{ inputs.eth_pk }}" + NETWORK_PROVIDER: ${{ fromJSON('["", "http://localhost:8585"]')[github.event.inputs.eth_pk == ''] }} + GOV_NETWORK_PROVIDER: ${{ fromJSON('["", "http://localhost:8685"]')[github.event.inputs.eth_pk == '' && env.GOV_NETWORK != ''] }} + GOV_NETWORK: ${{ env.GOV_NETWORK }} + REMOTE_ACCOUNTS: ${{ fromJSON('["", "true"]')[github.event.inputs.eth_pk == ''] }} + if: github.event.inputs.impersonateAccount != '' && github.event.inputs.with_deploy == 'true' - name: Run Enact Migration run: | yarn hardhat migrate --network ${{ github.event.inputs.network }} --deployment ${{ github.event.inputs.deployment }} --enact --overwrite ${{ fromJSON('["", "--simulate"]')[github.event.inputs.simulate == 'true'] }} ${{ fromJSON('["", "--no-enacted"]')[github.event.inputs.no_enacted == 'true'] }} ${{ github.event.inputs.migration }} @@ -118,7 +134,7 @@ jobs: GOV_NETWORK_PROVIDER: ${{ fromJSON('["", "http://localhost:8685"]')[github.event.inputs.eth_pk == '' && env.GOV_NETWORK != ''] }} GOV_NETWORK: ${{ env.GOV_NETWORK }} REMOTE_ACCOUNTS: ${{ fromJSON('["", "true"]')[github.event.inputs.eth_pk == ''] }} - if: github.event.inputs.impersonateAccount == '' + if: github.event.inputs.impersonateAccount == '' && github.event.inputs.with_deploy == 'false' - name: Run Enact Migration (impersonate) run: | yarn hardhat migrate --network ${{ github.event.inputs.network }} --deployment ${{ github.event.inputs.deployment }} --enact --overwrite ${{ fromJSON('["", "--simulate"]')[github.event.inputs.simulate == 'true'] }} ${{ fromJSON('["", "--no-enacted"]')[github.event.inputs.no_enacted == 'true'] }} ${{ github.event.inputs.migration }} --impersonate ${{ github.event.inputs.impersonateAccount }} @@ -129,7 +145,7 @@ jobs: GOV_NETWORK_PROVIDER: ${{ fromJSON('["", "http://localhost:8685"]')[github.event.inputs.eth_pk == '' && env.GOV_NETWORK != ''] }} GOV_NETWORK: ${{ env.GOV_NETWORK }} REMOTE_ACCOUNTS: ${{ fromJSON('["", "true"]')[github.event.inputs.eth_pk == ''] }} - if: github.event.inputs.impersonateAccount != '' + if: github.event.inputs.impersonateAccount != '' && github.event.inputs.with_deploy == 'false' - name: Commit changes if: ${{ github.event.inputs.simulate == 'false' }} run: | diff --git a/.github/workflows/run-scenarios.yaml b/.github/workflows/run-scenarios.yaml index 0fecdd98c..12853640f 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, 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-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, 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-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/bulkers/MainnetBulkerWithWstETHSupport.sol b/contracts/bulkers/MainnetBulkerWithWstETHSupport.sol new file mode 100644 index 000000000..a05016d91 --- /dev/null +++ b/contracts/bulkers/MainnetBulkerWithWstETHSupport.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.15; + +import "./BaseBulker.sol"; +import "../IWstETH.sol"; + +/** + * @title Compound's Bulker contract for Ethereum mainnet + * @notice Executes multiple Comet-related actions in a single transaction + * @author Compound + */ +contract MainnetBulkerWithWstETHSupport is BaseBulker { + /** General configuration constants **/ + + /// @notice The address of Lido staked ETH + address public immutable steth; + + /// @notice The address of Lido wrapped staked ETH + address public immutable wsteth; + + /** Actions **/ + + /// @notice The action for supplying staked ETH to Comet + bytes32 public constant ACTION_SUPPLY_STETH = "ACTION_SUPPLY_STETH"; + + /// @notice The action for withdrawing staked ETH from Comet + bytes32 public constant ACTION_WITHDRAW_STETH = "ACTION_WITHDRAW_STETH"; + + /** Custom errors **/ + + error UnsupportedBaseAsset(); + + /** + * @notice Construct a new MainnetBulker instance + * @param admin_ The admin of the Bulker contract + * @param weth_ The address of wrapped ETH + * @param wsteth_ The address of Lido wrapped staked ETH + **/ + constructor( + address admin_, + address payable weth_, + address wsteth_ + ) BaseBulker(admin_, weth_) { + wsteth = wsteth_; + steth = IWstETH(wsteth_).stETH(); + } + + /** + * @notice Handles actions specific to the Ethereum mainnet version of Bulker, specifically supplying and withdrawing stETH + */ + function handleAction(bytes32 action, bytes calldata data) override internal { + if (action == ACTION_SUPPLY_STETH) { + (address comet, address to, uint stETHAmount) = abi.decode(data, (address, address, uint)); + supplyStEthTo(comet, to, stETHAmount); + } else if (action == ACTION_WITHDRAW_STETH) { + (address comet, address to, uint wstETHAmount) = abi.decode(data, (address, address, uint)); + withdrawStEthTo(comet, to, wstETHAmount); + } else { + revert UnhandledAction(); + } + } + + /** + * @notice Wraps stETH to wstETH and supplies to a user in Comet + * @dev Note: This contract must have permission to manage msg.sender's Comet account + * @dev Note: Supports `stETHAmount` of `uint256.max` to fully repay the wstETH debt + * @dev Note: Only for the cwstETHv3 market + */ + function supplyStEthTo(address comet, address to, uint stETHAmount) internal { + if(CometInterface(comet).baseToken() != wsteth) revert UnsupportedBaseAsset(); + uint256 _stETHAmount = stETHAmount == type(uint256).max + ? IWstETH(wsteth).getStETHByWstETH(CometInterface(comet).borrowBalanceOf(msg.sender)) + : stETHAmount; + doTransferIn(steth, msg.sender, _stETHAmount); + ERC20(steth).approve(wsteth, _stETHAmount); + uint wstETHAmount = IWstETH(wsteth).wrap(_stETHAmount); + ERC20(wsteth).approve(comet, wstETHAmount); + CometInterface(comet).supplyFrom(address(this), to, wsteth, wstETHAmount); + } + + /** + * @notice Withdraws wstETH from Comet, unwraps it to stETH, and transfers it to a user + * @dev Note: This contract must have permission to manage msg.sender's Comet account + * @dev Note: Supports `amount` of `uint256.max` to withdraw all wstETH from Comet + * @dev Note: Only for the cwstETHv3 market + */ + function withdrawStEthTo(address comet, address to, uint stETHAmount) internal { + if(CometInterface(comet).baseToken() != wsteth) revert UnsupportedBaseAsset(); + uint wstETHAmount = stETHAmount == type(uint256).max + ? CometInterface(comet).balanceOf(msg.sender) + : IWstETH(wsteth).getWstETHByStETH(stETHAmount); + CometInterface(comet).withdrawFrom(msg.sender, address(this), wsteth, wstETHAmount); + uint unwrappedStETHAmount = IWstETH(wsteth).unwrap(wstETHAmount); + doTransferOut(steth, to, unwrappedStETHAmount); + } + + /** + * @notice Submits received ether to get stETH and wraps it to wstETH, received wstETH is transferred to Comet + */ + function deposit(address comet) external payable { + if(msg.sender != admin) revert Unauthorized(); + if(CometInterface(comet).baseToken() != wsteth) revert UnsupportedBaseAsset(); + (bool success, ) = payable(wsteth).call{value: msg.value}(new bytes(0)); + if(!success) revert TransferOutFailed(); + + uint wstETHAmount = ERC20(wsteth).balanceOf(address(this)); + doTransferOut(wsteth, comet, wstETHAmount); + } +} \ No newline at end of file diff --git a/deployments/mainnet/wsteth/configuration.json b/deployments/mainnet/wsteth/configuration.json new file mode 100644 index 000000000..0bd7b4d37 --- /dev/null +++ b/deployments/mainnet/wsteth/configuration.json @@ -0,0 +1,46 @@ +{ + "name": "Compound wstETH", + "symbol": "cWstETHv3", + "baseToken": "wstETH", + "baseTokenAddress": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", + "borrowMin": "0.1e18", + "governor": "0x6d903f6003cca6255d85cca4d3b5e5146dc33925", + "pauseGuardian": "0xbbf3f1421d886e9b2c5d716b5192ac998af2012c", + "storeFrontPriceFactor": 0.7, + "targetReserves": "5000e18", + "rates": { + "supplyBase": 0, + "supplySlopeLow": 0.012, + "supplyKink": 0.85, + "supplySlopeHigh": 1, + "borrowBase": 0.01, + "borrowSlopeLow": 0.014, + "borrowKink": 0.85, + "borrowSlopeHigh": 1.15 + }, + "tracking": { + "indexScale": "1e15", + "baseSupplySpeed": "92592592592e0", + "baseBorrowSpeed": "46296296296e0", + "baseMinForRewards": "10e18" + }, + "rewardTokenAddress": "0xc00e94cb662c3520282e6f5717214004a7f26888", + "assets": { + "rsETH": { + "address": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7", + "decimals": "18", + "borrowCF": 0.88, + "liquidateCF": 0.91, + "liquidationFactor": 0.96, + "supplyCap": "10_000e18" + }, + "ezETH": { + "address": "0xbf5495Efe5DB9ce00f80364C8B423567e58d2110", + "decimals": "18", + "borrowCF": 0.88, + "liquidateCF": 0.91, + "liquidationFactor": 0.94, + "supplyCap": "15_000e18" + } + } +} diff --git a/deployments/mainnet/wsteth/deploy.ts b/deployments/mainnet/wsteth/deploy.ts new file mode 100644 index 000000000..dfb9e1977 --- /dev/null +++ b/deployments/mainnet/wsteth/deploy.ts @@ -0,0 +1,77 @@ +import { Deployed, DeploymentManager } from '../../../plugins/deployment_manager'; +import { DeploySpec, deployComet, exp } from '../../../src/deploy'; + +export default async function deploy(deploymentManager: DeploymentManager, deploySpec: DeploySpec): Promise { + const wstETH = await deploymentManager.existing('wstETH', '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0'); + const weth = await deploymentManager.existing('weth', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); + const rsETHToETHPriceFeed = await deploymentManager.fromDep('rsETH:priceFeed', 'mainnet', 'weth'); + const wstETHToETHPriceFeed = await deploymentManager.fromDep('wstETH:priceFeed', 'mainnet', 'weth'); + const ezETHToETHPriceFeed = await deploymentManager.fromDep('ezETH:priceFeed', 'mainnet', 'weth'); + const weETHToETHPriceFeed = await deploymentManager.fromDep('weETH:priceFeed', 'mainnet', 'weth'); + + // Deploy constant price feed for wstETH + const wstETHConstantPriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'pricefeeds/ConstantPriceFeed.sol', + [ + 8, // decimals + exp(1, 8) // constantPrice + ], + true + ); + + // Deploy reverse multiplicative price feed for rsETH + const rsETHScalingPriceFeed = await deploymentManager.deploy( + 'rsETH:priceFeed', + 'pricefeeds/ReverseMultiplicativePriceFeed.sol', + [ + rsETHToETHPriceFeed.address, // rsETH / ETH price feed + wstETHToETHPriceFeed.address, // wstETH / ETH price feed (reversed) + 8, // decimals + 'rsETH / wstETH price feed' // description + ], + true + ); + + // Deploy reverse multiplicative price feed for ezETH + const ezETHScalingPriceFeed = await deploymentManager.deploy( + 'ezETH:priceFeed', + 'pricefeeds/ReverseMultiplicativePriceFeed.sol', + [ + ezETHToETHPriceFeed.address, // ezETH / ETH price feed + wstETHToETHPriceFeed.address, // wstETH / ETH price feed (reversed) + 8, // decimals + 'ezETH / wstETH price feed' // description + ], + true + ); + + // Import shared contracts from cUSDCv3 + const cometAdmin = await deploymentManager.fromDep('cometAdmin', 'mainnet', 'usdc'); + const cometFactory = await deploymentManager.fromDep('cometFactory', 'mainnet', 'usdt'); + 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'); + + // Deploy all Comet-related contracts + const deployed = await deployComet(deploymentManager, deploySpec); + const { comet } = deployed; + + // Deploy Bulker + const bulker = await deploymentManager.deploy( + 'bulker', + 'bulkers/MainnetBulkerWithWstETHSupport.sol', + [ + await comet.governor(), // admin_ + weth.address, // weth_ + wstETH.address // wsteth_ + ], + true + ); + console.log('Bulker deployed at:', bulker.address); + + const bulkerNow = await deploymentManager.contract('bulker'); + console.log('Bulker now at:', bulkerNow? bulkerNow.address: 'N/A'); + + return { ...deployed, bulker }; +} diff --git a/deployments/mainnet/wsteth/migrations/1723732097_configurate_and_ens.ts b/deployments/mainnet/wsteth/migrations/1723732097_configurate_and_ens.ts new file mode 100644 index 000000000..6d628e27c --- /dev/null +++ b/deployments/mainnet/wsteth/migrations/1723732097_configurate_and_ens.ts @@ -0,0 +1,237 @@ +import { ethers } 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 wstETHAmount = ethers.BigNumber.from(exp(20, 18)); + +export default migration('1723732097_configurate_and_ens', { + async prepare() { + return {}; + }, + + async enact(deploymentManager: DeploymentManager) { + const trace = deploymentManager.tracer(); + const cometFactory = await deploymentManager.fromDep('cometFactory', 'mainnet', 'usdt', true); + const ethToWstETHPriceFeed = await deploymentManager.fromDep('wstETH:priceFeed', 'mainnet', 'weth', true); + const price = (await ethToWstETHPriceFeed.latestRoundData())[1]; + const etherToWstETH = ethers.BigNumber.from(wstETHAmount).mul(price).div(exp(1,8)).toBigInt(); + + const { + comet, + cometAdmin, + configurator, + rewards, + COMP, + governor, + bulker, + } = await deploymentManager.getContracts(); + + const configuration = await getConfigurationStruct(deploymentManager); + + const ENSResolver = await deploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const currentChainId = 1; + const newMarketObject = { baseSymbol: 'wstETH', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse(await ENSResolver.text(subdomainHash, ENSTextRecordKey)); + + if (officialMarketsJSON[currentChainId]) { + officialMarketsJSON[currentChainId].push(newMarketObject); + } else { + officialMarketsJSON[currentChainId] = [newMarketObject]; + } + + 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. Deposit ether to get wstETH and transfer it to the Comet + { + target: bulker.address, + value: etherToWstETH, + signature: 'deposit(address)', + calldata: ethers.utils.defaultAbiCoder.encode( + ['address'], + [comet.address] + ), + }, + // 6. 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 cwstETHv3 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 wstETH market on Mainnet; upon execution, cwstETHv3 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-wsteth-market-on-mainnet/5504/4).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/911), [deploy market GitHub action run](https://github.com/woof-software/comet/actions/runs/10717773287) and [forum discussion](https://www.comp.xyz/t/add-wsteth-market-on-mainnet/5504).\n\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 converts ether to wstETH and transfers it to the Comet to seed the reserves.\n\nThe sixth 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 cwstETHv3 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, + COMP, + rsETH, + ezETH + } = await deploymentManager.getContracts(); + + // 1. & 2. & 3. + const rsETHInfo = await comet.getAssetInfoByAddress(rsETH.address); + const ezETHInfo = await comet.getAssetInfoByAddress(ezETH.address); + + expect(rsETHInfo.supplyCap).to.be.eq(exp(10_000, 18)); + expect(ezETHInfo.supplyCap).to.be.eq(exp(15_000, 18)); + + expect(await comet.baseTrackingSupplySpeed()).to.be.equal(exp(8 / 86400, 15, 18)); // 92592592592 + expect(await comet.baseTrackingBorrowSpeed()).to.be.equal(exp(4 / 86400, 15, 18)); // 46296296296 + + // 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 reserves to be close to wstETHAmount +- 0.1 + expect(await comet.getReserves()).to.be.closeTo(wstETHAmount, exp(1, 17)); + + // 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); + const officialMarkets = JSON.parse(officialMarketsJSON); + 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); + expect(officialMarkets).to.deep.equal({ + 1: [ + { + baseSymbol: 'USDC', + cometAddress: '0xc3d688B66703497DAA19211EEdff47f25384cdc3', + }, + { + baseSymbol: 'WETH', + cometAddress: '0xA17581A9E3356d9A858b789D68B4d866e593aE94', + }, + { + baseSymbol: 'USDT', + cometAddress: '0x3Afdc9BCA9213A35503b077a6072F3D0d5AB0840' + }, + { + baseSymbol: 'wstETH', + 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', + }, + ], + 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/wsteth/migrations/1727727546_change_base_price_feed.ts b/deployments/mainnet/wsteth/migrations/1727727546_change_base_price_feed.ts new file mode 100644 index 000000000..4cfa4d224 --- /dev/null +++ b/deployments/mainnet/wsteth/migrations/1727727546_change_base_price_feed.ts @@ -0,0 +1,73 @@ +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'; +const constantPriceFeedAddress = '0x72e9B6F907365d76C6192aD49C0C5ba356b7Fa48'; + +export default migration('1727727546_change_base_price_feed', { + async prepare() { + return {}; + }, + + enact: async (deploymentManager: DeploymentManager) => { + const trace = deploymentManager.tracer(); + const { + governor, + comet, + cometAdmin, + configurator + } = await deploymentManager.getContracts(); + + + const mainnetActions = [ + // 1. Add weETH as asset + { + contract: configurator, + signature: 'setBaseTokenPriceFeed(address,address)', + args: [comet.address, constantPriceFeedAddress], + }, + // 2. Deploy and upgrade to a new version of Comet + { + contract: cometAdmin, + signature: 'deployAndUpgradeTo(address,address)', + args: [configurator.address, comet.address], + }, + ]; + + const description = '# Update price feed in cWstETHv3 on Ethereum\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes to update wstETH price feed in cWstETHv3 market on Ethereum to constant price feed as it is intended to be.\n\n\n## Proposal Actions\n\nThe first proposal action updates price feed for wstETH.\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 false; + }, + + async verify(deploymentManager: DeploymentManager) { + const { comet, configurator } = await deploymentManager.getContracts(); + + // 1. Compare proposed asset config with Comet asset info + const basePriceFeed = await comet.baseTokenPriceFeed(); + + expect(basePriceFeed).to.eq(constantPriceFeedAddress); + expect(await comet.getPrice(basePriceFeed)).to.eq(exp(1, 8)); + + // 2. Compare proposed asset config with Configurator asset config + const basePriceFeedFromConfigurator = ( + await configurator.getConfiguration(comet.address) + ).baseTokenPriceFeed; + + expect(basePriceFeedFromConfigurator).to.eq(constantPriceFeedAddress); + }, +}); diff --git a/deployments/mainnet/wsteth/relations.ts b/deployments/mainnet/wsteth/relations.ts new file mode 100644 index 000000000..4211ead18 --- /dev/null +++ b/deployments/mainnet/wsteth/relations.ts @@ -0,0 +1,34 @@ +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', + }, + 'UUPSProxy': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, + 'TransparentUpgradeableProxy': { + artifact: 'contracts/ERC20.sol:ERC20', + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, +}; + diff --git a/deployments/mainnet/wsteth/roots.json b/deployments/mainnet/wsteth/roots.json new file mode 100644 index 000000000..6d3a235ec --- /dev/null +++ b/deployments/mainnet/wsteth/roots.json @@ -0,0 +1,6 @@ +{ + "comet": "0x3D0bb1ccaB520A66e607822fC55BC921738fAFE3", + "configurator": "0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3", + "rewards": "0x1B0e765F6224C21223AeA2af16c1C46E38885a40", + "bulker": "0x2c776041CCFe903071AF44aa147368a9c8EEA518" +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index c532b5ef2..d15407876 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -26,6 +26,7 @@ import mumbaiRelationConfigMap from './deployments/mumbai/usdc/relations'; 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 polygonRelationConfigMap from './deployments/polygon/usdc/relations'; import polygonUsdtRelationConfigMap from './deployments/polygon/usdt/relations'; import arbitrumBridgedUsdcRelationConfigMap from './deployments/arbitrum/usdc.e/relations'; @@ -353,7 +354,8 @@ const config: HardhatUserConfig = { mainnet: { usdc: mainnetRelationConfigMap, weth: mainnetWethRelationConfigMap, - usdt: mainnetUsdtRelationConfigMap + usdt: mainnetUsdtRelationConfigMap, + wsteth: mainnetWstETHRelationConfigMap }, polygon: { usdc: polygonRelationConfigMap, @@ -413,6 +415,11 @@ const config: HardhatUserConfig = { network: 'mainnet', deployment: 'usdt' }, + { + name: 'mainnet-wsteth', + network: 'mainnet', + deployment: 'wsteth' + }, { name: 'development', network: 'hardhat', diff --git a/plugins/deployment_manager/DeploymentManager.ts b/plugins/deployment_manager/DeploymentManager.ts index f15071978..4c53658b0 100644 --- a/plugins/deployment_manager/DeploymentManager.ts +++ b/plugins/deployment_manager/DeploymentManager.ts @@ -222,10 +222,11 @@ export class DeploymentManager { alias: Alias, network: string, deployment: string, + force?: boolean, otherAlias = alias ): Promise { const maybeExisting = await this.contract(alias); - if (!maybeExisting) { + if (!maybeExisting || force) { const trace = this.tracer(); const spider = await this.spiderOther(network, deployment); const contract = spider.contracts.get(otherAlias) as C; diff --git a/scenario/BulkerScenario.ts b/scenario/BulkerScenario.ts index 1f4ed579f..e2d55889d 100644 --- a/scenario/BulkerScenario.ts +++ b/scenario/BulkerScenario.ts @@ -1,14 +1,28 @@ -import { scenario } from './context/CometContext'; +import { CometContext, scenario } from './context/CometContext'; import { constants, utils } from 'ethers'; import { expect } from 'chai'; import { expectBase, isRewardSupported, isBulkerSupported, getExpectedBaseBalance, matchesDeployment } from './utils'; import { exp } from '../test/helpers'; +async function hasWETHAsCollateralOrBase(ctx: CometContext): Promise { + const comet = await ctx.getComet(); + const bulker = await ctx.getBulker(); + const wrappedNativeToken = await bulker.wrappedNativeToken(); + if((await comet.baseToken()).toLowerCase() === wrappedNativeToken.toLowerCase()) return true; + const numAssets = await comet.numAssets(); + for (let i = 0; i < numAssets; i++) { + const { asset } = await comet.getAssetInfo(i); + if (asset.toLowerCase() === wrappedNativeToken.toLowerCase()) { + return true; + } + } +} + // XXX properly handle cases where asset0 is WETH scenario( 'Comet#bulker > (non-WETH base) all non-reward actions in one txn', { - filter: async (ctx) => await isBulkerSupported(ctx) && !matchesDeployment(ctx, [{ deployment: 'weth' }, { network: 'mumbai' }, { network: 'linea-goerli' }]), + filter: async (ctx) => await isBulkerSupported(ctx) && !matchesDeployment(ctx, [{ deployment: 'weth' }, { deployment: 'wsteth' }, { network: 'mumbai' }, { network: 'linea-goerli' }]), supplyCaps: { $asset0: 5000, $asset1: 5000, @@ -86,6 +100,91 @@ scenario( } ); +scenario( + 'Comet#bulker > (wstETH base) all non-reward actions in one txn', + { + filter: async (ctx) => await isBulkerSupported(ctx) && !matchesDeployment(ctx, [{ deployment: 'weth' }, { network: 'mumbai' }, { network: 'linea-goerli' }]), + supplyCaps: { + $asset0: 5000, + $asset1: 5000, + }, + tokenBalances: { + albert: { $base: '== 0', $asset0: 5000, $asset1: 5000 }, + $comet: { $base: 5000 }, + }, + }, + async ({ comet, actors, bulker }, context) => { + const { albert, betty } = actors; + const wrappedNativeToken = await bulker.wrappedNativeToken(); + const baseAssetAddress = await comet.baseToken(); + const baseAsset = context.getAssetByAddress(baseAssetAddress); + const baseScale = (await comet.baseScale()).toBigInt(); + // if asset 0 is native token we took asset 1 + const { asset: asset0, scale: scale0 } = await comet.getAssetInfo(0); + const { asset: asset1, scale: scale1 } = await comet.getAssetInfo(1); + const { asset: collateralAssetAddress, scale: scaleBN } = asset0 === wrappedNativeToken ? { asset: asset1, scale: scale1 } : { asset: asset0, scale: scale0 }; + const collateralAsset = context.getAssetByAddress(collateralAssetAddress); + const collateralScale = scaleBN.toBigInt(); + const toSupplyCollateral = 5000n * collateralScale; + const toBorrowBase = 1000n * baseScale; + const toTransferBase = 500n * baseScale; + const toSupplyEth = exp(0.01, 18); + const toWithdrawEth = exp(0.005, 18); + + // Approvals + await collateralAsset.approve(albert, comet.address); + await albert.allow(bulker.address, true); + + // Initial expectations + expect(await collateralAsset.balanceOf(albert.address)).to.be.equal(toSupplyCollateral); + expect(await baseAsset.balanceOf(albert.address)).to.be.equal(0n); + expect(await comet.balanceOf(albert.address)).to.be.equal(0n); + + // Albert's actions: + // 1. Supplies 3000 units of collateral + // 2. Borrows 1000 base + // 3. Transfers 500 base to Betty + // 4. Supplies 0.01 ETH + // 5. Withdraws 0.005 ETH + const supplyAssetCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'address', 'uint'], [comet.address, albert.address, collateralAsset.address, toSupplyCollateral]); + const withdrawAssetCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'address', 'uint'], [comet.address, albert.address, baseAsset.address, toBorrowBase]); + const transferAssetCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'address', 'uint'], [comet.address, betty.address, baseAsset.address, toTransferBase]); + const supplyEthCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, albert.address, toSupplyEth]); + const withdrawEthCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, albert.address, toWithdrawEth]); + const calldata = [ + supplyAssetCalldata, + withdrawAssetCalldata, + transferAssetCalldata + ]; + const actions = [ + await bulker.ACTION_SUPPLY_ASSET(), + await bulker.ACTION_WITHDRAW_ASSET(), + await bulker.ACTION_TRANSFER_ASSET() + ]; + + if(await hasWETHAsCollateralOrBase(context)){ + calldata.push(supplyEthCalldata); + calldata.push(withdrawEthCalldata); + actions.push(await bulker.ACTION_SUPPLY_NATIVE_TOKEN()); + actions.push(await bulker.ACTION_WITHDRAW_NATIVE_TOKEN()); + } + + const txn = await albert.invoke({ actions, calldata }, { value: toSupplyEth }); + + // Final expectations + const baseIndexScale = (await comet.baseIndexScale()).toBigInt(); + const baseSupplyIndex = (await comet.totalsBasic()).baseSupplyIndex.toBigInt(); + const baseTransferred = getExpectedBaseBalance(toTransferBase, baseIndexScale, baseSupplyIndex); + expect(await comet.collateralBalanceOf(albert.address, collateralAsset.address)).to.be.equal(toSupplyCollateral); + if(await hasWETHAsCollateralOrBase(context)) expect(await comet.collateralBalanceOf(albert.address, wrappedNativeToken)).to.be.equal(toSupplyEth - toWithdrawEth); + expect(await baseAsset.balanceOf(albert.address)).to.be.equal(toBorrowBase); + expectBase((await comet.balanceOf(betty.address)).toBigInt(), baseTransferred); + expectBase((await comet.borrowBalanceOf(albert.address)).toBigInt(), toBorrowBase + toTransferBase); + + return txn; // return txn to measure gas + } +); + scenario( 'Comet#bulker > (WETH base) all non-reward actions in one txn', { @@ -165,7 +264,7 @@ scenario( scenario( 'Comet#bulker > (non-WETH base) all actions in one txn', { - filter: async (ctx) => await isBulkerSupported(ctx) && await isRewardSupported(ctx) && !matchesDeployment(ctx, [{ deployment: 'weth' }, { network: 'linea-goerli' }]), + filter: async (ctx) => await isBulkerSupported(ctx) && await isRewardSupported(ctx) && !matchesDeployment(ctx, [{ deployment: 'weth' }, { deployment: 'wsteth' }, { network: 'linea-goerli' }]), supplyCaps: { $asset0: 5000, $asset1: 5000, @@ -261,6 +360,110 @@ scenario( } ); + +scenario( + 'Comet#bulker > (wstETH base) all actions in one txn', + { + filter: async (ctx) => await isBulkerSupported(ctx) && await isRewardSupported(ctx) && !matchesDeployment(ctx, [{ deployment: 'weth' }, { network: 'linea-goerli' }]), + supplyCaps: { + $asset0: 5000, + $asset1: 5000, + }, + tokenBalances: { + albert: { $base: '== 1000000', $asset0: 5000, $asset1: 5000 }, + $comet: { $base: 5000 }, + } + }, + async ({ comet, actors, rewards, bulker }, context, world) => { + const { albert, betty } = actors; + const wrappedNativeToken = await bulker.wrappedNativeToken(); + const baseAssetAddress = await comet.baseToken(); + const baseAsset = context.getAssetByAddress(baseAssetAddress); + const baseScale = (await comet.baseScale()).toBigInt(); + // if asset 0 is native token we took asset 1 + const { asset: asset0, scale: scale0 } = await comet.getAssetInfo(0); + const { asset: asset1, scale: scale1 } = await comet.getAssetInfo(1); + const { asset: collateralAssetAddress, scale: scaleBN } = asset0 === wrappedNativeToken ? { asset: asset1, scale: scale1 } : { asset: asset0, scale: scale0 }; + const collateralAsset = context.getAssetByAddress(collateralAssetAddress); + const collateralScale = scaleBN.toBigInt(); + const [rewardTokenAddress] = await rewards.rewardConfig(comet.address); + const toSupplyBase = 1_000_000n * baseScale; + const toSupplyCollateral = 5000n * collateralScale; + const toBorrowBase = 1000n * baseScale; + const toTransferBase = 500n * baseScale; + const toSupplyEth = exp(0.01, 18); + const toWithdrawEth = exp(0.005, 18); + + // Approvals + await baseAsset.approve(albert, comet.address); + await collateralAsset.approve(albert, comet.address); + await albert.allow(bulker.address, true); + + // Accrue some rewards to Albert, then transfer away Albert's supplied base + await albert.safeSupplyAsset({ asset: baseAssetAddress, amount: toSupplyBase }); + await world.increaseTime(86400); // fast forward a day + await albert.transferAsset({ dst: constants.AddressZero, asset: baseAssetAddress, amount: constants.MaxUint256 }); // transfer all base away + + // Initial expectations + expect(await collateralAsset.balanceOf(albert.address)).to.be.equal(toSupplyCollateral); + expect(await baseAsset.balanceOf(albert.address)).to.be.equal(0n); + expect(await comet.balanceOf(albert.address)).to.be.equal(0n); + const startingRewardBalance = await albert.getErc20Balance(rewardTokenAddress); + const rewardOwed = ((await rewards.callStatic.getRewardOwed(comet.address, albert.address)).owed).toBigInt(); + const expectedFinalRewardBalance = collateralAssetAddress === rewardTokenAddress ? + startingRewardBalance + rewardOwed - toSupplyCollateral : + startingRewardBalance + rewardOwed; + + // Albert's actions: + // 1. Supplies 3000 units of collateral + // 2. Borrows 1000 base + // 3. Transfers 500 base to Betty + // 4. Supplies 0.01 ETH + // 5. Withdraws 0.005 ETH + // 6. Claim rewards + const supplyAssetCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'address', 'uint'], [comet.address, albert.address, collateralAsset.address, toSupplyCollateral]); + const withdrawAssetCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'address', 'uint'], [comet.address, albert.address, baseAsset.address, toBorrowBase]); + const transferAssetCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'address', 'uint'], [comet.address, betty.address, baseAsset.address, toTransferBase]); + const supplyEthCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, albert.address, toSupplyEth]); + const withdrawEthCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'uint'], [comet.address, albert.address, toWithdrawEth]); + const claimRewardCalldata = utils.defaultAbiCoder.encode(['address', 'address', 'address', 'bool'], [comet.address, rewards.address, albert.address, true]); + const calldata = [ + supplyAssetCalldata, + withdrawAssetCalldata, + transferAssetCalldata, + claimRewardCalldata + ]; + const actions = [ + await bulker.ACTION_SUPPLY_ASSET(), + await bulker.ACTION_WITHDRAW_ASSET(), + await bulker.ACTION_TRANSFER_ASSET(), + await bulker.ACTION_CLAIM_REWARD(), + ]; + + if(await hasWETHAsCollateralOrBase(context)){ + calldata.push(supplyEthCalldata); + calldata.push(withdrawEthCalldata); + actions.push(await bulker.ACTION_SUPPLY_NATIVE_TOKEN()); + actions.push(await bulker.ACTION_WITHDRAW_NATIVE_TOKEN()); + } + + const txn = await albert.invoke({ actions, calldata }, { value: toSupplyEth }); + + // Final expectations + const baseIndexScale = (await comet.baseIndexScale()).toBigInt(); + const baseSupplyIndex = (await comet.totalsBasic()).baseSupplyIndex.toBigInt(); + const baseTransferred = getExpectedBaseBalance(toTransferBase, baseIndexScale, baseSupplyIndex); + expect(await comet.collateralBalanceOf(albert.address, collateralAsset.address)).to.be.equal(toSupplyCollateral); + expect(await baseAsset.balanceOf(albert.address)).to.be.equal(toBorrowBase); + if(await hasWETHAsCollateralOrBase(context)) expect(await comet.collateralBalanceOf(albert.address, wrappedNativeToken)).to.be.equal(toSupplyEth - toWithdrawEth); + expect(await albert.getErc20Balance(rewardTokenAddress)).to.be.equal(expectedFinalRewardBalance); + expectBase((await comet.balanceOf(betty.address)).toBigInt(), baseTransferred); + expectBase((await comet.borrowBalanceOf(albert.address)).toBigInt(), toBorrowBase + toTransferBase); + + return txn; // return txn to measure gas + } +); + scenario( 'Comet#bulker > (WETH base) all actions in one txn', { diff --git a/scenario/LiquidationBotScenario.ts b/scenario/LiquidationBotScenario.ts index bf21963b7..8c504b8e1 100644 --- a/scenario/LiquidationBotScenario.ts +++ b/scenario/LiquidationBotScenario.ts @@ -537,7 +537,7 @@ 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' }]), + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]) && !matchesDeployment(ctx, [{deployment: 'wsteth', network: 'mainnet'}]), tokenBalances: { $comet: { $base: 100000 }, }, @@ -648,7 +648,7 @@ 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' }]), + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }, { network: 'polygon' }, { network: 'arbitrum' }]) && !matchesDeployment(ctx, [{deployment: 'wsteth', network: 'mainnet'}]), tokenBalances: { $comet: { $base: 100000 }, }, @@ -778,7 +778,7 @@ scenario( upgrade: { targetReserves: exp(20_000, 18) }, - filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }]), + filter: async (ctx) => matchesDeployment(ctx, [{ network: 'mainnet' }]) && !matchesDeployment(ctx, [{deployment: 'wsteth'}]), tokenBalances: async (ctx) => ( { $comet: { diff --git a/scenario/constraints/MigrationConstraint.ts b/scenario/constraints/MigrationConstraint.ts index a0c2b16a2..01401138f 100644 --- a/scenario/constraints/MigrationConstraint.ts +++ b/scenario/constraints/MigrationConstraint.ts @@ -1,7 +1,7 @@ import { StaticConstraint, Solution, World, debug } from '../../plugins/scenario'; import { CometContext, MigrationData } from '../context/CometContext'; import { Migration, loadMigrations, Actions } from '../../plugins/deployment_manager/Migration'; -import { modifiedPaths, subsets } from '../utils'; +import { modifiedPaths } from '../utils'; import { DeploymentManager } from '../../plugins/deployment_manager'; import { impersonateAddress } from '../../plugins/scenario/utils'; import { exp } from '../../test/helpers'; @@ -22,7 +22,17 @@ export class MigrationConstraint implements StaticConstr async solve(world: World) { const label = `[${world.base.name}] {MigrationConstraint}`; const solutions: Solution[] = []; - const migrationPaths = [...subsets(await getMigrations(world))]; + const migrations = await getMigrations(world); + + // remove enacted migrations + for(let i = 0; i < migrations.length; i++) { + if (await isEnacted(migrations[i].actions, world.deploymentManager, world.deploymentManager)) { + migrations.splice(i, 1); + i--; + } + } + + const migrationPaths = [migrations]; for (const migrationList of migrationPaths) { if (migrationList.length == 0 && migrationPaths.length > 1) { diff --git a/scenario/utils/index.ts b/scenario/utils/index.ts index 03f59d4d8..3fd02d954 100644 --- a/scenario/utils/index.ts +++ b/scenario/utils/index.ts @@ -328,7 +328,7 @@ export async function fetchLogs( } async function redeployRenzoOracle(dm: DeploymentManager){ - if(dm.network === 'mainnet' && dm.deployment === 'weth') { + if(dm.network === 'mainnet') { // renzo admin 0xD1e6626310fD54Eceb5b9a51dA2eC329D6D4B68A const renzoOracle = new Contract( '0x5a12796f7e7EBbbc8a402667d266d2e65A814042', diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 3a03a4e46..7381208a9 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -88,6 +88,7 @@ export const WHALES = { '0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e', '0x2775b1c75658be0f640272ccb8c72ac986009e38', '0x1a9c8182c09f50c8318d769245bea52c32be35bc', + '0x3c22ec75ea5D745c78fc84762F7F1E6D82a2c5BF', '0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b' ], polygon: [ diff --git a/tasks/deployment_manager/task.ts b/tasks/deployment_manager/task.ts index f68286910..cf69fdc2a 100644 --- a/tasks/deployment_manager/task.ts +++ b/tasks/deployment_manager/task.ts @@ -231,3 +231,124 @@ task('migrate', 'Runs migration') } } ); + +task('deploy_and_migrate', 'Runs deploy and migration') + .addPositionalParam('migration', 'name of migration') + .addOptionalParam('impersonate', 'the governor will impersonate the passed account for proposals [only when simulating]') + .addFlag('simulate', 'only simulates the blockchain effects') + .addFlag('noDeploy', 'skip the actual deploy step') + .addFlag('noVerify', 'do not verify any contracts') + .addFlag('noVerifyImpl', 'do not verify the impl contract') + .addFlag('overwrite', 'overwrites cache') + .addFlag('prepare', 'runs preparation [defaults to true if enact not specified]') + .addFlag('enact', 'enacts migration [implies prepare]') + .addFlag('noEnacted', 'do not write enacted to the migration script') + .addParam('deployment', 'The deployment to deploy') + .setAction( + async ({ migration: migrationName, prepare, enact, noEnacted, simulate, overwrite, deployment, impersonate, noDeploy, noVerify, noVerifyImpl }, env) => { + const maybeForkEnv = simulate ? getForkEnv(env, deployment) : env; + const network = env.network.name; + const tag = `${network}/${deployment}`; + const dm = new DeploymentManager( + network, + deployment, + maybeForkEnv, + { + writeCacheToDisk: !simulate || overwrite, // Don't write to disk when simulating, unless overwrite is set + verificationStrategy: 'lazy', + } + ); + + if (noDeploy) { + // Don't run the deploy script + } else { + try { + const overrides = undefined; // TODO: pass through cli args + const delta = await dm.runDeployScript(overrides ?? { allMissing: true }); + console.log(`[${tag}] Deployed ${dm.counter} contracts, spent ${dm.spent} Ξ`); + console.log(`[${tag}]\n${dm.diffDelta(delta)}`); + } catch (e) { + console.log(`[${tag}] Failed to deploy with error: ${e}`); + } + } + + const verify = noVerify ? false : !simulate; + const desc = verify ? 'Verify' : 'Would verify'; + if (noVerify && simulate) { + // Don't even print if --no-verify is set with --simulate + } else { + await dm.verifyContracts(async (address, args) => { + if (args.via === 'buildfile') { + const { contract: _, ...rest } = args; + console.log(`[${tag}] ${desc} ${address}:`, rest); + } else { + console.log(`[${tag}] ${desc} ${address}:`, args); + } + return verify; + }); + + if (noVerifyImpl) { + // Don't even try if --no-verify-impl + } else { + // Maybe verify the comet impl too + const comet = await dm.contract('comet'); + const cometImpl = await dm.contract('comet:implementation'); + const configurator = await dm.contract('configurator'); + const config = await configurator.getConfiguration(comet.address); + const args: VerifyArgs = { + via: 'artifacts', + address: cometImpl.address, + constructorArguments: [config] + }; + console.log(`[${tag}] ${desc} ${cometImpl.address}:`, args); + if (verify) { + await dm.verifyContract(args); + } + } + } + await dm.spider(); + + let governanceDm: DeploymentManager; + const base = env.config.scenario.bases.find(b => b.network === network && b.deployment === deployment); + const isBridgedDeployment = base.auxiliaryBase !== undefined; + const governanceBase = isBridgedDeployment ? env.config.scenario.bases.find(b => b.name === base.auxiliaryBase) : undefined; + + if (governanceBase) { + const governanceEnv = hreForBase(governanceBase, simulate); + governanceDm = new DeploymentManager( + governanceBase.network, + governanceBase.deployment, + governanceEnv, + { + writeCacheToDisk: !simulate || overwrite, // Don't write to disk when simulating, unless overwrite is set + verificationStrategy: 'eager', // We use eager here to verify contracts right after they are deployed + } + ); + await governanceDm.spider(); + } else { + governanceDm = dm; + } + + if (impersonate && !simulate) { + throw new Error('Cannot impersonate an address if not simulating a migration. Please specify --simulate to simulate.'); + } else if (impersonate && simulate) { + const signer = await impersonateAddress(governanceDm, impersonate, 10n ** 18n); + governanceDm._signers.unshift(signer); + } + + const migrationPath = `${__dirname}/../../deployments/${network}/${deployment}/migrations/${migrationName}.ts`; + const [migration] = await loadMigrations([migrationPath]); + if (!migration) { + throw new Error(`Unknown migration for network ${network}/${deployment}: \`${migrationName}\`.`); + } + if (!prepare && !enact) { + prepare = true; + } + + await runMigration(dm, governanceDm, prepare, enact, migration, overwrite); + + if (enact && !noEnacted) { + await writeEnacted(migration, dm, true); + } + + });