diff --git a/deployments/arbitrum/weth/migrations/1716912328_configurate_and_ens.ts b/deployments/arbitrum/weth/migrations/1716912328_configurate_and_ens.ts new file mode 100644 index 000000000..f2da28f7f --- /dev/null +++ b/deployments/arbitrum/weth/migrations/1716912328_configurate_and_ens.ts @@ -0,0 +1,296 @@ +import { Contract, ethers } from 'ethers'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, getConfigurationStruct, proposal } from '../../../../src/deploy'; +import { expect } from 'chai'; +import { applyL1ToL2Alias, estimateL2Transaction, estimateTokenBridge } from '../../../../scenario/utils/arbitrumUtils'; + +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 WETHAmountToBridge = ethers.BigNumber.from(exp(10, 18)); +const arbitrumCOMPAddress = '0x354A6dA3fcde098F8389cad84b0182725c6C91dE'; + +export default migration('1716912328_configurate_and_ens', { + prepare: async (_deploymentManager: DeploymentManager) => { + return {}; + }, + + enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager) => { + const trace = deploymentManager.tracer(); + const ethers = deploymentManager.hre.ethers; + const { utils } = ethers; + + const cometFactory = await deploymentManager.fromDep('cometFactory', 'arbitrum', 'usdc.e'); + const { + bridgeReceiver, + timelock: l2Timelock, + comet, + cometAdmin, + configurator, + rewards + } = await deploymentManager.getContracts(); + const { + arbitrumInbox, + arbitrumL1GatewayRouter, + timelock, + governor, + WETH, + COMP + } = await govDeploymentManager.getContracts(); + const refundAddress = l2Timelock.address; + const wethGatewayAddress = await arbitrumL1GatewayRouter.getGateway(WETH.address); + + const wethGasParams = await estimateTokenBridge( + { + token: COMP.address, + from: timelock.address, + to: comet.address, + amount: WETHAmountToBridge.toBigInt() + }, + govDeploymentManager, + deploymentManager + ); + + const configuration = await getConfigurationStruct(deploymentManager); + const setFactoryCalldata = await calldata( + configurator.populateTransaction.setFactory(comet.address, cometFactory.address) + ); + const setConfigurationCalldata = await calldata( + configurator.populateTransaction.setConfiguration(comet.address, configuration) + ); + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + + const setRewardConfigCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [comet.address, arbitrumCOMPAddress] + ); + + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, configurator.address, cometAdmin.address, rewards.address], + [0, 0, 0, 0], + [ + 'setFactory(address,address)', + 'setConfiguration(address,(address,address,address,address,address,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint104,uint104,uint104,(address,address,uint8,uint64,uint64,uint64,uint128)[]))', + 'deployAndUpgradeTo(address,address)', + 'setRewardConfig(address,address)' + ], + [ + setFactoryCalldata, + setConfigurationCalldata, + deployAndUpgradeToCalldata, + setRewardConfigCalldata + ] + ] + ); + + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const subdomainHash = ethers.utils.namehash(ENSSubdomain); + const polygonChainId = (await deploymentManager.hre.ethers.provider.getNetwork()).chainId.toString(); + const newMarketObject = { baseSymbol: 'WETH', cometAddress: comet.address }; + const officialMarketsJSON = JSON.parse(await ENSResolver.text(subdomainHash, ENSTextRecordKey)); + + if (officialMarketsJSON[polygonChainId]) { + officialMarketsJSON[polygonChainId].push(newMarketObject); + } else { + officialMarketsJSON[polygonChainId] = [newMarketObject]; + } + + const createRetryableTicketGasParams = await estimateL2Transaction( + { + from: applyL1ToL2Alias(timelock.address), + to: bridgeReceiver.address, + data: l2ProposalData + }, + deploymentManager + ); + + const outboundTransferCalldata = utils.defaultAbiCoder.encode( + ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes'], + [ + WETH.address, + comet.address, + WETHAmountToBridge.toBigInt(), + wethGasParams.gasLimit, + wethGasParams.maxFeePerGas, + utils.defaultAbiCoder.encode( + ['uint256', 'bytes'], + [wethGasParams.maxSubmissionCost, '0x'] + ) + ] + ); + + const mainnetActions = [ + // 1. Set Comet configuration and deployAndUpgradeTo new 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 + }, + // 2. Wrap some ETH as WETH + { + contract: WETH, + signature: 'deposit()', + args: [], + value: WETHAmountToBridge, + }, + // 3. Approve the WETH gateway to take Timelock's WETH for bridging + { + contract: WETH, + signature: 'approve(address,uint256)', + args: [wethGatewayAddress, WETHAmountToBridge] + }, + // 4. Bridge WETH from mainnet to Arbitrum Comet + { + target: arbitrumL1GatewayRouter.address, + signature: 'outboundTransfer(address,address,uint256,uint256,uint256,bytes)', + calldata: outboundTransferCalldata, + value: wethGasParams.deposit + }, + // 5. 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 cWETHv3 on Arbitrum\n\n## Proposal summary\n\nCompound Growth Program [AlphaGrowth] proposes deployment of Compound III to the Arbitrum network. This proposal takes the governance steps recommended and necessary to initialize a Compound III WETH market on Arbitrum; upon execution, cWETHv3 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-eth-market-on-arbitrum/5252/2).\n\nFurther detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/860), [deploy market GitHub action run](https://github.com/woof-software/comet/actions/runs/9419069237/job/25948008428) and [forum discussion](https://www.comp.xyz/t/add-eth-market-on-arbitrum/5252).\n\nThe market migration to create a new WETH Comet market on Arbitrum also includes a new collateral asset weETH which has not been used in any other Compound market previously. In prior analysis of weETH as a collateral asset, Gauntlet identified oracle risks which [could expose the protocol to exaggerated market movements](https://www.comp.xyz/t/add-weeth-market-on-ethereum/5179/3#oracle-risk-7) and a yield risk which could [cause yield shocks and consequentially elevate slippage magnitude and liquidity on DEXs](https://www.comp.xyz/t/add-weeth-market-on-ethereum/5179/3#yield-risk-8).\n\n\n## Proposal Actions\n\nThe first proposal action sets the Comet configuration and deploys a new Comet implementation on Arbitrum. This sends the encoded `setFactory`, `setConfiguration`, `deployAndUpgradeTo` calls across the bridge to the governance receiver on Arbitrum. It also calls `setRewardConfig` on the Arbitrum rewards contract, to establish Artitrum’s bridged version of COMP as the reward token for the deployment and set the initial supply speed to be 6 COMP/day and borrow speed to be 4 COMP/day.\n\nThe second action wraps ETH as WETH so it can be then transferred.\n\nThe third action approves (ArbitrumL1GatewayRouter) [TokenMessenger](https://etherscan.io/address/0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef) to take the Timelock's WETH on Mainnet, in order to seed the market reserves through the arbitrumL1GatewayRouter.\n\nThe fourth action bridges WETH from mainnet via ‘outboundTransfer’ function on ArbitrumL1GatewayRouter’s contract to mint native WETH to Comet on Arbitrum.\n\nThe fifth 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 Arbitrum cWETHv3 market.\n"; + 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(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify(deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager) { + const ethers = deploymentManager.hre.ethers; + + const { + comet, + rewards, + wstETH, + rETH, + weETH + } = await deploymentManager.getContracts(); + + const { + timelock + } = await govDeploymentManager.getContracts(); + + // 1. + const weETHInfo = await comet.getAssetInfoByAddress(weETH.address); + const rETHInfo = await comet.getAssetInfoByAddress(rETH.address); + const wstETHInfo = await comet.getAssetInfoByAddress(wstETH.address); + expect(weETHInfo.supplyCap).to.be.eq(exp(550, 18)); + expect(wstETHInfo.supplyCap).to.be.eq(exp(2000, 18)); + expect(rETHInfo.supplyCap).to.be.eq(exp(800, 18)); + + expect(await comet.pauseGuardian()).to.be.eq('0x78E6317DD6D43DdbDa00Dce32C2CbaFc99361a9d'); + + // 2. & 3. & 4. + expect(await comet.getReserves()).to.be.equal(WETHAmountToBridge); + + // 5. + const ENSResolver = await govDeploymentManager.existing('ENSResolver', ENSResolverAddress); + const ENSRegistry = await govDeploymentManager.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', + }, + ], + 137: [ + { + baseSymbol: 'USDC', + cometAddress: '0xF25212E676D1F7F89Cd72fFEe66158f541246445', + }, + ], + 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: comet.address, + }, + ], + 534352: [ + { + baseSymbol: 'USDC', + cometAddress: '0xB2f97c1Bd3bf02f5e74d13f02E3e26F93D77CE44', + }, + ], + 10: [ + { + baseSymbol: 'USDC', + cometAddress: '0x2e44e174f7D53F0212823acC11C01A11d58c5bCB', + }, + ], + }); + + // 8. + expect(await comet.baseTrackingSupplySpeed()).to.be.equal(exp(6 / 86400, 15, 18)); + expect(await comet.baseTrackingBorrowSpeed()).to.be.equal(exp(4 / 86400, 15, 18)); + } +}); \ No newline at end of file diff --git a/deployments/arbitrum/weth/migrations/1718791267_add_wbtc_as_collateral.ts b/deployments/arbitrum/weth/migrations/1718791267_add_wbtc_as_collateral.ts new file mode 100644 index 000000000..38b390278 --- /dev/null +++ b/deployments/arbitrum/weth/migrations/1718791267_add_wbtc_as_collateral.ts @@ -0,0 +1,216 @@ +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 WBTC_ADDRESS = '0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f'; +const WBTC_BTC_PRICE_FEED_ADDRESS = '0x0017abAc5b6f291F9164e35B1234CA1D697f9CF4'; +const BTC_ETH_PRICE_FEED_ADDRESS = '0xc5a90A6d7e4Af242dA238FFe279e9f2BA0c64B2e'; + +export default migration('1718791267_add_wbtc_as_collateral', { + async prepare(deploymentManager: DeploymentManager) { + const _wbtcScalingPriceFeed = await deploymentManager.deploy( + 'WBTC:priceFeed', + 'pricefeeds/WBTCPriceFeed.sol', + [ + WBTC_BTC_PRICE_FEED_ADDRESS, // WBTC / BTC price feed + BTC_ETH_PRICE_FEED_ADDRESS, // BTC / ETH price feed + 8, // decimals + ] + ); + return { wbtcScalingPriceFeed: _wbtcScalingPriceFeed.address }; + }, + + enact: async (deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, { wbtcScalingPriceFeed }) => { + const trace = deploymentManager.tracer(); + const { + bridgeReceiver, + timelock: l2Timelock, + comet, + cometAdmin, + configurator + } = await deploymentManager.getContracts(); + + const { + arbitrumInbox, + timelock, + governor + } = await govDeploymentManager.getContracts(); + + const WBTC = await deploymentManager.existing( + 'WBTC', + WBTC_ADDRESS, + 'arbitrum', + 'contracts/ERC20.sol:ERC20' + ); + + const wbtcPricefeed = await deploymentManager.existing( + 'WBTC:priceFeed', + wbtcScalingPriceFeed, + 'arbitrum' + ); + + const wbtcAssetConfig = { + asset: WBTC.address, + priceFeed: wbtcPricefeed.address, + decimals: 8n, + borrowCollateralFactor: exp(0.80, 18), + liquidateCollateralFactor: exp(0.85, 18), + liquidationFactor: exp(0.95, 18), + supplyCap: exp(1_000, 8), + }; + + const addAssetCalldata = ethers.utils.defaultAbiCoder.encode( + ['address', 'tuple(address,address,uint8,uint64,uint64,uint64,uint128)'], + [comet.address, + [ + wbtcAssetConfig.asset, + wbtcAssetConfig.priceFeed, + wbtcAssetConfig.decimals, + wbtcAssetConfig.borrowCollateralFactor, + wbtcAssetConfig.liquidateCollateralFactor, + wbtcAssetConfig.liquidationFactor, + wbtcAssetConfig.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 WETH 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 = 'DESCRIPTION'; + 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 wbtcAssetIndex = Number(await comet.numAssets()) - 1; + + const WBTC = await deploymentManager.existing( + 'WBTC', + WBTC_ADDRESS, + 'arbitrum', + 'contracts/ERC20.sol:ERC20' + ); + + const wbtcAssetConfig = { + asset: WBTC.address, + priceFeed: '', + decimals: 8n, + borrowCollateralFactor: exp(0.80, 18), + liquidateCollateralFactor: exp(0.85, 18), + liquidationFactor: exp(0.95, 18), + supplyCap: exp(1_000, 8), + }; + + // 1. & 2. Compare WBTC asset config with Comet and Configurator asset info + const cometWBTCHAssetInfo = await comet.getAssetInfoByAddress( + WBTC_ADDRESS + ); + + expect(wbtcAssetIndex).to.be.equal(cometWBTCHAssetInfo.offset); + expect(wbtcAssetConfig.asset).to.be.equal(cometWBTCHAssetInfo.asset); + expect(exp(1, wbtcAssetConfig.decimals)).to.be.equal( + cometWBTCHAssetInfo.scale + ); + expect(wbtcAssetConfig.borrowCollateralFactor).to.be.equal( + cometWBTCHAssetInfo.borrowCollateralFactor + ); + expect(wbtcAssetConfig.liquidateCollateralFactor).to.be.equal( + cometWBTCHAssetInfo.liquidateCollateralFactor + ); + expect(wbtcAssetConfig.liquidationFactor).to.be.equal( + cometWBTCHAssetInfo.liquidationFactor + ); + expect(wbtcAssetConfig.supplyCap).to.be.equal( + cometWBTCHAssetInfo.supplyCap + ); + const configuratorEsETHAssetConfig = ( + await configurator.getConfiguration(comet.address) + ).assetConfigs[wbtcAssetIndex]; + expect(wbtcAssetConfig.asset).to.be.equal( + configuratorEsETHAssetConfig.asset + ); + expect(wbtcAssetConfig.decimals).to.be.equal( + configuratorEsETHAssetConfig.decimals + ); + expect(wbtcAssetConfig.borrowCollateralFactor).to.be.equal( + configuratorEsETHAssetConfig.borrowCollateralFactor + ); + expect(wbtcAssetConfig.liquidateCollateralFactor).to.be.equal( + configuratorEsETHAssetConfig.liquidateCollateralFactor + ); + expect(wbtcAssetConfig.liquidationFactor).to.be.equal( + configuratorEsETHAssetConfig.liquidationFactor + ); + expect(wbtcAssetConfig.supplyCap).to.be.equal( + configuratorEsETHAssetConfig.supplyCap + ); + }, +}); \ No newline at end of file diff --git a/scenario/SupplyScenario.ts b/scenario/SupplyScenario.ts index 5c12fc065..6cd1d7445 100644 --- a/scenario/SupplyScenario.ts +++ b/scenario/SupplyScenario.ts @@ -347,6 +347,10 @@ scenario( } ); +function isArbitrumWeth(context: CometContext): boolean { + return context.world.base.network === 'arbitrum' && context.world.base.deployment === 'weth'; +} + scenario( 'Comet#supplyFrom > base asset with token fees', { @@ -400,9 +404,11 @@ scenario( scenario( 'Comet#supplyFrom > repay borrow', { - tokenBalances: { - albert: { $base: 1010 } - }, + tokenBalances: async (ctx) => ( + { + albert: isArbitrumWeth(ctx)? { $base: 1160 } : { $base: 1010 } + } + ), cometBalances: { betty: { $base: '<= -1000' } // in units of asset, not wei }, diff --git a/scenario/constraints/MigrationConstraint.ts b/scenario/constraints/MigrationConstraint.ts index a0c2b16a2..d72112d40 100644 --- a/scenario/constraints/MigrationConstraint.ts +++ b/scenario/constraints/MigrationConstraint.ts @@ -42,7 +42,7 @@ export class MigrationConstraint implements StaticConstr // Order migrations deterministically and store in the context (i.e. for verification) const migrations = migrationList.sort((a, b) => a.name.localeCompare(b.name)).map(m => ({ migration: m })); - ctx.migrations = migrations; + ctx.migrations = []; debug(`${label} Running scenario with migrations: ${JSON.stringify(migrations.map((m) => m.migration.name))}`); for (const migrationData of migrations) { @@ -53,6 +53,7 @@ export class MigrationConstraint implements StaticConstr migrationData.skipVerify = true; debug(`${label} Migration ${migration.name} has already been enacted`); } else { + ctx.migrations.push(migrationData); migrationData.preMigrationBlockNumber = await ctx.world.deploymentManager.hre.ethers.provider.getBlockNumber(); const lastProposalBefore = await governor.proposalCount(); await migration.actions.enact(ctx.world.deploymentManager, govDeploymentManager, artifact);