diff --git a/.github/workflows/deploy-market.yaml b/.github/workflows/deploy-market.yaml index a41584ded..c2066d7ec 100644 --- a/.github/workflows/deploy-market.yaml +++ b/.github/workflows/deploy-market.yaml @@ -61,7 +61,7 @@ jobs: with: wallet_connect_project_id: ${{ secrets.WALLET_CONNECT_PROJECT_ID }} requested_network: "${{ inputs.network }}" - ethereum_url: "${{ fromJSON('{\"linea\":\"${LINEA_QUICKNODE_LINK}\",\"ronin\":\"${RONIN_QUICKNODE_LINK}\",\"unichain\":\"${UNICHAIN_QUICKNODE_LINK}\",\"mantle\":\"${MANTLE_QUICKNODE_LINK}\",\"optimism\":\"${OPTIMISM_QUICKNODE_LINK}\",\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"${MAINNET_QUICKNODE_LINK}\",\"sepolia\":\"${SEPOLIA_QUICKNODE_LINK}\",\"polygon\":\"${POLYGON_QUICKNODE_LINK}\",\"arbitrum\":\"${ARBITRUM_QUICKNODE_LINK}\",\"base\":\"${BASE_QUICKNODE_LINK}\",\"scroll\":\"https://rpc.scroll.io\"}')[github.event.inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"linea\":\"${LINEA_QUICKNODE_LINK}\",\"ronin\":\"${RONIN_QUICKNODE_LINK}\",\"unichain\":\"${UNICHAIN_QUICKNODE_LINK}\",\"mantle\":\"${MANTLE_QUICKNODE_LINK}\",\"optimism\":\"${OPTIMISM_QUICKNODE_LINK}\",\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"${MAINNET_QUICKNODE_LINK}\",\"sepolia\":\"${SEPOLIA_QUICKNODE_LINK}\",\"polygon\":\"${POLYGON_QUICKNODE_LINK}\",\"arbitrum\":\"${ARBITRUM_QUICKNODE_LINK}\",\"base\":\"${BASE_QUICKNODE_LINK}\",\"scroll\":\"https://scroll.drpc.org\"}')[github.event.inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' diff --git a/.github/workflows/enact-migration.yaml b/.github/workflows/enact-migration.yaml index faa035cbf..73b48a0cb 100644 --- a/.github/workflows/enact-migration.yaml +++ b/.github/workflows/enact-migration.yaml @@ -105,7 +105,7 @@ jobs: with: wallet_connect_project_id: ${{ secrets.WALLET_CONNECT_PROJECT_ID }} requested_network: "${{ inputs.network }}" - ethereum_url: "${{ fromJSON('{\"linea\":\"${LINEA_QUICKNODE_LINK}\",\"ronin\":\"${RONIN_QUICKNODE_LINK}\",\"unichain\":\"${UNICHAIN_QUICKNODE_LINK}\",\"mantle\":\"${MANTLE_QUICKNODE_LINK}\",\"optimism\":\"${OPTIMISM_QUICKNODE_LINK}\",\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"${MAINNET_QUICKNODE_LINK}\",\"sepolia\":\"${SEPOLIA_QUICKNODE_LINK}\",\"polygon\":\"${POLYGON_QUICKNODE_LINK}\",\"arbitrum\":\"${ARBITRUM_QUICKNODE_LINK}\",\"base\":\"${BASE_QUICKNODE_LINK}\",\"scroll\":\"https://rpc.scroll.io\"}')[github.event.inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"linea\":\"${LINEA_QUICKNODE_LINK}\",\"ronin\":\"${RONIN_QUICKNODE_LINK}\",\"unichain\":\"${UNICHAIN_QUICKNODE_LINK}\",\"mantle\":\"${MANTLE_QUICKNODE_LINK}\",\"optimism\":\"${OPTIMISM_QUICKNODE_LINK}\",\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"${MAINNET_QUICKNODE_LINK}\",\"sepolia\":\"${SEPOLIA_QUICKNODE_LINK}\",\"polygon\":\"${POLYGON_QUICKNODE_LINK}\",\"arbitrum\":\"${ARBITRUM_QUICKNODE_LINK}\",\"base\":\"${BASE_QUICKNODE_LINK}\",\"scroll\":\"https://scroll.drpc.org\"}')[github.event.inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' diff --git a/.github/workflows/prepare-migration.yaml b/.github/workflows/prepare-migration.yaml index 042e66729..27392ffcc 100644 --- a/.github/workflows/prepare-migration.yaml +++ b/.github/workflows/prepare-migration.yaml @@ -75,7 +75,7 @@ jobs: with: wallet_connect_project_id: ${{ secrets.WALLET_CONNECT_PROJECT_ID }} requested_network: "${{ inputs.network }}" - ethereum_url: "${{ fromJSON('{\"linea\":\"${LINEA_QUICKNODE_LINK}\",\"ronin\":\"${RONIN_QUICKNODE_LINK}\",\"unichain\":\"${UNICHAIN_QUICKNODE_LINK}\",\"mantle\":\"${MANTLE_QUICKNODE_LINK}\",\"optimism\":\"${OPTIMISM_QUICKNODE_LINK}\",\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"${MAINNET_QUICKNODE_LINK}\",\"sepolia\":\"${SEPOLIA_QUICKNODE_LINK}\",\"polygon\":\"${POLYGON_QUICKNODE_LINK}\",\"arbitrum\":\"${ARBITRUM_QUICKNODE_LINK}\",\"base\":\"${BASE_QUICKNODE_LINK}\",\"scroll\":\"https://rpc.scroll.io\"}')[github.event.inputs.network] }}" + ethereum_url: "${{ fromJSON('{\"linea\":\"${LINEA_QUICKNODE_LINK}\",\"ronin\":\"${RONIN_QUICKNODE_LINK}\",\"unichain\":\"${UNICHAIN_QUICKNODE_LINK}\",\"mantle\":\"${MANTLE_QUICKNODE_LINK}\",\"optimism\":\"${OPTIMISM_QUICKNODE_LINK}\",\"fuji\":\"https://api.avax-test.network/ext/bc/C/rpc\",\"mainnet\":\"${MAINNET_QUICKNODE_LINK}\",\"sepolia\":\"${SEPOLIA_QUICKNODE_LINK}\",\"polygon\":\"${POLYGON_QUICKNODE_LINK}\",\"arbitrum\":\"${ARBITRUM_QUICKNODE_LINK}\",\"base\":\"${BASE_QUICKNODE_LINK}\",\"scroll\":\"https://scroll.drpc.org\"}')[github.event.inputs.network] }}" port: 8585 if: github.event.inputs.eth_pk == '' diff --git a/deployments/arbitrum/usdc/relations.ts b/deployments/arbitrum/usdc/relations.ts index 9a8535766..0e4bb52be 100644 --- a/deployments/arbitrum/usdc/relations.ts +++ b/deployments/arbitrum/usdc/relations.ts @@ -11,6 +11,13 @@ export default { TransparentUpgradeableProxy: { artifact: 'contracts/ERC20.sol:ERC20' }, + AdminUpgradableProxy: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, // Native USDC '0xaf88d065e77c8cC2239327C5EDb3A432268e5831': { artifact: 'contracts/ERC20.sol:ERC20', diff --git a/deployments/arbitrum/usdc/roots.json b/deployments/arbitrum/usdc/roots.json index 54603b0c6..1ae6e7f63 100644 --- a/deployments/arbitrum/usdc/roots.json +++ b/deployments/arbitrum/usdc/roots.json @@ -4,5 +4,6 @@ "rewards": "0x88730d254A2f7e6AC8388c3198aFd694bA9f7fae", "bridgeReceiver": "0x42480C37B249e33aABaf4c22B20235656bd38068", "bulker": "0xbdE8F31D2DdDA895264e27DD990faB3DC87b372d", - "CCTPMessageTransmitter": "0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca" + "CCTPTokenMessenger": "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", + "CCTPMessageTransmitter": "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64" } \ No newline at end of file diff --git a/deployments/base/aero/migrations/1761125221_upgrade_to_capo_price_feeds.ts b/deployments/base/aero/migrations/1761125221_upgrade_to_capo_price_feeds.ts new file mode 100644 index 000000000..d58fd5fb7 --- /dev/null +++ b/deployments/base/aero/migrations/1761125221_upgrade_to_capo_price_feeds.ts @@ -0,0 +1,160 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, proposal, exp } from '../../../../src/deploy'; +import { utils } from 'ethers'; +import { AggregatorV3Interface } from '../../../../build/types'; + +const ETH_USD_PRICE_FEED = '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70'; + +const WSTETH_ADDRESS = '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452'; +const WSTETH_STETH_PRICE_FEED_ADDRESS = '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061'; + +const FEED_DECIMALS = 8; +const blockToFetch = 36000000; + +let newWstETHPriceFeed: string; +let oldWstETHPriceFeed: string; + +export default migration('1761125221_upgrade_to_capo_price_feeds', { + async prepare(deploymentManager: DeploymentManager) { + const { timelock } = await deploymentManager.getContracts(); + const blockToFetchTimestamp = (await deploymentManager.hre.ethers.provider.getBlock(blockToFetch))!.timestamp; + + //1. wstEth + const rateProviderWstEth = await deploymentManager.existing('wstEth:priceFeed', WSTETH_STETH_PRICE_FEED_ADDRESS, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioWstEth] = await rateProviderWstEth.latestRoundData({ blockTag: blockToFetch }); + + const wstEthCapoPriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + ETH_USD_PRICE_FEED, + WSTETH_STETH_PRICE_FEED_ADDRESS, + 'wstETH / USD CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioWstEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0404, 4) + } + ], + true + ); + + return { + wstEthCapoPriceFeedAddress: wstEthCapoPriceFeed.address + }; + }, + + async enact(deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, { + wstEthCapoPriceFeedAddress + }) { + newWstETHPriceFeed = wstEthCapoPriceFeedAddress; + + const trace = deploymentManager.tracer(); + + const { + configurator, + comet, + bridgeReceiver, + cometAdmin + } = await deploymentManager.getContracts(); + + const { + governor, + baseL1CrossDomainMessenger + } = await govDeploymentManager.getContracts(); + + const updateWstEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + WSTETH_ADDRESS, + wstEthCapoPriceFeedAddress + ) + ); + + const deployAndUpgradeToCalldata = await calldata( + cometAdmin.populateTransaction.deployAndUpgradeTo( + configurator.address, + comet.address + ) + ); + + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, cometAdmin.address], + [0, 0], + ['updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)'], + [updateWstEthPriceFeedCalldata, deployAndUpgradeToCalldata], + ] + ); + + [,, oldWstETHPriceFeed] = await comet.getAssetInfoByAddress(WSTETH_ADDRESS); + + const mainnetActions = [ + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [ + bridgeReceiver.address, + l2ProposalData, + 3_000_000 + ] + }, + ]; + + const description = `# Update wstETH price feed in cAEROv3 on Base with CAPO implementation. + +## Proposal summary + +This proposal updates existing price feeds for wstETH on the AERO market on Base. + +### CAPO summary + +CAPO is a price oracle adapter designed to support assets that grow gradually relative to a base asset - such as liquid staking tokens that accumulate yield over time. It provides a mechanism to track this expected growth while protecting downstream protocol from sudden or manipulated price spikes. wstETH price feed is updated to their CAPO implementations. + +Further detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/1038) and [forum discussion for CAPO](https://www.comp.xyz/t/woof-correlated-assets-price-oracle-capo/6245). + +### CAPO audit + +CAPO has been audited by [OpenZeppelin](https://www.comp.xyz/t/capo-price-feed-audit/6631, as well as the LST / LRT implementation [here](https://www.comp.xyz/t/capo-lst-lrt-audit/7118). + +## Proposal actions + +The first action updates wstETH price feed to the CAPO implementation. This sends the encoded 'updateAssetPriceFeed' and 'deployAndUpgradeTo' calls across the bridge to the governance receiver on Base. +`; + + const txn = await deploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ) + ); + const event = txn.events.find( + (event: { event: string }) => event.event === 'ProposalCreated' + ); + const [proposalId] = event.args; + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify(deploymentManager: DeploymentManager) { + const { comet, configurator } = await deploymentManager.getContracts(); + + const wstETHIndexInComet = await configurator.getAssetIndex(comet.address, WSTETH_ADDRESS); + + // Check if the price feeds are set correctly. + const wstETHInCometInfo = await comet.getAssetInfoByAddress(WSTETH_ADDRESS); + const wstETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[wstETHIndexInComet]; + + expect(wstETHInCometInfo.priceFeed).to.eq(newWstETHPriceFeed); + expect(wstETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newWstETHPriceFeed); + expect(await comet.getPrice(newWstETHPriceFeed)).to.equal(await comet.getPrice(oldWstETHPriceFeed)); + }, +}); diff --git a/deployments/base/weth/migrations/1761228877_upgrade_to_capo_price_feeds.ts b/deployments/base/weth/migrations/1761228877_upgrade_to_capo_price_feeds.ts new file mode 100644 index 000000000..56cffa4f0 --- /dev/null +++ b/deployments/base/weth/migrations/1761228877_upgrade_to_capo_price_feeds.ts @@ -0,0 +1,319 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, proposal, exp } from '../../../../src/deploy'; +import { utils } from 'ethers'; +import { AggregatorV3Interface } from '../../../../build/types'; + +const WSTETH_ADDRESS = '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452'; +const WSTETH_STETH_PRICE_FEED_ADDRESS = '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061'; + +const EZETH_ADDRESS = '0x2416092f143378750bb29b79ed961ab195cceea5'; +const EZETH_TO_ETH_PRICE_FEED_ADDRESS = '0xC4300B7CF0646F0Fe4C5B2ACFCCC4dCA1346f5d8'; + +const WRSETH_ADDRESS = '0xEDfa23602D0EC14714057867A78d01e94176BEA0'; +const WRSETH_ORACLE = '0xe8dD07CCf5BC4922424140E44Eb970F5950725ef'; + +const WEETH_ADDRESS = '0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A'; +const WEETH_STETH_PRICE_FEED_ADDRESS = '0x35e9D7001819Ea3B39Da906aE6b06A62cfe2c181'; + +const blockToFetch = 36000000; + +let newWstETHToETHPriceFeed: string; +let newEzETHToETHPriceFeed: string; +let newWrsEthToETHPriceFeed: string; +let newWeEthToETHPriceFeed: string; + +let oldWstETHToETHPriceFeed: string; +let oldEzETHToETHPriceFeed: string; +let oldWrsEthToETHPriceFeed: string; +let oldWeEthToETHPriceFeed: string; + +const FEED_DECIMALS = 8; +export default migration('1761228877_upgrade_to_capo_price_feeds', { + async prepare(deploymentManager: DeploymentManager) { + const { timelock } = await deploymentManager.getContracts(); + const blockToFetchTimestamp = (await deploymentManager.hre.ethers.provider.getBlock(blockToFetch))!.timestamp; + const constantPriceFeed = await deploymentManager.fromDep('WETH:priceFeed', 'base', 'weth'); + + //1. wstEth + const rateProviderWstEth = await deploymentManager.existing('wstETH:_rateProvider', WSTETH_STETH_PRICE_FEED_ADDRESS, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioWstEth] = await rateProviderWstEth.latestRoundData({blockTag: blockToFetch}); + + const wstEthCapoPriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + constantPriceFeed.address, + WSTETH_STETH_PRICE_FEED_ADDRESS, + 'wstETH / ETH CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioWstEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0404, 4) + } + ], + true + ); + + //2. ezEth + const rateProviderEzEth = await deploymentManager.existing('ezETH:_rateProvider', EZETH_TO_ETH_PRICE_FEED_ADDRESS, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioEzEth] = await rateProviderEzEth.latestRoundData({blockTag: blockToFetch}); + const ezEthCapoPriceFeed = await deploymentManager.deploy( + 'ezETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + constantPriceFeed.address, + EZETH_TO_ETH_PRICE_FEED_ADDRESS, + 'ezETH / ETH CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioEzEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0707, 4) + } + ], + true + ); + + const rateProviderRsEth = await deploymentManager.existing('rsETH:_rateProvider', WRSETH_ORACLE, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioWrsEth] = await rateProviderRsEth.latestRoundData({blockTag: blockToFetch}); + const rsEthCapoPriceFeed = await deploymentManager.deploy( + 'rsETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + constantPriceFeed.address, + WRSETH_ORACLE, + 'rsETH / ETH CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioWrsEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0554, 4) + } + ], + true + ); + + + const rateProviderWeEth = await deploymentManager.existing('weETH:_rateProvider', WEETH_STETH_PRICE_FEED_ADDRESS, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioWeEth] = await rateProviderWeEth.latestRoundData({blockTag: blockToFetch}); + const weEthCapoPriceFeed = await deploymentManager.deploy( + 'weETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + constantPriceFeed.address, + WEETH_STETH_PRICE_FEED_ADDRESS, + 'weETH / ETH CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioWeEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0323, 4) + } + ], + true + ); + + return { + wstEthCapoPriceFeedAddress: wstEthCapoPriceFeed.address, + ezEthCapoPriceFeedAddress: ezEthCapoPriceFeed.address, + rsEthCapoPriceFeedAddress: rsEthCapoPriceFeed.address, + weEthCapoPriceFeedAddress: weEthCapoPriceFeed.address + }; + }, + + async enact(deploymentManager: DeploymentManager, govDeploymentManager, { + wstEthCapoPriceFeedAddress, + ezEthCapoPriceFeedAddress, + rsEthCapoPriceFeedAddress, + weEthCapoPriceFeedAddress + }) { + + newWstETHToETHPriceFeed = wstEthCapoPriceFeedAddress; + newEzETHToETHPriceFeed = ezEthCapoPriceFeedAddress; + newWrsEthToETHPriceFeed = rsEthCapoPriceFeedAddress; + newWeEthToETHPriceFeed = weEthCapoPriceFeedAddress; + + const trace = deploymentManager.tracer(); + + const { + configurator, + comet, + bridgeReceiver, + cometAdmin + } = await deploymentManager.getContracts(); + + const { + governor, + baseL1CrossDomainMessenger + } = await govDeploymentManager.getContracts(); + + const updateEzEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + EZETH_ADDRESS, + ezEthCapoPriceFeedAddress + ) + ); + + const updateWstEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + WSTETH_ADDRESS, + wstEthCapoPriceFeedAddress + ) + ); + + const updateRsEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + WRSETH_ADDRESS, + rsEthCapoPriceFeedAddress + ) + ); + + const updateWeEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + WEETH_ADDRESS, + weEthCapoPriceFeedAddress + ) + ); + + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, + configurator.address, + configurator.address, + configurator.address, + cometAdmin.address + ], + [0, 0, 0, 0, 0], + [ + 'updateAssetPriceFeed(address,address,address)', + 'updateAssetPriceFeed(address,address,address)', + 'updateAssetPriceFeed(address,address,address)', + 'updateAssetPriceFeed(address,address,address)', + 'deployAndUpgradeTo(address,address)' + ], + [ + updateWstEthPriceFeedCalldata, + updateEzEthPriceFeedCalldata, + updateRsEthPriceFeedCalldata, + updateWeEthPriceFeedCalldata, + deployAndUpgradeToCalldata + ], + ] + ); + + [,, oldWstETHToETHPriceFeed] = await comet.getAssetInfoByAddress(WSTETH_ADDRESS); + [,, oldEzETHToETHPriceFeed] = await comet.getAssetInfoByAddress(EZETH_ADDRESS); + [,, oldWrsEthToETHPriceFeed] = await comet.getAssetInfoByAddress(WRSETH_ADDRESS); + [,, oldWeEthToETHPriceFeed] = await comet.getAssetInfoByAddress(WEETH_ADDRESS); + + const mainnetActions = [ + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [ + bridgeReceiver.address, + l2ProposalData, + 3_000_000 + ] + }, + ]; + + const description = `# Update price feeds in cWETHv3 on Base with CAPO implementation. + +## Proposal summary + +This proposal updates existing price feeds for wstETH, ezETH, rsETH, and weETH on the WETH market on Base. + +### CAPO summary + +CAPO is a price oracle adapter designed to support assets that grow gradually relative to a base asset - such as liquid staking tokens that accumulate yield over time. It provides a mechanism to track this expected growth while protecting downstream protocol from sudden or manipulated price spikes. wstETH, ezETH, rsETH, and weETH price feeds are updated to their CAPO implementations. + +Further detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/1040) and [forum discussion for CAPO](https://www.comp.xyz/t/woof-correlated-assets-price-oracle-capo/6245). + +### CAPO audit + +CAPO has been audited by [OpenZeppelin](https://www.comp.xyz/t/capo-price-feed-audit/6631, as well as the LST / LRT implementation [here](https://www.comp.xyz/t/capo-lst-lrt-audit/7118). + +## Proposal actions + +The first action updates wstETH, ezETH, rsETH, and weETH price feeds to the CAPO implementation. This sends the encoded 'updateAssetPriceFeed' and 'deployAndUpgradeTo' calls across the bridge to the governance receiver on Base. +`; + const txn = await govDeploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ) + ); + + const event = txn.events.find( + (event: { event: string }) => event.event === 'ProposalCreated' + ); + const [proposalId] = event.args; + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify(deploymentManager: DeploymentManager) { + const { comet, configurator } = await deploymentManager.getContracts(); + + // 1. wstETH + const wstETHIndexInComet = await configurator.getAssetIndex(comet.address, WSTETH_ADDRESS); + const wstETHInCometInfo = await comet.getAssetInfoByAddress(WSTETH_ADDRESS); + const wstETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[wstETHIndexInComet]; + + expect(wstETHInCometInfo.priceFeed).to.eq(newWstETHToETHPriceFeed); + expect(wstETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newWstETHToETHPriceFeed); + expect(await comet.getPrice(newWstETHToETHPriceFeed)).to.be.closeTo(await comet.getPrice(oldWstETHToETHPriceFeed), 1e6); + + // 2. ezETH + const ezETHIndexInComet = await configurator.getAssetIndex(comet.address, EZETH_ADDRESS); + const ezETHInCometInfo = await comet.getAssetInfoByAddress(EZETH_ADDRESS); + const ezETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[ezETHIndexInComet]; + + expect(ezETHInCometInfo.priceFeed).to.eq(newEzETHToETHPriceFeed); + expect(ezETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newEzETHToETHPriceFeed); + expect(await comet.getPrice(newEzETHToETHPriceFeed)).to.equal(await comet.getPrice(oldEzETHToETHPriceFeed)); + + // 3. wrsETH + const wrsETHIndexInComet = await configurator.getAssetIndex(comet.address, WRSETH_ADDRESS); + const wrsETHInCometInfo = await comet.getAssetInfoByAddress(WRSETH_ADDRESS); + const wrsETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[wrsETHIndexInComet]; + + expect(wrsETHInCometInfo.priceFeed).to.eq(newWrsEthToETHPriceFeed); + expect(wrsETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newWrsEthToETHPriceFeed); + expect(await comet.getPrice(newWrsEthToETHPriceFeed)).to.equal(await comet.getPrice(oldWrsEthToETHPriceFeed)); + + // 4. weETH + const weETHIndexInComet = await configurator.getAssetIndex(comet.address, WEETH_ADDRESS); + const weETHInCometInfo = await comet.getAssetInfoByAddress(WEETH_ADDRESS); + const weETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[weETHIndexInComet]; + + expect(weETHInCometInfo.priceFeed).to.eq(newWeEthToETHPriceFeed); + expect(weETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newWeEthToETHPriceFeed); + expect(await comet.getPrice(newWeEthToETHPriceFeed)).to.equal(await comet.getPrice(oldWeEthToETHPriceFeed)); + }, +}); diff --git a/deployments/mainnet/usdc/migrations/1776768296_update_curve_params_on_l2.ts b/deployments/mainnet/usdc/migrations/1776768296_update_curve_params_on_l2.ts new file mode 100644 index 000000000..63048e21d --- /dev/null +++ b/deployments/mainnet/usdc/migrations/1776768296_update_curve_params_on_l2.ts @@ -0,0 +1,483 @@ +import { expect } from 'chai'; +import { BigNumber, utils } from 'ethers'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { proposal } from '../../../../src/deploy'; +import { forkedHreForBase } from '../../../../plugins/scenario/utils/hreForBase'; +import { applyL1ToL2Alias, estimateL2Transaction } from '../../../../scenario/utils/arbitrumUtils'; + +/* +Comet Kink Rate Max Rate +Base WETH 1.50% 3.00% +Arbitrum WETH 1.50% 3.00% +Optimism WETH 1.50% 3.00% +Linea WETH 1.50% 3.00% +Unichain WETH 1.50% 3.00% + +Params: + +Optimism WETH changes +borrowPerYearInterestRateSlopeLow : 5555555555555555 +borrowPerYearInterestRateSlopeHigh : 150000000000000000 +supplyPerYearInterestRateSlopeLow : 15000000000000000 +supplyPerYearInterestRateSlopeHigh : 135000000000000000 + +Base WETH changes +borrowPerYearInterestRateSlopeLow : 5555555555555555 +borrowPerYearInterestRateSlopeHigh : 150000000000000000 +supplyPerYearInterestRateSlopeLow : 15000000000000000 +supplyPerYearInterestRateSlopeHigh : 135000000000000000 + +Arbitrum WETH changes +borrowPerYearInterestRateSlopeLow : 5555555555555555 +borrowPerYearInterestRateSlopeHigh : 150000000000000000 +supplyPerYearInterestRateSlopeLow : 15000000000000000 +supplyPerYearInterestRateSlopeHigh : 135000000000000000 + +Linea WETH changes +borrowPerYearInterestRateSlopeLow : 5555555555555555 +borrowPerYearInterestRateSlopeHigh : 150000000000000000 +supplyPerYearInterestRateSlopeLow : 15000000000000000 +supplyPerYearInterestRateSlopeHigh : 135000000000000000 + +Unichain WETH changes +borrowPerYearInterestRateSlopeLow : 5555555555555555 +borrowPerYearInterestRateSlopeHigh : 150000000000000000 +supplyPerYearInterestRateSlopeLow : 15000000000000000 +supplyPerYearInterestRateSlopeHigh : 135000000000000000 + +*/ +const borrowPerYearInterestRateSlopeLow = '5555555555555555'; +const borrowPerYearInterestRateSlopeHigh = '150000000000000000'; + +const supplyPerYearInterestRateSlopeLow = '15000000000000000'; +const supplyPerYearInterestRateSlopeHigh = '135000000000000000'; + +let expectedBorrowPerSecondInterestRateSlopeLow: BigNumber; +let expectedBorrowPerSecondInterestRateSlopeHigh: BigNumber; + +let expectedSupplyPerSecondInterestRateSlopeLow: BigNumber; +let expectedSupplyPerSecondInterestRateSlopeHigh: BigNumber; + +export default migration('1776768296_update_curve_params_on_l2', { + async prepare() { + return {}; + }, + + async enact(deploymentManager: DeploymentManager) { + + const trace = deploymentManager.tracer(); + + const { + timelock, + governor, + opL1CrossDomainMessenger, + baseL1CrossDomainMessenger, + arbitrumInbox, + lineaMessageService, + unichainL1CrossDomainMessenger + } = await deploymentManager.getContracts(); + + expectedBorrowPerSecondInterestRateSlopeLow = BigNumber.from(borrowPerYearInterestRateSlopeLow).div(365 * 86400); + expectedBorrowPerSecondInterestRateSlopeHigh = BigNumber.from(borrowPerYearInterestRateSlopeHigh).div(365 * 86400); + + expectedSupplyPerSecondInterestRateSlopeLow = BigNumber.from(supplyPerYearInterestRateSlopeLow).div(365 * 86400); + expectedSupplyPerSecondInterestRateSlopeHigh = BigNumber.from(supplyPerYearInterestRateSlopeHigh).div(365 * 86400); + + // Optimism + const opHre = await forkedHreForBase({ name: 'optimism-weth', network: 'optimism', deployment: 'weth' }); + const opDm = await deploymentManager.addBridgedDeploymentManager('optimism', 'weth', opHre); + const { + bridgeReceiver : opBridgeReceiver, + configurator: opConfigurator, + cometAdmin: opCometAdmin, + comet: opWethComet, + } = await opDm.getContracts(); + + const optimismSetBorrowPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [opWethComet.address, borrowPerYearInterestRateSlopeLow]); + const optimismSetBorrowPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [opWethComet.address, borrowPerYearInterestRateSlopeHigh]); + const optimismSetSupplyPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [opWethComet.address, supplyPerYearInterestRateSlopeLow]); + const optimismSetSupplyPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [opWethComet.address, supplyPerYearInterestRateSlopeHigh]); + const optimismDeployAndUpgradeToCalldata = utils.defaultAbiCoder.encode(['address', 'address'], [opConfigurator.address, opWethComet.address]); + + const opProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + opConfigurator.address, opConfigurator.address, + opConfigurator.address, opConfigurator.address, + opCometAdmin.address, + ], + [ + 0, 0, + 0, 0, + 0 + ], + [ + 'setBorrowPerYearInterestRateSlopeLow(address,uint64)', + 'setBorrowPerYearInterestRateSlopeHigh(address,uint64)', + 'setSupplyPerYearInterestRateSlopeLow(address,uint64)', + 'setSupplyPerYearInterestRateSlopeHigh(address,uint64)', + 'deployAndUpgradeTo(address,address)', + ], + [ + optimismSetBorrowPerYearInterestRateSlopeLowCalldata, optimismSetBorrowPerYearInterestRateSlopeHighCalldata, + optimismSetSupplyPerYearInterestRateSlopeLowCalldata, optimismSetSupplyPerYearInterestRateSlopeHighCalldata, + optimismDeployAndUpgradeToCalldata + ], + ] + ); + + // Base + const baseHre = await forkedHreForBase({ name: 'base-weth', network: 'base', deployment: 'weth' }); + const baseDm = await deploymentManager.addBridgedDeploymentManager('base', 'weth', baseHre); + const { + bridgeReceiver : baseBridgeReceiver, + configurator: baseConfigurator, + cometAdmin: baseCometAdmin, + comet: baseWethComet, + } = await baseDm.getContracts(); + + const baseSetBorrowPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [baseWethComet.address, borrowPerYearInterestRateSlopeLow]); + const baseSetBorrowPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [baseWethComet.address, borrowPerYearInterestRateSlopeHigh]); + const baseSetSupplyPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [baseWethComet.address, supplyPerYearInterestRateSlopeLow]); + const baseSetSupplyPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [baseWethComet.address, supplyPerYearInterestRateSlopeHigh]); + const baseDeployAndUpgradeToCalldata = utils.defaultAbiCoder.encode(['address', 'address'], [baseConfigurator.address, baseWethComet.address]); + + const baseProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + baseConfigurator.address, baseConfigurator.address, + baseConfigurator.address, baseConfigurator.address, + baseCometAdmin.address, + ], + [ + 0, 0, + 0, 0, + 0 + ], + [ + 'setBorrowPerYearInterestRateSlopeLow(address,uint64)', + 'setBorrowPerYearInterestRateSlopeHigh(address,uint64)', + 'setSupplyPerYearInterestRateSlopeLow(address,uint64)', + 'setSupplyPerYearInterestRateSlopeHigh(address,uint64)', + 'deployAndUpgradeTo(address,address)', + ], + [ + baseSetBorrowPerYearInterestRateSlopeLowCalldata, baseSetBorrowPerYearInterestRateSlopeHighCalldata, + baseSetSupplyPerYearInterestRateSlopeLowCalldata, baseSetSupplyPerYearInterestRateSlopeHighCalldata, + baseDeployAndUpgradeToCalldata + ], + ] + ); + + // Arbitrum + const arbitrumHre = await forkedHreForBase({ name: 'arbitrum-weth', network: 'arbitrum', deployment: 'weth' }); + const arbitrumDm = await deploymentManager.addBridgedDeploymentManager('arbitrum', 'weth', arbitrumHre); + const { + bridgeReceiver: arbitrumBridgeReceiver, + configurator: arbitrumConfigurator, + cometAdmin: arbitrumCometAdmin, + comet: arbitrumWethComet, + timelock: arbitrumTimelock, + } = await arbitrumDm.getContracts(); + + const arbitrumSetBorrowPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [arbitrumWethComet.address, borrowPerYearInterestRateSlopeLow]); + const arbitrumSetBorrowPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [arbitrumWethComet.address, borrowPerYearInterestRateSlopeHigh]); + const arbitrumSetSupplyPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [arbitrumWethComet.address, supplyPerYearInterestRateSlopeLow]); + const arbitrumSetSupplyPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [arbitrumWethComet.address, supplyPerYearInterestRateSlopeHigh]); + const arbitrumDeployAndUpgradeToCalldata = utils.defaultAbiCoder.encode(['address', 'address'], [arbitrumConfigurator.address, arbitrumWethComet.address]); + + const arbitrumProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + arbitrumConfigurator.address, arbitrumConfigurator.address, + arbitrumConfigurator.address, arbitrumConfigurator.address, + arbitrumCometAdmin.address, + ], + [ + 0, 0, + 0, 0, + 0 + ], + [ + 'setBorrowPerYearInterestRateSlopeLow(address,uint64)', + 'setBorrowPerYearInterestRateSlopeHigh(address,uint64)', + 'setSupplyPerYearInterestRateSlopeLow(address,uint64)', + 'setSupplyPerYearInterestRateSlopeHigh(address,uint64)', + 'deployAndUpgradeTo(address,address)', + ], + [ + arbitrumSetBorrowPerYearInterestRateSlopeLowCalldata, arbitrumSetBorrowPerYearInterestRateSlopeHighCalldata, + arbitrumSetSupplyPerYearInterestRateSlopeLowCalldata, arbitrumSetSupplyPerYearInterestRateSlopeHighCalldata, + arbitrumDeployAndUpgradeToCalldata + ], + ] + ); + const createRetryableTicketGasParams = await estimateL2Transaction( + { + from: applyL1ToL2Alias(timelock.address), + to: arbitrumBridgeReceiver.address, + data: arbitrumProposalData + }, + arbitrumDm + ); + + // Linea + const lineaHre = await forkedHreForBase({ name: 'linea-weth', network: 'linea', deployment: 'weth' }); + const lineaDm = await deploymentManager.addBridgedDeploymentManager('linea', 'weth', lineaHre); + const { + bridgeReceiver: lineaBridgeReceiver, + configurator: lineaConfigurator, + cometAdmin: lineaCometAdmin, + comet: lineaWethComet, + } = await lineaDm.getContracts(); + + const lineaSetBorrowPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [lineaWethComet.address, borrowPerYearInterestRateSlopeLow]); + const lineaSetBorrowPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [lineaWethComet.address, borrowPerYearInterestRateSlopeHigh]); + const lineaSetSupplyPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [lineaWethComet.address, supplyPerYearInterestRateSlopeLow]); + const lineaSetSupplyPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [lineaWethComet.address, supplyPerYearInterestRateSlopeHigh]); + const lineaDeployAndUpgradeToCalldata = utils.defaultAbiCoder.encode(['address', 'address'], [lineaConfigurator.address, lineaWethComet.address]); + + const lineaProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + lineaConfigurator.address, lineaConfigurator.address, + lineaConfigurator.address, lineaConfigurator.address, + lineaCometAdmin.address, + ], + [ + 0, 0, + 0, 0, + 0 + ], + [ + 'setBorrowPerYearInterestRateSlopeLow(address,uint64)', + 'setBorrowPerYearInterestRateSlopeHigh(address,uint64)', + 'setSupplyPerYearInterestRateSlopeLow(address,uint64)', + 'setSupplyPerYearInterestRateSlopeHigh(address,uint64)', + 'deployAndUpgradeTo(address,address)', + ], + [ + lineaSetBorrowPerYearInterestRateSlopeLowCalldata, lineaSetBorrowPerYearInterestRateSlopeHighCalldata, + lineaSetSupplyPerYearInterestRateSlopeLowCalldata, lineaSetSupplyPerYearInterestRateSlopeHighCalldata, + lineaDeployAndUpgradeToCalldata + ], + ] + ); + + // Unichain + const unichainHre = await forkedHreForBase({ name: 'unichain-weth', network: 'unichain', deployment: 'weth' }); + const unichainDm = await deploymentManager.addBridgedDeploymentManager('unichain', 'weth', unichainHre); + const { + bridgeReceiver: unichainBridgeReceiver, + configurator: unichainConfigurator, + cometAdmin: unichainCometAdmin, + comet: unichainWethComet, + } = await unichainDm.getContracts(); + + const unichainSetBorrowPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [unichainWethComet.address, borrowPerYearInterestRateSlopeLow]); + const unichainSetBorrowPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [unichainWethComet.address, borrowPerYearInterestRateSlopeHigh]); + const unichainSetSupplyPerYearInterestRateSlopeLowCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [unichainWethComet.address, supplyPerYearInterestRateSlopeLow]); + const unichainSetSupplyPerYearInterestRateSlopeHighCalldata = utils.defaultAbiCoder.encode(['address', 'uint64'], [unichainWethComet.address, supplyPerYearInterestRateSlopeHigh]); + const unichainDeployAndUpgradeToCalldata = utils.defaultAbiCoder.encode(['address', 'address'], [unichainConfigurator.address, unichainWethComet.address]); + + const unichainProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + unichainConfigurator.address, unichainConfigurator.address, + unichainConfigurator.address, unichainConfigurator.address, + unichainCometAdmin.address, + ], + [ + 0, 0, + 0, 0, + 0 + ], + [ + 'setBorrowPerYearInterestRateSlopeLow(address,uint64)', + 'setBorrowPerYearInterestRateSlopeHigh(address,uint64)', + 'setSupplyPerYearInterestRateSlopeLow(address,uint64)', + 'setSupplyPerYearInterestRateSlopeHigh(address,uint64)', + 'deployAndUpgradeTo(address,address)', + ], + [ + unichainSetBorrowPerYearInterestRateSlopeLowCalldata, unichainSetBorrowPerYearInterestRateSlopeHighCalldata, + unichainSetSupplyPerYearInterestRateSlopeLowCalldata, unichainSetSupplyPerYearInterestRateSlopeHighCalldata, + unichainDeployAndUpgradeToCalldata + ], + ] + ); + + + + const mainnetActions = [ + // Optimism proposal + { + contract: opL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [opBridgeReceiver.address, opProposalData, 3_000_000] + }, + // Base proposal + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [baseBridgeReceiver.address, baseProposalData, 3_000_000] + }, + // Arbitrum proposal + { + contract: arbitrumInbox, + signature: 'createRetryableTicket(address,uint256,uint256,address,address,uint256,uint256,bytes)', + args: [ + arbitrumBridgeReceiver.address, // address to, + 0, // uint256 l2CallValue, + createRetryableTicketGasParams.maxSubmissionCost, // uint256 maxSubmissionCost, + arbitrumTimelock.address, // address excessFeeRefundAddress, + arbitrumTimelock.address, // address callValueRefundAddress, + createRetryableTicketGasParams.gasLimit, // uint256 gasLimit, + createRetryableTicketGasParams.maxFeePerGas*2, // uint256 maxFeePerGas, + arbitrumProposalData, // bytes calldata data + ], + value: createRetryableTicketGasParams.deposit.mul(2), + }, + // Linea proposal + { + contract: lineaMessageService, + signature: 'sendMessage(address,uint256,bytes)', + args: [lineaBridgeReceiver.address, 0, lineaProposalData], + }, + // Unichain proposal + { + contract: unichainL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [unichainBridgeReceiver.address, unichainProposalData, 3_000_000], + }, + ]; + + const description = `# WETH Comets Interest Rate Curve Recommendations on L2 networks + +## Simple Summary + +Given heightened market activity and elevated volatility across ETH markets Gauntlet recommends the following Interest Rate (IR) Curve updates across WETH comets on L2 networks. These changes are designed to keep borrow costs low and avoid liquidations during the volatile period. + +## Motivation +The recent Kelp exploit has led to heightened volatility and shifting dynamics across ETH-correlated assets. In this environment LST/LRT-based looping strategies are facing a significantly high negative carry. The recommendations below flatten the slope below kink and compress the slope above kink resulting in lower kink and max borrow rates across all WETH markets. + +The intent is to: +* Keep Compound’s WETH borrow rates not too high so that unwinding can happen gradually in a volatile environment. +* Decrease existing borrow positions gradually without pushing rates to a level that would unwind a high amount of the existing ETH-leverage positions. We will continue to monitor utilization and reserve growth and will adjust as conditions evolve. + +## Specification +Target APRs at kink and at 100% utilization across the comets: +| Comet | Kink Rate | Max Rate | +| --------------- | --------- | -------- | +| Base WETH | 1.50% | 3.00% | +| Arbitrum WETH | 1.50% | 3.00% | +| Optimism WETH | 1.50% | 3.00% | +| Linea WETH | 1.50% | 3.00% | +| Unichain WETH | 1.50% | 3.00% | +Borrow kink is held at 90% across all markets. The base rate is held constant at current values. Slope Low and Slope High are updated to achieve the target kink and max rates above. [Forum Post](https://www.comp.xyz/t/weth-comet-interest-rate-curve-recommendations/7749)`; + + const txn = await deploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ), 0, 600_000 + ); + + const event = txn.events.find( + (event: { event: string }) => event.event === 'ProposalCreated' + ); + const [proposalId] = event.args; + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(): Promise { + return false; + }, + + async verify(deploymentManager: DeploymentManager) { + // Optimism + const opDm = deploymentManager.bridgedDeploymentManagers.get('optimism:weth') as DeploymentManager; + const { comet } = await opDm.getContracts(); + + const currentBorrowPerSecondInterestRateSlopeLow = await comet.borrowPerSecondInterestRateSlopeLow(); + const currentBorrowPerSecondInterestRateSlopeHigh = await comet.borrowPerSecondInterestRateSlopeHigh(); + + const currentSupplyPerSecondInterestRateSlopeLow = await comet.supplyPerSecondInterestRateSlopeLow(); + const currentSupplyPerSecondInterestRateSlopeHigh = await comet.supplyPerSecondInterestRateSlopeHigh(); + + expect(currentBorrowPerSecondInterestRateSlopeLow.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeLow.toString()); + expect(currentBorrowPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeHigh.toString()); + + expect(currentSupplyPerSecondInterestRateSlopeLow.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeLow.toString()); + expect(currentSupplyPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeHigh.toString()); + + // Base + const baseDm = deploymentManager.bridgedDeploymentManagers.get('base:weth') as DeploymentManager; + const { comet: baseComet } = await baseDm.getContracts(); + + const baseCurrentBorrowPerSecondInterestRateSlopeLow = await baseComet.borrowPerSecondInterestRateSlopeLow(); + const baseCurrentBorrowPerSecondInterestRateSlopeHigh = await baseComet.borrowPerSecondInterestRateSlopeHigh(); + + const baseCurrentSupplyPerSecondInterestRateSlopeLow = await baseComet.supplyPerSecondInterestRateSlopeLow(); + const baseCurrentSupplyPerSecondInterestRateSlopeHigh = await baseComet.supplyPerSecondInterestRateSlopeHigh(); + + expect(baseCurrentBorrowPerSecondInterestRateSlopeLow.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeLow.toString()); + expect(baseCurrentBorrowPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeHigh.toString()); + + expect(baseCurrentSupplyPerSecondInterestRateSlopeLow.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeLow.toString()); + expect(baseCurrentSupplyPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeHigh.toString()); + + // Arbitrum + const arbitrumDm = deploymentManager.bridgedDeploymentManagers.get('arbitrum:weth') as DeploymentManager; + const { comet: arbitrumComet } = await arbitrumDm.getContracts(); + + const arbitrumCurrentBorrowPerSecondInterestRateSlopeLow = await arbitrumComet.borrowPerSecondInterestRateSlopeLow(); + const arbitrumCurrentBorrowPerSecondInterestRateSlopeHigh = await arbitrumComet.borrowPerSecondInterestRateSlopeHigh(); + + const arbitrumCurrentSupplyPerSecondInterestRateSlopeLow = await arbitrumComet.supplyPerSecondInterestRateSlopeLow(); + const arbitrumCurrentSupplyPerSecondInterestRateSlopeHigh = await arbitrumComet.supplyPerSecondInterestRateSlopeHigh(); + + expect(arbitrumCurrentBorrowPerSecondInterestRateSlopeLow.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeLow.toString()); + expect(arbitrumCurrentBorrowPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeHigh.toString()); + + expect(arbitrumCurrentSupplyPerSecondInterestRateSlopeLow.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeLow.toString()); + expect(arbitrumCurrentSupplyPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeHigh.toString()); + + // Linea + const lineaDm = deploymentManager.bridgedDeploymentManagers.get('linea:weth') as DeploymentManager; + const { comet: lineaComet } = await lineaDm.getContracts(); + + const lineaCurrentBorrowPerSecondInterestRateSlopeLow = await lineaComet.borrowPerSecondInterestRateSlopeLow(); + const lineaCurrentBorrowPerSecondInterestRateSlopeHigh = await lineaComet.borrowPerSecondInterestRateSlopeHigh(); + + const lineaCurrentSupplyPerSecondInterestRateSlopeLow = await lineaComet.supplyPerSecondInterestRateSlopeLow(); + const lineaCurrentSupplyPerSecondInterestRateSlopeHigh = await lineaComet.supplyPerSecondInterestRateSlopeHigh(); + + expect(lineaCurrentBorrowPerSecondInterestRateSlopeLow.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeLow.toString()); + expect(lineaCurrentBorrowPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeHigh.toString()); + + expect(lineaCurrentSupplyPerSecondInterestRateSlopeLow.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeLow.toString()); + expect(lineaCurrentSupplyPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeHigh.toString()); + + // Unichain + const unichainDm = deploymentManager.bridgedDeploymentManagers.get('unichain:weth') as DeploymentManager; + const { comet: unichainComet } = await unichainDm.getContracts(); + + const unichainCurrentBorrowPerSecondInterestRateSlopeLow = await unichainComet.borrowPerSecondInterestRateSlopeLow(); + const unichainCurrentBorrowPerSecondInterestRateSlopeHigh = await unichainComet.borrowPerSecondInterestRateSlopeHigh(); + + const unichainCurrentSupplyPerSecondInterestRateSlopeLow = await unichainComet.supplyPerSecondInterestRateSlopeLow(); + const unichainCurrentSupplyPerSecondInterestRateSlopeHigh = await unichainComet.supplyPerSecondInterestRateSlopeHigh(); + + expect(unichainCurrentBorrowPerSecondInterestRateSlopeLow.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeLow.toString()); + expect(unichainCurrentBorrowPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedBorrowPerSecondInterestRateSlopeHigh.toString()); + + expect(unichainCurrentSupplyPerSecondInterestRateSlopeLow.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeLow.toString()); + expect(unichainCurrentSupplyPerSecondInterestRateSlopeHigh.toString()).to.equal(expectedSupplyPerSecondInterestRateSlopeHigh.toString()); + }, +}); \ No newline at end of file diff --git a/deployments/mainnet/usdc/relations.ts b/deployments/mainnet/usdc/relations.ts index 776d101e6..ef9403471 100644 --- a/deployments/mainnet/usdc/relations.ts +++ b/deployments/mainnet/usdc/relations.ts @@ -21,6 +21,13 @@ export default { } } }, + AdminUpgradableProxy: { + delegates: { + field: { + slot: '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + } + } + }, UUPSProxy: { artifact: 'contracts/ERC20.sol:ERC20', delegates: { diff --git a/deployments/mainnet/usdc/roots.json b/deployments/mainnet/usdc/roots.json index f01a53815..8cdf987c1 100644 --- a/deployments/mainnet/usdc/roots.json +++ b/deployments/mainnet/usdc/roots.json @@ -1,5 +1,4 @@ { - "l1TokenAdminRegistry": "0xb22764f98dD05c789929716D677382Df22C05Cb6", "comptrollerV2": "0x3d9819210a31b4961b30ef54be2aed79b9c9cd3b", "comet": "0xc3d688B66703497DAA19211EEdff47f25384cdc3", "configurator": "0x316f9708bB98af7dA9c68C1C3b5e79039cD336E3", @@ -8,8 +7,8 @@ "fxRoot": "0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2", "arbitrumInbox": "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f", "arbitrumL1GatewayRouter": "0x72Ce9c846789fdB6fC1f34aC4AD25Dd9ef7031ef", - "CCTPTokenMessenger": "0xbd3fa81b58ba92a82136038b25adec7066af3155", - "CCTPMessageTransmitter": "0x0a992d191deec32afe36203ad87d7d289a738f81", + "CCTPTokenMessenger": "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", + "CCTPMessageTransmitter": "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64", "baseL1CrossDomainMessenger": "0x866E82a600A1414e583f7F13623F1aC5d58b0Afa", "baseL1StandardBridge": "0x3154Cf16ccdb4C6d922629664174b904d80F2C35", "baseL1USDSBridge": "0xA5874756416Fa632257eEA380CAbd2E87cED352A", @@ -25,6 +24,7 @@ "lineaMessageService": "0xd19d4B5d358258f05D7B411E21A1460D11B0876F", "lineaL1TokenBridge": "0x051F1D88f0aF5763fB888eC4378b4D8B29ea3319", "lineaL1USDCBridge": "0x504A330327A089d8364C4ab3811Ee26976d388ce", + "l1TokenAdminRegistry": "0xb22764f98dD05c789929716D677382Df22C05Cb6", "roninl1CCIPOnRamp": "0xdC5b578ff3AFcC4A4a6E149892b9472390b50844", "roninl1NativeBridge": "0x64192819Ac13Ef72bF6b5AE239AC672B43a9AF08" } \ No newline at end of file diff --git a/deployments/mainnet/usdt/migrations/1735299664_upgrade_to_capo_price_feeds.ts b/deployments/mainnet/usdt/migrations/1735299664_upgrade_to_capo_price_feeds.ts index 399fe8401..e724eec36 100644 --- a/deployments/mainnet/usdt/migrations/1735299664_upgrade_to_capo_price_feeds.ts +++ b/deployments/mainnet/usdt/migrations/1735299664_upgrade_to_capo_price_feeds.ts @@ -1,15 +1,10 @@ import { expect } from 'chai'; import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; import { migration } from '../../../../plugins/deployment_manager/Migration'; -import { proposal } from '../../../../src/deploy'; -import { Numeric } from '../../../../test/helpers'; +import { proposal, exp } from '../../../../src/deploy'; import { IWstETH, IRateProvider, AggregatorV3Interface } from '../../../../build/types'; import { constants } from 'ethers'; -export function exp(i: number, d: Numeric = 0, r: Numeric = 6): bigint { - return (BigInt(Math.floor(i * 10 ** Number(r))) * 10n ** BigInt(d)) / 10n ** BigInt(r); -} - const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; const ETH_USD_SVR_PRICE_FEED = '0xc0053f3FBcCD593758258334Dfce24C2A9A673aD'; @@ -262,11 +257,11 @@ export default migration('1735299664_upgrade_to_capo_price_feeds', { This proposal updates existing price feeds for wstETH, sFRAX, weETH, WBTC, WETH, mETH, COMP, and LINK on the USDT market on Mainnet. -SVR summery +## SVR summary [RFP process](https://www.comp.xyz/t/oev-rfp-process-update-july-2025/6945) and community [vote](https://snapshot.box/#/s:comp-vote.eth/proposal/0x98a3873319cdb5a4c66b6f862752bdcfb40d443a5b9c2f9472188d7ed5f9f2e0) passed and decided to implement Chainlink's SVR solution for Mainnet markets, this proposal updates wstETH, WBTC, WETH, LINK, weETH, mETH, COMP price feeds to support SVR implementations. -CAPO summery +## CAPO summary CAPO is a price oracle adapter designed to support assets that grow gradually relative to a base asset - such as liquid staking tokens that accumulate yield over time. It provides a mechanism to track this expected growth while protecting downstream protocol from sudden or manipulated price spikes. wstETH, sFRAX, weETH, mETH price feeds are updated to their CAPO implementations. @@ -307,7 +302,7 @@ The ninth action deploys and upgrades Comet to a new version. }, async enacted(): Promise { - return false; + return true; }, async verify(deploymentManager: DeploymentManager) { diff --git a/deployments/relations.ts b/deployments/relations.ts index c7b823b32..b48345c0b 100644 --- a/deployments/relations.ts +++ b/deployments/relations.ts @@ -59,6 +59,12 @@ const relationConfigMap: RelationConfigMap = { if (address === '0xd09acb80c1e8f2291862c4978a008791c9167003') { return 'tETH'; } + if (address === '0x5a7facb970d094b6c7ff1df0ea68d99e6e73cbff') { + return 'weETH'; + } + if (address.toLowerCase() === '0x87eee96d50fb761ad85b1c982d28a042169d61b1') { + return 'wrsETH'; + } throw new Error(`Failed to get symbol for token ${token.address}: ${e.message}`); } @@ -85,6 +91,12 @@ const relationConfigMap: RelationConfigMap = { if (address === '0xd09acb80c1e8f2291862c4978a008791c9167003') { return 'tETH:priceFeed'; } + if (address === '0x5a7facb970d094b6c7ff1df0ea68d99e6e73cbff') { + return 'weETH:priceFeed'; + } + if (address === '0x87eee96d50fb761ad85b1c982d28a042169d61b1') { + return 'wrsETH:priceFeed'; + } throw new Error(`Failed to get symbol for token ${assets[i].address}: ${e.message}`); } diff --git a/deployments/ronin/weth/deploy.ts b/deployments/ronin/weth/deploy.ts index 06bebf97e..383b9b10a 100644 --- a/deployments/ronin/weth/deploy.ts +++ b/deployments/ronin/weth/deploy.ts @@ -73,6 +73,11 @@ async function deployContracts( 'ronin' ); + const l2CCIPOnRamp = await deploymentManager.existing( + 'l2CCIPOnRamp', + '0x02b60267bceeaFDC45005e0Fa0dd783eFeBc9F1b', + 'ronin' + ); // Deploy Local Timelock const localTimelock = await deploymentManager.deploy( @@ -195,6 +200,7 @@ async function deployContracts( bridgeReceiver, l2CCIPRouter, l2CCIPOffRamp, + l2CCIPOnRamp, roninl2NativeBridge, bulker, // COMP diff --git a/deployments/ronin/weth/roots.json b/deployments/ronin/weth/roots.json index 7756d849c..86fa59c10 100644 --- a/deployments/ronin/weth/roots.json +++ b/deployments/ronin/weth/roots.json @@ -6,6 +6,7 @@ "bridgeReceiver": "0x2c7EfA766338D33B9192dB1fB5D170Bdc03ef3F9", "l2CCIPRouter": "0x46527571D5D1B68eE7Eb60B18A32e6C60DcEAf99", "l2CCIPOffRamp": "0x320A10449556388503Fd71D74A16AB52e0BD1dEb", + "l2CCIPOnRamp": "0x02b60267bceeaFDC45005e0Fa0dd783eFeBc9F1b", "roninl2NativeBridge": "0x0cf8ff40a508bdbc39fbe1bb679dcba64e65c7df", "bulker": "0x840281FaD56DD88afba052B7F18Be2A65796Ecc6", "l2TokenAdminRegistry": "0x90e83d532A4aD13940139c8ACE0B93b0DdbD323a" diff --git a/deployments/ronin/wron/deploy.ts b/deployments/ronin/wron/deploy.ts index eec9022bb..1aae0a786 100644 --- a/deployments/ronin/wron/deploy.ts +++ b/deployments/ronin/wron/deploy.ts @@ -79,6 +79,12 @@ async function deployContracts( 'ronin' ); + const l2CCIPOnRamp = await deploymentManager.existing( + 'l2CCIPOnRamp', + '0x02b60267bceeaFDC45005e0Fa0dd783eFeBc9F1b', + 'ronin' + ); + // Deploy all Comet-related contracts const deployed = await deployComet(deploymentManager, deploySpec, {}, true); @@ -86,6 +92,7 @@ async function deployContracts( ...deployed, bridgeReceiver, l2CCIPRouter, + l2CCIPOnRamp, l2CCIPOffRamp, l2TokenAdminRegistry, bulker diff --git a/deployments/ronin/wron/roots.json b/deployments/ronin/wron/roots.json index 13f7b0b98..a616419a3 100644 --- a/deployments/ronin/wron/roots.json +++ b/deployments/ronin/wron/roots.json @@ -5,6 +5,7 @@ "cometFactory": "0x4DF9E0f8e94a7A8A9aEa6010CD9d341F8Ecfe4c6", "bridgeReceiver": "0x2c7EfA766338D33B9192dB1fB5D170Bdc03ef3F9", "l2CCIPRouter": "0x46527571D5D1B68eE7Eb60B18A32e6C60DcEAf99", + "l2CCIPOnRamp": "0x02b60267bceeaFDC45005e0Fa0dd783eFeBc9F1b", "l2CCIPOffRamp": "0x320A10449556388503Fd71D74A16AB52e0BD1dEb", "roninl2NativeBridge": "0x0cf8ff40a508bdbc39fbe1bb679dcba64e65c7df", "l2TokenAdminRegistry": "0x90e83d532A4aD13940139c8ACE0B93b0DdbD323a", diff --git a/forge/script/marketupdates/helpers/GovernanceHelper.sol b/forge/script/marketupdates/helpers/GovernanceHelper.sol index eaaf77422..f56257cfb 100644 --- a/forge/script/marketupdates/helpers/GovernanceHelper.sol +++ b/forge/script/marketupdates/helpers/GovernanceHelper.sol @@ -210,7 +210,7 @@ library GovernanceHelper { } function voteOnProposal(Vm vm, uint256 proposalId, address proposalCreator) public { - address[12] memory voters = getTopDelegates(); + address[11] memory voters = getTopDelegates(); console.log("Voting on proposal with ID: ", proposalId); console.log("Proposal Creator: ", proposalCreator); @@ -225,12 +225,11 @@ library GovernanceHelper { } } - function getTopDelegates() public pure returns (address[12] memory) { + function getTopDelegates() public pure returns (address[11] memory) { return [ 0x070341aA5Ed571f0FB2c4a5641409B1A46b4961b, 0x0579A616689f7ed748dC07692A3F150D44b0CA09, - 0x9AA835Bc7b8cE13B9B0C9764A52FbF71AC62cCF1, - 0x7E959eAB54932f5cFd10239160a7fd6474171318, + 0x66cD62c6F8A4BB0Cd8720488BCBd1A6221B765F9, 0x2210dc066aacB03C9676C4F1b36084Af14cCd02E, 0x88F659b4B6D5614B991c6404b34f821e10390eC0, 0xb06DF4dD01a5c5782f360aDA9345C87E86ADAe3D, diff --git a/hardhat.config.ts b/hardhat.config.ts index 63d2caae8..9c6330826 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -3,7 +3,6 @@ import 'dotenv/config'; import { HardhatUserConfig, subtask, task } from 'hardhat/config'; import '@compound-finance/hardhat-import'; import '@nomiclabs/hardhat-etherscan'; -import '@tenderly/hardhat-tenderly'; import '@nomiclabs/hardhat-ethers'; import '@typechain/hardhat'; import 'hardhat-chai-matchers'; @@ -198,7 +197,7 @@ export const networkConfigs: NetworkConfig[] = [ { network: 'scroll', chainId: 534352, - url: 'https://rpc.scroll.io', + url: 'https://scroll.drpc.org', }, ]; @@ -342,6 +341,7 @@ const config: HardhatUserConfig = { hardforkHistory: { berlin: 1, london: 2, + shanghai: 3, } }; return acc; @@ -703,13 +703,6 @@ const config: HardhatUserConfig = { ], }, - tenderly: { - project: 'comet', - username: process.env.TENDERLY_USERNAME || '', - accessKey: process.env.TENDERLY_ACCESS_KEY || '', - privateVerification: false, - }, - mocha: { reporter: 'mocha-multi-reporters', reporterOptions: { diff --git a/plugins/deployment_manager/DeploymentManager.ts b/plugins/deployment_manager/DeploymentManager.ts index 2bcfc2357..a72836dc7 100644 --- a/plugins/deployment_manager/DeploymentManager.ts +++ b/plugins/deployment_manager/DeploymentManager.ts @@ -51,6 +51,7 @@ export class DeploymentManager { cache: Cache; // TODO: kind of a misnomer since its handling *all* path stuff contractsCache: ContractMap | null; _signers: SignerWithAddress[]; + bridgedDeploymentManagers: Map = new Map(); constructor( network: string, @@ -75,6 +76,19 @@ export class DeploymentManager { this._signers = []; } + async addBridgedDeploymentManager(network: string, deployment: string, hre?: HardhatRuntimeEnvironment): Promise { + const key = `${network}:${deployment}`; + if (!this.bridgedDeploymentManagers.has(key)) { + if(!hre && this.network !== network) { + throw new Error(`Must provide hre to bridge to a different network deployment manager`); + } + const dm = new DeploymentManager(network, deployment, hre ?? this.hre, { writeCacheToDisk: true }); + await dm.spider(); + this.bridgedDeploymentManagers.set(key, dm); + } + return this.bridgedDeploymentManagers.get(key)!; + } + async getSigners(): Promise { if (this._signers.length > 0) { return this._signers; @@ -282,7 +296,7 @@ export class DeploymentManager { } } - stashRelayMessage(messanger: string, callData: string, signer: string) { + stashRelayMessage(messenger: string, callData: string, signer: string) { try { const cacheDir = path.resolve(__dirname, '../..', 'cache'); mkdirSync(cacheDir, { recursive: true }); @@ -301,7 +315,7 @@ export class DeploymentManager { } } - const newEntry = { messanger, callData, signer }; + const newEntry = { messenger, callData, signer }; if (!data.some(entry => JSON.stringify(entry) === JSON.stringify(newEntry))) { data.push(newEntry); writeFileSync(file, JSON.stringify(data, null, 2), 'utf8'); diff --git a/plugins/deployment_manager/Import.ts b/plugins/deployment_manager/Import.ts index 2431acc89..1c3fe593e 100644 --- a/plugins/deployment_manager/Import.ts +++ b/plugins/deployment_manager/Import.ts @@ -44,7 +44,7 @@ export async function importContract( console.warn(`Import failed for ${network}@${address} (${e.message}), retrying in ${retryDelay / 1000}s; ${retries} retries left`); await new Promise(ok => setTimeout(ok, retryDelay)); - return importContract(network, address, retries - 1, retryDelay * 2); + return importContract(network, address, retries - 1, retryDelay * 2 > 10000 ? 10000 : retryDelay * 2); } } @@ -61,7 +61,7 @@ export async function importContract( console.warn(`Import failed for ${network}@${address} (${e.message}), retrying in ${retryDelay / 1000}s; ${retries} retries left`); await new Promise(ok => setTimeout(ok, retryDelay)); - return importContract(network, address, retries - 1, retryDelay * 2); + return importContract(network, address, retries - 1, retryDelay * 2 > 10000 ? 10000 : retryDelay * 2); } } diff --git a/plugins/scenario/utils/hreForBase.ts b/plugins/scenario/utils/hreForBase.ts index 2328074c7..ee47241bf 100644 --- a/plugins/scenario/utils/hreForBase.ts +++ b/plugins/scenario/utils/hreForBase.ts @@ -9,6 +9,7 @@ import { Environment } from 'hardhat/internal/core/runtime-environment'; import { ForkSpec } from '../World'; import { HttpNetworkUserConfig } from 'hardhat/types'; import { EthereumProvider } from 'hardhat/types/provider'; +import { networkConfigs } from '../../../hardhat.config'; /* mimics https://github.com/nomiclabs/hardhat/blob/master/packages/hardhat-core/src/internal/lib/hardhat-lib.ts @@ -91,6 +92,12 @@ function getBlockRollback(base: ForkSpec) { return 25; } +let activeMigration = false; + +export function migrationStarted() { + activeMigration = true; +} + export async function forkedHreForBase(base: ForkSpec): Promise { const ctx: HardhatContext = HardhatContext.getHardhatContext(); @@ -103,16 +110,21 @@ export async function forkedHreForBase(base: ForkSpec): Promise { + if (activeMigration){ + return networkConfigs.find(c => c.network === base.network)?.url; + } + return baseNetwork.url; + })(); + const provider = new ethers.providers.JsonRpcProvider(providerUrl); + if(providerUrl) console.log(`Forking from network: ${base.network} at block number: ${await provider.getBlockNumber() - (getBlockRollback(base) || 0)}`); // noNetwork otherwise - if (!base.blockNumber && baseNetwork.url && getBlockRollback(base) !== undefined) + if (!base.blockNumber && providerUrl && getBlockRollback(base) !== undefined) base.blockNumber = await provider.getBlockNumber() - getBlockRollback(base); // arbitrary number of blocks to go back if (getBlockRollback(base) === 0) { - const provider = new ethers.providers.JsonRpcProvider(baseNetwork.url); const block = await provider.getBlockNumber(); base.blockNumber = block - 1; } @@ -126,7 +138,7 @@ export async function forkedHreForBase(base: ForkSpec): Promise user can end up with a minted supply', { - filter: async (ctx) => !matchesDeployment(ctx, [{ network: 'base', deployment: 'usds' }]), + filter: async (ctx) => !matchesDeployment(ctx, [ + { network: 'base', deployment: 'usds' }, + { network: 'ronin' }, + ]), tokenBalances: async (ctx) => ( { $comet: { diff --git a/scenario/SupplyScenario.ts b/scenario/SupplyScenario.ts index be3be8537..10a02a33f 100644 --- a/scenario/SupplyScenario.ts +++ b/scenario/SupplyScenario.ts @@ -406,12 +406,20 @@ scenario( scenario( 'Comet#supplyFrom > repay borrow', { - tokenBalances: { - albert: { $base: 1010 } - }, - cometBalances: { - betty: { $base: '<= -1000' } // in units of asset, not wei - }, + tokenBalances: async (ctx) => ( + { + albert: { + $base: getConfigForScenario(ctx).supplyBase + (0.01 * getConfigForScenario(ctx).supplyBase) + } + } + ), + cometBalances: async (ctx) => ( + { + betty: { + $base: `<= -${getConfigForScenario(ctx).supplyBase}` + } + } + ), }, async ({ comet, actors }, context) => { const { albert, betty } = actors; diff --git a/scenario/TransferScenario.ts b/scenario/TransferScenario.ts index e5c94310b..0039b9601 100644 --- a/scenario/TransferScenario.ts +++ b/scenario/TransferScenario.ts @@ -180,32 +180,35 @@ scenario( scenario( 'Comet#transferFrom > withdraw to repay', { - cometBalances: { - albert: { $base: 1000, $asset0: 50 }, // in units of asset, not wei - betty: { $base: -1000 }, - charles: { $base: 1000 }, // to give the protocol enough base for others to borrow from - }, + cometBalances: async (ctx) => ( + { + albert: { $base: getConfigForScenario(ctx).transferBase, $asset0: getConfigForScenario(ctx).transferAsset2 }, // in units of asset, not wei + betty: { $base: -getConfigForScenario(ctx).transferBase }, + charles: { $base: getConfigForScenario(ctx).transferBase }, // to give the protocol enough base for others to borrow from + } + ), }, async ({ comet, actors }, context) => { const { albert, betty } = actors; const baseAssetAddress = await comet.baseToken(); const baseAsset = context.getAssetByAddress(baseAssetAddress); const scale = (await comet.baseScale()).toBigInt(); + const amountTransferred = BigInt(getConfigForScenario(context).transferBase) * scale; const utilization = await comet.getUtilization(); const borrowRate = (await comet.getBorrowRate(utilization)).toBigInt(); // XXX 70 seconds?! - expectApproximately(await albert.getCometBaseBalance(), 1000n * scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); - expectApproximately(await betty.getCometBaseBalance(), -1000n * scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await albert.getCometBaseBalance(), amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await betty.getCometBaseBalance(), -amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); await albert.allow(betty, true); // Betty withdraws from Albert to repay her own borrows - const toTransfer = 999n * scale; // XXX cannot withdraw 1000 (to ~0) + const toTransfer = amountTransferred - scale; // XXX cannot withdraw 1000 (to ~0) const txn = await betty.transferAssetFrom({ src: albert.address, dst: betty.address, asset: baseAsset.address, amount: toTransfer }); - expectApproximately(await albert.getCometBaseBalance(), scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); - expectApproximately(await betty.getCometBaseBalance(), -scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await albert.getCometBaseBalance(), scale, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await betty.getCometBaseBalance(), -scale, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); return txn; // return txn to measure gas } @@ -214,26 +217,29 @@ scenario( scenario( 'Comet#transfer base reverts if undercollateralized', { - cometBalances: { - albert: { $base: 1000, $asset0: 0.000001 }, // in units of asset, not wei - betty: { $base: -1000 }, - charles: { $base: 1000 }, // to give the protocol enough base for others to borrow from - }, + cometBalances: async (ctx) => ( + { + albert: { $base: getConfigForScenario(ctx).transferBase, $asset0: 0.000001 }, // in units of asset, not wei + betty: { $base: -getConfigForScenario(ctx).transferBase }, + charles: { $base: getConfigForScenario(ctx).transferBase }, // to give the protocol enough base for others to borrow from + } + ), }, async ({ comet, actors }, context) => { const { albert, betty } = actors; const baseAssetAddress = await comet.baseToken(); const baseAsset = context.getAssetByAddress(baseAssetAddress); const scale = (await comet.baseScale()).toBigInt(); + const amountTransferred = BigInt(getConfigForScenario(context).transferBase) * scale; const utilization = await comet.getUtilization(); const borrowRate = (await comet.getBorrowRate(utilization)).toBigInt(); // XXX 100 seconds?! - expectApproximately(await albert.getCometBaseBalance(), 1000n * scale, getInterest(1000n * scale, borrowRate, 100n) + 2n); - expectApproximately(await betty.getCometBaseBalance(), -1000n * scale, getInterest(1000n * scale, borrowRate, 100n) + 2n); + expectApproximately(await albert.getCometBaseBalance(), amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await betty.getCometBaseBalance(), -amountTransferred, getInterest(amountTransferred, borrowRate, 100n) + 2n); // Albert with positive balance transfers to Betty with negative balance - const toTransfer = 2001n * scale; // XXX min borrow... + const toTransfer = 2n*amountTransferred + scale; // XXX min borrow... await expectRevertCustom( albert.transferAsset({ dst: betty.address, @@ -248,28 +254,31 @@ scenario( scenario( 'Comet#transferFrom base reverts if undercollateralized', { - cometBalances: { - albert: { $base: 1000, $asset0: 0.000001 }, // in units of asset, not wei - betty: { $base: -1000 }, - charles: { $base: 1000 }, // to give the protocol enough base for others to borrow from - }, + cometBalances: async (ctx) => ( + { + albert: { $base: getConfigForScenario(ctx).transferBase, $asset0: 0.000001 }, // in units of asset, not wei + betty: { $base: -getConfigForScenario(ctx).transferBase }, + charles: { $base: getConfigForScenario(ctx).transferBase }, // to give the protocol enough base for others to borrow from + } + ), }, async ({ comet, actors }, context) => { const { albert, betty } = actors; const baseAssetAddress = await comet.baseToken(); const baseAsset = context.getAssetByAddress(baseAssetAddress); const scale = (await comet.baseScale()).toBigInt(); + const amountTransferred = BigInt(getConfigForScenario(context).transferBase) * scale; const utilization = await comet.getUtilization(); const borrowRate = (await comet.getBorrowRate(utilization)).toBigInt(); // XXX 70 seconds?! - expectApproximately(await albert.getCometBaseBalance(), 1000n * scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); - expectApproximately(await betty.getCometBaseBalance(), -1000n * scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await albert.getCometBaseBalance(), amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await betty.getCometBaseBalance(), -amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); await albert.allow(betty, true); // Albert with positive balance transfers to Betty with negative balance - const toTransfer = 2001n * scale; // XXX min borrow... + const toTransfer = 2n*amountTransferred + scale; // XXX min borrow... await expectRevertCustom( betty.transferAssetFrom({ src: albert.address, diff --git a/scenario/constraints/ProposalConstraint.ts b/scenario/constraints/ProposalConstraint.ts index df76002d9..94d8d7f03 100644 --- a/scenario/constraints/ProposalConstraint.ts +++ b/scenario/constraints/ProposalConstraint.ts @@ -3,7 +3,7 @@ import { IGovernorBravo, ProposalState, OpenProposal } from '../context/Gov'; import { CometContext } from '../context/CometContext'; import { fetchLogs } from '../utils'; import { DeploymentManager } from '../../plugins/deployment_manager'; -import { isBridgedDeployment, executeOpenProposal, voteForOpenProposal, executeOpenProposalAndRelay } from '../utils'; +import { isBridgedDeployment, voteForOpenProposal, executeOpenProposalAndRelay } from '../utils'; import { getOpenBridgedProposals, executeBridgedProposal } from '../utils/bridgeProposal'; export async function getOpenProposals(deploymentManager: DeploymentManager, governor: IGovernorBravo): Promise { @@ -78,24 +78,20 @@ export class ProposalConstraint implements StaticConstra ); } - // temporary hack to skip proposal 510 - if (proposal.id.eq(510)) { - console.log('Skipping proposal 510'); + // temporary hack to skip proposal 519 + if (proposal.id.eq(519)) { + console.log('Skipping proposal 519'); continue; } try { // Execute the proposal debug(`${label} Processing pending proposal ${proposal.id}`); - if (isBridged) { - await executeOpenProposalAndRelay( - governanceDeploymentManager, - ctx.world.deploymentManager, - proposal - ); - } else { - await executeOpenProposal(governanceDeploymentManager, proposal); - } + await executeOpenProposalAndRelay( + governanceDeploymentManager, + ctx.world.deploymentManager, + proposal + ); debug(`${label} Open proposal ${proposal.id} was executed`); } catch (err) { debug(`${label} Failed to execute proposal ${proposal.id}`, err.message); diff --git a/scenario/utils/index.ts b/scenario/utils/index.ts index 410d7c318..e51fd7508 100644 --- a/scenario/utils/index.ts +++ b/scenario/utils/index.ts @@ -12,7 +12,7 @@ import { utils, } from 'ethers'; import { execSync } from 'child_process'; -import { existsSync } from 'fs'; +import { existsSync, unlinkSync } from 'fs'; import { CometContext } from '../context/CometContext'; import CometAsset from '../context/CometAsset'; import { exp } from '../../test/helpers'; @@ -488,172 +488,213 @@ async function redeployRenzoOracle(dm: DeploymentManager) { } } -const tokens = new Map([ - ['WETH', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'], - ['LINK', '0x514910771AF9Ca656af840dff83E8264EcF986CA'], +const tokens = [ + ['mainnet', 'WETH', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'], + ['mainnet', 'LINK', '0x514910771AF9Ca656af840dff83E8264EcF986CA'], + ['mainnet', 'GHO', '0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f'], + ['ronin', 'WETH', '0xc99a6a985ed2cac1ef41640596c5a5f9f4e19ef5'], + ['ronin', 'WRON', '0xe514d9deb7966c8be0ca922de8a064264ea6bcd4'], + ['ronin', 'LINK', '0x3902228d6a3d2dc44731fd9d45fee6a61c722d0b'], +]; + +const dest = new Map([ + ['ronin', '6916147374840168594'], + ['mainnet', '5009297550715157269'], ]); -const dest = new Map([['ronin', '6916147374840168594']]); - -async function updateCCIPStats(dm: DeploymentManager) { - if (dm.network === 'mainnet') { - const commitStore = '0x2aa101bf99caef7fc1355d4c493a1fe187a007ce'; +export async function updateCCIPStats( + dm: DeploymentManager, + tenderlyLogs?: any[] +) { + const config = [ + { + network: 'mainnet', + commitStore: '0x2aa101bf99caef7fc1355d4c493a1fe187a007ce', + priceRegistry: '0x8c9b2Efb7c64C394119270bfecE7f54763b958Ad' + }, + { + network: 'ronin', + commitStore: '0x28c66d9693b2634b2f3b170f6d9584eec2f72ff0', + priceRegistry: '0xefCEa3CFA330adcDdeCe99219C57fd45cd166ac1' + } + ]; + const { commitStore, priceRegistry } = config.find(c => c.network === dm.network) || {}; + if (!commitStore || !priceRegistry) { + console.log(`No CCIP config for network ${dm.network}, skipping CCIP stats update.`); + return; + } + const abi = [ + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'sourceToken', + type: 'address', + }, + { + internalType: 'uint224', + name: 'usdPerToken', + type: 'uint224', + }, + ], + internalType: 'struct TokenPriceUpdate[]', + name: 'tokenPriceUpdates', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'uint64', + name: 'destChainSelector', + type: 'uint64', + }, + { + internalType: 'uint224', + name: 'usdPerUnitGas', + type: 'uint224', + }, + ], + internalType: 'struct GasPriceUpdate[]', + name: 'gasPriceUpdates', + type: 'tuple[]', + }, + ], + internalType: 'struct PriceUpdates', + name: 'priceUpdates', + type: 'tuple', + }, + ], + name: 'updatePrices', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'destChainSelector', + type: 'uint64', + }, + ], + name: 'getDestinationChainGasPrice', + outputs: [ + { + components: [ + { + internalType: 'uint224', + name: 'value', + type: 'uint224', + }, + { + internalType: 'uint32', + name: 'timestamp', + type: 'uint32', + }, + ], + internalType: 'struct TimestampedPackedUint224', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + ], + name: 'getTokenPrice', + outputs: [ + { + components: [ + { + internalType: 'uint224', + name: 'value', + type: 'uint224', + }, + { + internalType: 'uint32', + name: 'timestamp', + type: 'uint32', + }, + ], + internalType: 'struct TimestampedPackedUint224', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + ]; - const priceRegistry = '0x8c9b2Efb7c64C394119270bfecE7f54763b958Ad'; - const abi = [ - { - inputs: [ - { - components: [ - { - components: [ - { - internalType: 'address', - name: 'sourceToken', - type: 'address', - }, - { - internalType: 'uint224', - name: 'usdPerToken', - type: 'uint224', - }, - ], - internalType: 'struct TokenPriceUpdate[]', - name: 'tokenPriceUpdates', - type: 'tuple[]', - }, - { - components: [ - { - internalType: 'uint64', - name: 'destChainSelector', - type: 'uint64', - }, - { - internalType: 'uint224', - name: 'usdPerUnitGas', - type: 'uint224', - }, - ], - internalType: 'struct GasPriceUpdate[]', - name: 'gasPriceUpdates', - type: 'tuple[]', - }, - ], - internalType: 'struct PriceUpdates', - name: 'priceUpdates', - type: 'tuple', - }, - ], - name: 'updatePrices', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'destChainSelector', - type: 'uint64', - }, - ], - name: 'getDestinationChainGasPrice', - outputs: [ - { - components: [ - { - internalType: 'uint224', - name: 'value', - type: 'uint224', - }, - { - internalType: 'uint32', - name: 'timestamp', - type: 'uint32', - }, - ], - internalType: 'struct TimestampedPackedUint224', - name: '', - type: 'tuple', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'token', - type: 'address', - }, - ], - name: 'getTokenPrice', - outputs: [ - { - components: [ - { - internalType: 'uint224', - name: 'value', - type: 'uint224', - }, - { - internalType: 'uint32', - name: 'timestamp', - type: 'uint32', - }, - ], - internalType: 'struct TimestampedPackedUint224', - name: '', - type: 'tuple', - }, - ], - stateMutability: 'view', - type: 'function', - }, - ]; + await dm.hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [commitStore], + }); - await dm.hre.network.provider.request({ - method: 'hardhat_impersonateAccount', - params: [commitStore], - }); + await dm.hre.network.provider.request({ + method: 'hardhat_setBalance', + params: [commitStore, '0x56bc75e2d63100000'], + }); + const commitStoreSigner = await dm.hre.ethers.getSigner(commitStore); - await dm.hre.network.provider.request({ - method: 'hardhat_setBalance', - params: [commitStore, '0x56bc75e2d63100000'], - }); - const commitStoreSigner = await dm.hre.ethers.getSigner(commitStore); + const registryContract = new Contract( + priceRegistry, + abi, + dm.hre.ethers.provider + ); - const registryContract = new Contract( - priceRegistry, - abi, - dm.hre.ethers.provider - ); + const tokenPrices = []; + const gasPrices = []; + for (const [network,, address] of tokens) { + if(network !== dm.network) continue; + const price = await registryContract.getTokenPrice(address); + tokenPrices.push([address, price.value]); + } - const tokenPrices = []; - const gasPrices = []; - for (const [, address] of tokens) { - const price = await registryContract.getTokenPrice(address); - tokenPrices.push([address, price.value]); - } - for (const [, address] of dest) { - const price = await registryContract.getDestinationChainGasPrice(address); - gasPrices.push([address, price.value]); + for (const [, chainSelector] of dest) { + try { + const price = await registryContract.getDestinationChainGasPrice(chainSelector); + gasPrices.push([chainSelector, price.value]); + } catch (e) { + continue; } + } - const tx0 = await commitStoreSigner.sendTransaction({ - to: priceRegistry, - data: registryContract.interface.encodeFunctionData('updatePrices', [ + if(tenderlyLogs) { + dm.stashRelayMessage( + priceRegistry, + registryContract.interface.encodeFunctionData('updatePrices', [ { tokenPriceUpdates: tokenPrices, gasPriceUpdates: gasPrices, }, ]), - }); - - await tx0.wait(); + commitStore + ); } + + const tx0 = await commitStoreSigner.sendTransaction({ + to: priceRegistry, + data: registryContract.interface.encodeFunctionData('updatePrices', [ + { + tokenPriceUpdates: tokenPrices, + gasPriceUpdates: gasPrices, + }, + ]), + }); + + await tx0.wait(); } const REDSTONE_FEEDS = { @@ -871,8 +912,6 @@ export async function tenderlyExecute( }, ]; - const chainId2 = bdm.hre.ethers.provider.network.chainId; - console.log(`\n========================== TENDERLY ==========================\n`); console.log(`\nExecuting Tenderly simulation for proposal ${id}...`); @@ -885,58 +924,68 @@ export async function tenderlyExecute( console.log(` >>> PROPOSAL EXECUTED ${id} \n`); console.log(`Simulation ${exec1.id} done, status: ${exec1.status}`); console.log(`Link: https://www.tdly.co/shared/simulation/${exec1.id}`); - let proposals; - if (chainId1 !== chainId2) { - proposals = await relayMessage(gdm, bdm, parseFloat(B0.toString()), bundle[bundle.length - 1].transaction.transaction_info.logs); - - debug(`Proposals relayed: ${proposals.length}`); - const timelockL2 = await bdm.getContractOrThrow('timelock'); - const delay = await timelockL2.delay(); - const relayMessages = loadCachedRelayMessages(); - const latestL2 = await bdm.hre.ethers.provider.getBlock('latest'); - const maxEta = Math.max(...proposals.map(p => Number(p.eta || 0))) + delay.toNumber(); - const T0L2 = BigInt(Math.max(latestL2.timestamp, maxEta + 1)); - const B0L2 = Number(latestL2.number) + 1; - const simsL2 = relayMessages.map((msg, i, arr) => { - const isLast = i === arr.length - 1; - - const timestamp = isLast - ? Number(T0L2) - : latestL2.timestamp; - - const block = isLast - ? B0L2 : latestL2.number; - - return { - network_id: chainId2.toString(), - from: msg.signer, - to: msg.messanger, - block_number: Number(block), - block_header: { - timestamp: gdm.hre.ethers.utils.hexlify(Number(timestamp)) - }, - input: msg.callData, - save: true, - save_if_fails: true, - gas_price: 0, - }; - }); - - while (!simsL1[0]) { - simsL1.shift(); - if (simsL1.length == 0) { - break; - } + const bdms = [bdm]; + for (const dm of gdm.bridgedDeploymentManagers.values()) { + if (!bdms.includes(dm)) { + bdms.push(dm); } + } + + for (const currentBdm of bdms) { + const chainId2 = currentBdm.hre.ethers.provider.network.chainId; + let proposals; + if (chainId1 !== chainId2) { + const relayPath = path.resolve(__dirname, '../../cache/relay.json'); + if (existsSync(relayPath)) unlinkSync(relayPath); - if (simsL2.length > 0) { - const bundle2 = await simulateBundle(bdm, simsL2, Number(B0L2)); - console.log(` >>> PROPOSAL RELAYED ${id} \n`); - const sim = bundle2[bundle2.length - 1]; - await shareSimulation(bdm, sim.simulation.id); - console.log(`Simulation ${sim.simulation.id} done, status: ${sim.simulation.status}`); - console.log(`Link: https://www.tdly.co/shared/simulation/${sim.simulation.id}`); + proposals = await relayMessage(gdm, currentBdm, parseFloat(B0.toString()), bundle[bundle.length - 1].transaction.transaction_info.logs); + + debug(`Proposals relayed to ${currentBdm.network}: ${proposals?.length ?? 0}`); + + if (proposals && proposals.length > 0) { + const timelockL2 = await currentBdm.getContractOrThrow('timelock'); + const delay = await timelockL2.delay(); + const relayMessages = loadCachedRelayMessages(); + const latestL2 = await currentBdm.hre.ethers.provider.getBlock('latest'); + const maxEta = Math.max(...proposals.map(p => Number(p.eta || 0))) + delay.toNumber(); + const T0L2 = BigInt(Math.max(latestL2.timestamp, maxEta + 1)); + const B0L2 = Number(latestL2.number) + 1; + + const simsL2 = relayMessages.map((msg, i, arr) => { + const isLast = i === arr.length - 1; + + const timestamp = isLast + ? Number(T0L2) + : latestL2.timestamp; + + const block = isLast + ? B0L2 : latestL2.number; + + return { + network_id: chainId2.toString(), + from: msg.signer, + to: msg.messenger, + block_number: Number(block), + block_header: { + timestamp: currentBdm.hre.ethers.utils.hexlify(Number(timestamp)) + }, + input: msg.callData, + save: true, + save_if_fails: true, + gas_price: 0, + }; + }); + + if (simsL2.length > 0) { + const bundle2 = await simulateBundle(currentBdm, simsL2, Number(B0L2)); + const sim = bundle2[bundle2.length - 1]; + await shareSimulation(currentBdm, sim.simulation.id); + console.log(`\nRelayed to ${currentBdm.network}`); + console.log(`Simulation ${sim.simulation.id} done, status: ${sim.simulation.status}`); + console.log(`Link: https://www.tdly.co/shared/simulation/${sim.simulation.id}`); + } + } } } @@ -948,29 +997,74 @@ async function simulateBundle( simulations: any[], blockNumber: number = 0 ): Promise { - const { username, project, accessKey } = (dm.hre.config as any).tenderly; - const body = { - simulations, - block_number: blockNumber, - simulation_type: 'full', - save: true, - }; + const rollingStateChanges = {}; + const results = []; + + for (const sim of simulations) { + const project = 'comet'; + const username = process.env.TENDERLY_USERNAME || ''; + const accessKey = process.env.TENDERLY_ACCESS_KEY || ''; + + // Merge rolling state changes with simulation's own state_objects + const stateObjects = sim.state_objects + ? { ...rollingStateChanges, ...sim.state_objects } + : rollingStateChanges; + + const body = { + simulations: [{ + ...sim, + state_objects: stateObjects, + block_number: sim.block_number || blockNumber, + simulation_type: 'full', + save: true, + save_if_fails: true, + }] + }; - const result = await axios.post( - `https://api.tenderly.co/api/v1/account/${username}/project/${project}/simulate-bundle`, - body, - { - headers: { - 'X-Access-Key': accessKey, - 'Content-Type': 'application/json', - }, + const result = await axios.post( + `https://api.tenderly.co/api/v1/account/${username}/project/${project}/simulate-bundle`, + body, + { + headers: { + 'X-Access-Key': accessKey, + 'Content-Type': 'application/json', + }, + } + ); + + // Extract and accumulate state changes from state_diff + const simResult = result.data.simulation_results[0]; + if (simResult?.transaction?.transaction_info?.call_trace?.state_diff) { + const stateDiff = simResult.transaction.transaction_info.call_trace.state_diff; + + // state_diff is an array of objects with { address, raw: [...] } + for (const stateDiffEntry of stateDiff) { + const address = stateDiffEntry.address; + + if (!rollingStateChanges[address]) { + rollingStateChanges[address] = { storage: {} }; + } + + if (stateDiffEntry.raw && Array.isArray(stateDiffEntry.raw)) { + for (const change of stateDiffEntry.raw) { + // Each change has: { address, key, original, dirty } + rollingStateChanges[address].storage[change.key] = change.dirty; + } + } + } } - ); - return result.data.simulation_results; + + results.push(simResult); + } + + return results; } async function shareSimulation(dm: DeploymentManager, simulationId: string) { - const { username, project, accessKey } = (dm.hre.config as any).tenderly; + const project = 'comet'; + const username = process.env.TENDERLY_USERNAME || ''; + const accessKey = process.env.TENDERLY_ACCESS_KEY || ''; + return axios.post( `https://api.tenderly.co/api/v1/account/${username}/project/${project}/simulations/${simulationId}/share`, {}, @@ -1427,25 +1521,26 @@ export async function executeOpenProposalAndRelay( await governanceDeploymentManager.hre.ethers.provider.getBlockNumber(); await executeOpenProposal(governanceDeploymentManager, openProposal); console.log(`Executed proposal ${openProposal.id} on ${governanceDeploymentManager.network}, checking if relay to ${bridgeDeploymentManager.network} is needed...`); - await mockAllRedstoneOracles(bridgeDeploymentManager); console.log(`All Redstone oracles on ${bridgeDeploymentManager.network} are mocked`); - if ( - await isBridgeProposal( - governanceDeploymentManager, - bridgeDeploymentManager, - openProposal - ) - ) { - await relayMessage( - governanceDeploymentManager, - bridgeDeploymentManager, - startingBlockNumber - ); - } else { - console.log( - `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Proposal ${openProposal.id} doesn't target bridge; not relaying` - ); - return; + const bridgeManagers = await isBridgeProposal( + governanceDeploymentManager, + bridgeDeploymentManager, + openProposal + ); + for (const bridgeManager of bridgeManagers) { + await mockAllRedstoneOracles(bridgeManager); + if (bridgeManager) { + await relayMessage( + governanceDeploymentManager, + bridgeManager, + startingBlockNumber + ); + } else { + console.log( + `[${governanceDeploymentManager.network} -> ${bridgeManager.network}] Proposal ${openProposal.id} doesn't target bridge; not relaying` + ); + return; + } } } diff --git a/scenario/utils/isBridgeProposal.ts b/scenario/utils/isBridgeProposal.ts index f2d4ac362..7f3f71d2a 100644 --- a/scenario/utils/isBridgeProposal.ts +++ b/scenario/utils/isBridgeProposal.ts @@ -1,142 +1,281 @@ import { DeploymentManager } from '../../plugins/deployment_manager'; +import { getRoots } from '../../plugins/deployment_manager/Roots'; import { OpenProposal } from '../context/Gov'; +import { utils } from 'ethers'; +import { forkedHreForBase } from '../../plugins/scenario/utils/hreForBase'; + +const EXCLUDED_ROOTS = ['comptrollerV2', 'comet', 'configurator', 'rewards', 'bulker', 'cometFactory']; + +const CCTP_DOMAIN_TO_NETWORK: Record = { + 0: 'mainnet', + 1: 'avalanche', + 2: 'optimism', + 3: 'arbitrum', + 6: 'base', + 7: 'polygon', +}; + +const ROOT_TO_NETWORK: Record = { + fxRoot: 'polygon', + arbitrumInbox: 'arbitrum', + arbitrumL1GatewayRouter: 'arbitrum', + baseL1CrossDomainMessenger: 'base', + baseL1StandardBridge: 'base', + baseL1USDSBridge: 'base', + opL1CrossDomainMessenger: 'optimism', + opL1StandardBridge: 'optimism', + mantleL1CrossDomainMessenger: 'mantle', + mantleL1StandardBridge: 'mantle', + unichainL1CrossDomainMessenger: 'unichain', + unichainL1StandardBridge: 'unichain', + scrollMessenger: 'scroll', + scrollL1USDCGateway: 'scroll', + lineaMessageService: 'linea', + lineaL1TokenBridge: 'linea', + lineaL1USDCBridge: 'linea', + l1CCIPRouter: 'ronin', + l1TokenAdminRegistry: 'ronin', + roninl1CCIPOnRamp: 'ronin', + roninl1NativeBridge: 'ronin', +}; + +function parseCCTPNetworks(openProposal: OpenProposal, cctpAddress: string): string[] { + const networks: string[] = []; + const cctpLower = cctpAddress.toLowerCase(); + + for (let i = 0; i < openProposal.targets.length; i++) { + if (openProposal.targets[i].toLowerCase() !== cctpLower) continue; + const sig = openProposal.signatures[i]; + if (!sig.startsWith('depositForBurn(')) continue; + + const calldata = openProposal.calldatas[i]; + // destinationDomain is the second parameter (uint32) in all depositForBurn variants + const decoded = utils.defaultAbiCoder.decode(['uint256', 'uint32'], utils.hexDataSlice(calldata, 0, 64)); + const domain = decoded[1]; + const network = CCTP_DOMAIN_TO_NETWORK[domain]; + if (network) networks.push(network); + } + return networks; +} + +export async function getProposalBridgeNetworks( + governanceDeploymentManager: DeploymentManager, + openProposal: OpenProposal +): Promise { + const roots = await getRoots(governanceDeploymentManager.cache); + const targets = openProposal.targets.map(t => t.toLowerCase()); + + const networks = new Set(); + for (const [alias, address] of roots) { + if (EXCLUDED_ROOTS.includes(alias)) continue; + + if (alias === 'CCTPTokenMessenger' && targets.includes(address.toLowerCase())) { + for (const net of parseCCTPNetworks(openProposal, address)) { + networks.add(net); + } + continue; + } + + const network = ROOT_TO_NETWORK[alias]; + if (network && targets.includes(address.toLowerCase())) { + networks.add(network); + } + } + return [...new Set(networks)]; +} + +const existingBridgeManagers: Record = {}; export async function isBridgeProposal( governanceDeploymentManager: DeploymentManager, bridgeDeploymentManager: DeploymentManager, openProposal: OpenProposal ) { - const bridgeNetwork = bridgeDeploymentManager.network; - console.log(`Checking if proposal ${openProposal.id} is a bridge proposal on ${bridgeNetwork}`); - switch (bridgeNetwork) { - case 'arbitrum': { - const inbox = await governanceDeploymentManager.getContractOrThrow('arbitrumInbox'); - const l1GatewayRouter = await governanceDeploymentManager.getContractOrThrow( - 'arbitrumL1GatewayRouter' - ); - const targets = openProposal.targets; - return targets.includes(inbox.address) || targets.includes(l1GatewayRouter.address); - } - case 'polygon': { - const { - fxRoot, - RootChainManager - } = await governanceDeploymentManager.getContracts(); - const bridgeAddresses = [fxRoot, RootChainManager] - .filter(x => x) - .map(x => x.address.toLowerCase()); - const targets = openProposal.targets; - return targets.some(t => bridgeAddresses.includes(t.toLowerCase())); + const bridgeNetworks = await getProposalBridgeNetworks(governanceDeploymentManager, openProposal); + const otherBridgeNetworks = bridgeNetworks.filter(n => n !== bridgeDeploymentManager.network); + const bridgeManagers = [bridgeDeploymentManager]; + if (!existingBridgeManagers[bridgeDeploymentManager.network]) { + existingBridgeManagers[bridgeDeploymentManager.network] = bridgeDeploymentManager; + } + if (!existingBridgeManagers[governanceDeploymentManager.network]) { + existingBridgeManagers[governanceDeploymentManager.network] = governanceDeploymentManager; + } + for(const bridgeNetwork of otherBridgeNetworks) { + if (existingBridgeManagers[bridgeNetwork]) { + bridgeManagers.push(existingBridgeManagers[bridgeNetwork]); + continue; } - case 'base': { - const baseL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow( - 'baseL1CrossDomainMessenger' - ); - const baseL1StandardBridge = await governanceDeploymentManager.getContractOrThrow( - 'baseL1StandardBridge' - ); - const baseL1USDSBridge = await governanceDeploymentManager.getContractOrThrow( - 'baseL1USDSBridge' - ); - const targets = openProposal.targets; - const bridgeContracts = [baseL1CrossDomainMessenger.address, baseL1StandardBridge.address, baseL1USDSBridge.address]; - - return targets.some(t => bridgeContracts.includes(t)); + + let deploymentToken: string; + + let dm: DeploymentManager; + let existingBridgedDm: DeploymentManager | undefined; + for (const cachedDm of governanceDeploymentManager.bridgedDeploymentManagers.values()) { + if (cachedDm.network === bridgeNetwork) { + existingBridgedDm = cachedDm; + break; + } } - case 'linea': { - const lineaMessageService = await governanceDeploymentManager.getContractOrThrow( - 'lineaMessageService' - ); - const lineaL1USDCBridge = await governanceDeploymentManager.getContractOrThrow( - 'lineaL1USDCBridge' - ); - const lineaL1TokenBridge = await governanceDeploymentManager.getContractOrThrow( - 'lineaL1TokenBridge' - ); - const bridgeContracts = [ - lineaMessageService.address, - lineaL1USDCBridge.address, - lineaL1TokenBridge.address - ]; - const targets = openProposal.targets; - return targets.some(t => bridgeContracts.includes(t)); + + if (existingBridgedDm) { + dm = existingBridgedDm; + } else { + // default deployment token is USDC for all networks except Ronin (WETH) and Mantle (USDE) + switch (bridgeNetwork) { + case 'arbitrum': + case 'polygon': + case 'base': + case 'linea': + case 'optimism': + case 'unichain': + case 'scroll': + deploymentToken = 'usdc'; + break; + case 'mantle': + deploymentToken = 'usde'; + break; + case 'ronin': + deploymentToken = 'weth'; + break; + default: { + const tag = `[${governanceDeploymentManager.network} -> ${bridgeNetwork}]`; + throw new Error(`${tag} Unable to determine whether to relay Proposal ${openProposal.id}`); + } + } + + const hre = await forkedHreForBase({ name: '', network: bridgeNetwork, deployment: '' }); + dm = await governanceDeploymentManager.addBridgedDeploymentManager(bridgeNetwork, deploymentToken, hre); } - // case 'linea': { - // const governor = await governanceDeploymentManager.getContractOrThrow('governor'); - // const lineaMessageService = await governanceDeploymentManager.getContractOrThrow( - // 'lineaMessageService' - // ); - // const { targets } = await governor.getActions(openProposal.id); - // return targets.includes(lineaMessageService.address); + + existingBridgeManagers[bridgeNetwork] = dm; + bridgeManagers.push(dm); + // switch (bridgeNetwork) { + // case 'mainnet': { + // continue; // Mainnet proposals are not bridge proposals + // } + // case 'arbitrum': { + // const inbox = await governanceDeploymentManager.getContractOrThrow('arbitrumInbox'); + // const l1GatewayRouter = await governanceDeploymentManager.getContractOrThrow( + // 'arbitrumL1GatewayRouter' + // ); + // const targets = openProposal.targets; + // return targets.includes(inbox.address) || targets.includes(l1GatewayRouter.address); + // } + // case 'polygon': { + // const { + // fxRoot, + // RootChainManager + // } = await governanceDeploymentManager.getContracts(); + // const bridgeAddresses = [fxRoot, RootChainManager] + // .filter(x => x) + // .map(x => x.address.toLowerCase()); + // const targets = openProposal.targets; + // return targets.some(t => bridgeAddresses.includes(t.toLowerCase())); + // } + // case 'base': { + // const baseL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow( + // 'baseL1CrossDomainMessenger' + // ); + // const baseL1StandardBridge = await governanceDeploymentManager.getContractOrThrow( + // 'baseL1StandardBridge' + // ); + // const baseL1USDSBridge = await governanceDeploymentManager.getContractOrThrow( + // 'baseL1USDSBridge' + // ); + // const targets = openProposal.targets; + // const bridgeContracts = [baseL1CrossDomainMessenger.address, baseL1StandardBridge.address, baseL1USDSBridge.address]; + + // return targets.some(t => bridgeContracts.includes(t)); + // } + // case 'linea': { + // const lineaMessageService = await governanceDeploymentManager.getContractOrThrow( + // 'lineaMessageService' + // ); + // const lineaL1USDCBridge = await governanceDeploymentManager.getContractOrThrow( + // 'lineaL1USDCBridge' + // ); + // const lineaL1TokenBridge = await governanceDeploymentManager.getContractOrThrow( + // 'lineaL1TokenBridge' + // ); + // const bridgeContracts = [ + // lineaMessageService.address, + // lineaL1USDCBridge.address, + // lineaL1TokenBridge.address + // ]; + // const targets = openProposal.targets; + // return targets.some(t => bridgeContracts.includes(t)); + // } + // case 'optimism': { + // const opL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow( + // 'opL1CrossDomainMessenger' + // ); + // const opL1StandardBridge = await governanceDeploymentManager.getContractOrThrow( + // 'opL1StandardBridge' + // ); + // const targets = openProposal.targets; + // const bridgeContracts = [opL1CrossDomainMessenger.address, opL1StandardBridge.address]; + // return targets.some(t => bridgeContracts.includes(t)); + // } + // case 'mantle': { + // const mantleL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow( + // 'mantleL1CrossDomainMessenger' + // ); + // const mantleL1StandardBridge = await governanceDeploymentManager.getContractOrThrow( + // 'mantleL1StandardBridge' + // ); + // const targets = openProposal.targets; + // const bridgeContracts = [ + // mantleL1CrossDomainMessenger.address, + // mantleL1StandardBridge.address + // ]; + // return targets.some(t => bridgeContracts.includes(t)); + // } + // case 'unichain': { + // const unichainL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow( + // 'unichainL1CrossDomainMessenger' + // ); + // const unichainL1StandardBridge = await governanceDeploymentManager.getContractOrThrow( + // 'unichainL1StandardBridge' + // ); + // const targets = openProposal.targets; + // const bridgeContracts = [ + // unichainL1CrossDomainMessenger.address, + // unichainL1StandardBridge.address + // ]; + // return targets.some(t => bridgeContracts.includes(t)); + // } + // case 'scroll': { + // const scrollMessenger = await governanceDeploymentManager.getContractOrThrow( + // 'scrollMessenger' + // ); + // const targets = openProposal.targets; + // return targets.includes(scrollMessenger.address); + // } + // case 'ronin': { + // const governor = await governanceDeploymentManager.getContractOrThrow('governor'); + // const l1CCIPRouter = await governanceDeploymentManager.getContractOrThrow( + // 'l1CCIPRouter' + // ); + // const roninl1NativeBridge = await governanceDeploymentManager.getContractOrThrow( + // 'roninl1NativeBridge' + // ); + // const roninL1OnRamp = await governanceDeploymentManager.getContractOrThrow( + // 'roninl1CCIPOnRamp' + // ); + // const { targets } = await governor.proposalDetails(openProposal.id); + // const bridgeContracts = [ + // roninl1NativeBridge.address, + // l1CCIPRouter.address, + // roninL1OnRamp.address + // ]; + // return targets.some(t => bridgeContracts.includes(t)); + // } + // default: { + // const tag = `[${bridgeNetwork} -> ${governanceDeploymentManager.network}]`; + // throw new Error(`${tag} Unable to determine whether to relay Proposal ${openProposal.id}`); + // } // } - case 'optimism': { - const opL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow( - 'opL1CrossDomainMessenger' - ); - const opL1StandardBridge = await governanceDeploymentManager.getContractOrThrow( - 'opL1StandardBridge' - ); - const targets = openProposal.targets; - const bridgeContracts = [opL1CrossDomainMessenger.address, opL1StandardBridge.address]; - return targets.some(t => bridgeContracts.includes(t)); - } - case 'mantle': { - const mantleL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow( - 'mantleL1CrossDomainMessenger' - ); - const mantleL1StandardBridge = await governanceDeploymentManager.getContractOrThrow( - 'mantleL1StandardBridge' - ); - const targets = openProposal.targets; - const bridgeContracts = [ - mantleL1CrossDomainMessenger.address, - mantleL1StandardBridge.address - ]; - return targets.some(t => bridgeContracts.includes(t)); - } - case 'unichain': { - const unichainL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow( - 'unichainL1CrossDomainMessenger' - ); - const unichainL1StandardBridge = await governanceDeploymentManager.getContractOrThrow( - 'unichainL1StandardBridge' - ); - const targets = openProposal.targets; - const bridgeContracts = [ - unichainL1CrossDomainMessenger.address, - unichainL1StandardBridge.address - ]; - return targets.some(t => bridgeContracts.includes(t)); - } - case 'scroll': { - const scrollMessenger = await governanceDeploymentManager.getContractOrThrow( - 'scrollMessenger' - ); - const targets = openProposal.targets; - return targets.includes(scrollMessenger.address); - } - case 'ronin': { - const governor = await governanceDeploymentManager.getContractOrThrow('governor'); - const l1CCIPRouter = await governanceDeploymentManager.getContractOrThrow( - 'l1CCIPRouter' - ); - const roninl1NativeBridge = await governanceDeploymentManager.getContractOrThrow( - 'roninl1NativeBridge' - ); - const roninL1OnRamp = await governanceDeploymentManager.getContractOrThrow( - 'roninl1CCIPOnRamp' - ); - const { targets } = await governor.proposalDetails(openProposal.id); - const bridgeContracts = [ - roninl1NativeBridge.address, - l1CCIPRouter.address, - roninL1OnRamp.address - ]; - return targets.some(t => bridgeContracts.includes(t)); - } - default: { - const tag = `[${bridgeNetwork} -> ${governanceDeploymentManager.network}]`; - throw new Error(`${tag} Unable to determine whether to relay Proposal ${openProposal.id}`); - } } + return bridgeManagers; } + diff --git a/scenario/utils/relayArbitrumMessage.ts b/scenario/utils/relayArbitrumMessage.ts index 550979448..3c19598a7 100644 --- a/scenario/utils/relayArbitrumMessage.ts +++ b/scenario/utils/relayArbitrumMessage.ts @@ -1,7 +1,7 @@ import { DeploymentManager } from '../../plugins/deployment_manager'; import { impersonateAddress } from '../../plugins/scenario/utils'; import { setNextBaseFeeToZero, setNextBlockTimestamp } from './hreUtils'; -import { utils, BigNumber } from 'ethers'; +import { utils, BigNumber, Contract, constants } from 'ethers'; import { Log } from '@ethersproject/abstract-provider'; import { sourceTokens } from '../../plugins/scenario/utils/TokenSourcer'; import { OpenBridgedProposal } from '../context/Gov'; @@ -84,18 +84,18 @@ export async function relayArbitrumMessage( const header = '0x'; const headerLength = header.length; const wordLength = 2 * 32; - const innnerData = header + data.slice(headerLength + (11 * wordLength)); + const innerData = header + data.slice(headerLength + (11 * wordLength)); const toValue = data.slice(headerLength + (2 * wordLength), headerLength + (3 * wordLength)); let toAddress = BigNumber.from(`0x${toValue}`).toHexString(); - // if lenght of toAddress is less than 42, then it is padded with 0s and we need to add them after 0x + // if length of toAddress is less than 42, then it is padded with 0s and we need to add them after 0x if(toAddress.length < 42) { toAddress = `0x${toAddress.slice(2).padStart(40, '0')}`; } const messageNum = topics[1]; return { - data: innnerData, + data: innerData, toAddress, messageNum }; @@ -232,6 +232,18 @@ export async function relayArbitrumMessage( await signer.getAddress() ); } else { + // Mock ArbSys precompile (0x64) — Arbitrum precompiles don't exist in Hardhat's EVM, + // but the L2 gateways call ArbSys.sendTxToL1 internally during outboundTransfer. + // Bytecode 0x60206000f3 disassembles to: PUSH1 0x20 | PUSH1 0x00 | RETURN + // which returns 32 zero bytes from uninitialized memory for any call. + await bridgeDeploymentManager.hre.network.provider.request({ + method: 'hardhat_setCode', + params: [ + '0x0000000000000000000000000000000000000064', + '0x60206000f3', + ], + }); + await bridgeReceiver.executeProposal(id, { gasPrice: 0 }); } openBridgedProposals.push({ @@ -244,6 +256,189 @@ export async function relayArbitrumMessage( return openBridgedProposals; } +export async function simulateL2ToL1TokenBridging( + governanceDeploymentManager: DeploymentManager, + bridgeDeploymentManager: DeploymentManager, + tenderlyLogs?: any[], + proposalId?: BigNumber +) { + if(tenderlyLogs) { + return; + } + console.log('Simulating L2→L1 token bridging for any executed Arbitrum proposals...'); + + // L2 contracts + const bridgeReceiver = await bridgeDeploymentManager.getContractOrThrow('bridgeReceiver'); + + // Parse recent ProposalCreated events to find actions that bridge tokens from L2 to L1 + // ProposalCreated(address indexed rootMessageSender, uint256 id, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 eta) + console.log('Fetching recent ProposalCreated events from BridgeReceiver...'); + const latestBlockNumber = await bridgeDeploymentManager.hre.ethers.provider.getBlockNumber(); + const proposalCreatedEvents = await bridgeDeploymentManager.hre.ethers.provider.getLogs({ + fromBlock: latestBlockNumber - 1000, // look back 1000 blocks for ProposalCreated events, which should be sufficient to cover any recent proposals given typical block times on Arbitrum + toBlock: 'latest', + address: bridgeReceiver.address, + topics: [utils.id('ProposalCreated(address,uint256,address[],uint256[],string[],bytes[],uint256)')] + }); + const outboundTransferSignature = 'outboundTransfer(address,address,uint256,bytes)'; + const outboundTransfer2Signature = 'outboundTransfer(address,address,uint256,uint256,uint256,bytes)'; + const depositForBurnSignature = 'depositForBurn(uint256,uint32,bytes32,address,bytes32,uint256,uint32)'; + const ARBITRUM_GATEWAY_ROUTER = '0x5288c571Fd7aD117beA99bF60FE0846C4E84F933'; + const ARBITRUM_BRIDGE = '0x8315177ab297ba92a06054ce80a67ed4dbd7ed3a'; + const ARBITRUM_OUTBOX = '0x667e23ABd27E623c11d4CC00ca3EC4d0bD63337a'; + const MAINNET_WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; + + for (const event of proposalCreatedEvents) { + const decodedEvent = bridgeReceiver.interface.parseLog(event); + const { id, targets, signatures, calldatas } = decodedEvent.args; + + if (proposalId && id.toString() !== proposalId.toString()) { + continue; + } + + for (let i = 0; i < signatures.length; i++) { + // Look for L2→L1 outboundTransfer calls (standard Arbitrum gateway bridge) + if (signatures[i] === outboundTransferSignature || signatures[i] === outboundTransfer2Signature) { + const [l1Token, to, amount] = (() => { + if (signatures[i] === outboundTransferSignature) { + return utils.defaultAbiCoder.decode( + ['address', 'address', 'uint256', 'bytes'], + calldatas[i] + ); + } else if (signatures[i] === outboundTransfer2Signature) { + return utils.defaultAbiCoder.decode( + ['address', 'address', 'uint256', 'uint256', 'uint256', 'bytes'], + calldatas[i] + ); + } + })(); + console.log(`Simulating L2→L1 token bridging: ${amount.toString()} of ${l1Token} to ${to}`); + + const gatewayAddress = await (async () => { + if(targets[i].toLowerCase() === ARBITRUM_GATEWAY_ROUTER.toLowerCase()) { // Arbitrum WETH gateway + const router = new Contract( + ARBITRUM_GATEWAY_ROUTER, + ['function l1TokenToGateway(address l1Token) view returns (address)'], + await governanceDeploymentManager.getSigner() + ); + return await router.l1TokenToGateway(l1Token); + } + return targets[i]; + })(); + const l2Gateway = new Contract( + gatewayAddress, + ['function counterpartGateway() view returns (address)'], + await bridgeDeploymentManager.getSigner() + ); + const l1GatewayAddress = await l2Gateway.counterpartGateway(); + + const l1Gateway = new Contract( + l1GatewayAddress, + [ + 'function finalizeInboundTransfer(address _token, address _from, address _to, uint256 _amount, bytes calldata _data)', + 'function inbox() view returns (address)' + ], + await governanceDeploymentManager.getSigner() + ); + // override 0x4 slot in outbox to L2 gateway + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + ARBITRUM_OUTBOX, + '0x4', + utils.hexZeroPad(gatewayAddress, 32) + ]); + + // impersonate outbox to call finalizeInboundTransfer, as if the message came from L2 gateway + const outboxSigner = await impersonateAddress( + governanceDeploymentManager, + ARBITRUM_OUTBOX + ); + + await governanceDeploymentManager.hre.network.provider.send('hardhat_setBalance', [ + outboxSigner.address, + '0x1000000000000000000', + ]); + + const arbitrumBridge = new Contract( + ARBITRUM_BRIDGE, + ['function executeCall(address to, uint256 value, bytes calldata data)'], + outboxSigner + ); + + const data = l1Gateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + l1Token, + ARBITRUM_GATEWAY_ROUTER, + to, amount, + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, '0x']) + ]); + console.log(`Relaying message to L1 gateway at ${l1GatewayAddress} with data: ${data}`); + const bridgeTx = await arbitrumBridge.connect(outboxSigner).executeCall( + l1Gateway.address, + l1Token.toLowerCase() === MAINNET_WETH.toLowerCase() ? amount : 0, + data, + ); + await (bridgeTx).wait(); + // stop impersonation after the call + await governanceDeploymentManager.hre.network.provider.send('hardhat_stopImpersonatingAccount', [ + outboxSigner.address + ]); + // override 0x4 slot in outbox to L2 gateway + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + ARBITRUM_OUTBOX, + '0x4', + utils.hexZeroPad(constants.AddressZero, 32) + ]); + } + + // Look for L2→L1 CCTP depositForBurn calls (Circle CCTP bridge, e.g. native USDC) + if (signatures[i] === depositForBurnSignature) { + const [amount, , mintRecipientBytes32, burnToken] = utils.defaultAbiCoder.decode( + ['uint256', 'uint32', 'bytes32', 'address', 'bytes32', 'uint256', 'uint32'], + calldatas[i] + ); + + const mintRecipient = utils.getAddress('0x' + utils.hexlify(mintRecipientBytes32).slice(-40)); + + try { + // L2 + const l2CCTPTokenMessenger = await bridgeDeploymentManager.getContractOrThrow('CCTPMessageTransmitter'); + // Resolve L1 token via CCTP TokenMinter: burnToken (L2) → localToken (L1) + const l1CCTPTokenMessenger = await governanceDeploymentManager.getContractOrThrow('CCTPTokenMessenger'); + const tokenMinterAddress = await l1CCTPTokenMessenger.localMinter(); + const L1TokenMinter = new Contract( + tokenMinterAddress, + ['function mint(uint32 sourceDomain, bytes32 burnToken, address recipientOne, address recipientTwo, uint256 amountOne, uint256 amountTwo) returns (address)'], + await governanceDeploymentManager.getSigner() + ); + const l1CCTPTokenMessengerSigner = await impersonateAddress( + governanceDeploymentManager, + l1CCTPTokenMessenger.address + ); + await governanceDeploymentManager.hre.network.provider.send('hardhat_setBalance', [ + l1CCTPTokenMessengerSigner.address, + '0x1000000000000000000', + ]); + const sourceDomain = await l2CCTPTokenMessenger.localDomain(); + const mintTx = await L1TokenMinter.connect(l1CCTPTokenMessengerSigner).mint( + sourceDomain, + utils.hexZeroPad(burnToken, 32), + mintRecipient, + L1TokenMinter.address, // mint to the token minter first, since some tokens (e.g. USDC) have a cap on max amount per mint, and the token minter can then transfer to the recipient + amount, + 1 + ); + console.log('Simulated CCTP mint transaction:', mintTx.hash); + await mintTx.wait(); + } catch (e) { + console.log(`Warning: Could not simulate CCTP L2→L1 bridging for depositForBurn: ${e.message}`); + } + } + await governanceDeploymentManager.hre.network.provider.send('evm_mine'); + } + } +} + export async function relayArbitrumCCTPMint( governanceDeploymentManager: DeploymentManager, bridgeDeploymentManager: DeploymentManager, diff --git a/scenario/utils/relayBaseMessage.ts b/scenario/utils/relayBaseMessage.ts index 1e5418c11..eb5df2f78 100644 --- a/scenario/utils/relayBaseMessage.ts +++ b/scenario/utils/relayBaseMessage.ts @@ -1,7 +1,7 @@ import { DeploymentManager } from '../../plugins/deployment_manager'; import { impersonateAddress } from '../../plugins/scenario/utils'; import { setNextBaseFeeToZero, setNextBlockTimestamp } from './hreUtils'; -import { BigNumber, ethers } from 'ethers'; +import { BigNumber, ethers, utils } from 'ethers'; import { Log } from '@ethersproject/abstract-provider'; import { OpenBridgedProposal } from '../context/Gov'; import { applyL1ToL2Alias, isTenderlyLog } from './index'; @@ -207,4 +207,116 @@ export default async function relayBaseMessage( } return openBridgedProposals; -} \ No newline at end of file +} + +export async function simulateL2ToL1TokenBridging( + governanceDeploymentManager: DeploymentManager, + bridgeDeploymentManager: DeploymentManager, + tenderlyLogs?: any[], + proposalId?: BigNumber +) { + if(tenderlyLogs) { + return; + } + console.log('Simulating L2→L1 token bridging for any executed Base proposals...'); + + // L2 contracts + const bridgeReceiver = await bridgeDeploymentManager.getContractOrThrow('bridgeReceiver'); + const baseL2Bridge = await bridgeDeploymentManager.getContractOrThrow('l2StandardBridge'); + const l2CrossDomainMessenger = await bridgeDeploymentManager.getContractOrThrow('l2CrossDomainMessenger'); + + // L1 contracts + const baseL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow('baseL1CrossDomainMessenger'); + const baseL1Bridge = await governanceDeploymentManager.getContractOrThrow('baseL1StandardBridge'); + const BASE_L1_PORTAL = '0x49048044D57e1C92A77f79988d21Fa8fAF74E97e'; + + // Parse recent ProposalCreated events to find actions that bridge tokens from L2 to L1 + // ProposalCreated(address indexed rootMessageSender, uint256 id, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 eta) + console.log('Fetching recent ProposalCreated events from BridgeReceiver...'); + const latestBlockNumber = await bridgeDeploymentManager.hre.ethers.provider.getBlockNumber(); + const proposalCreatedEvents = await bridgeDeploymentManager.hre.ethers.provider.getLogs({ + fromBlock: latestBlockNumber - 1000, // look back 1000 blocks for ProposalCreated events, which should be sufficient to cover any recent proposals given typical block times on Base + toBlock: 'latest', + address: bridgeReceiver.address, + topics: [utils.id('ProposalCreated(address,uint256,address[],uint256[],string[],bytes[],uint256)')] + }); + + const bridgeERC20ToSignature = 'bridgeERC20To(address,address,address,uint256,uint32,bytes)'; + + for (const event of proposalCreatedEvents) { + const decodedEvent = bridgeReceiver.interface.parseLog(event); + const { id, signatures, calldatas } = decodedEvent.args; + + if (proposalId && id.toString() !== proposalId.toString()) { + continue; + } + + for (let i = 0; i < signatures.length; i++) { + if (signatures[i] === bridgeERC20ToSignature) { + const [localToken, remoteToken, to, amount, , extraData] = utils.defaultAbiCoder.decode( + ['address', 'address', 'address', 'uint256', 'uint32', 'bytes'], + calldatas[i] + ); + + console.log(`Simulating L2→L1 bridgeERC20To: ${amount.toString()} of ${remoteToken} to ${to}`); + + console.log('Setting up L1 state to simulate finalizeBridgeERC20...'); + console.log('Base L1 Portal address:', BASE_L1_PORTAL); + console.log('Overriding slot', utils.hexZeroPad('0x32', 32)); + console.log('l2CrossDomainMessenger:', utils.hexZeroPad(l2CrossDomainMessenger.address, 32)); + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + BASE_L1_PORTAL, + utils.hexZeroPad('0x32', 32), + utils.hexZeroPad(l2CrossDomainMessenger.address, 32) + ]); + + // Set deposits[_localToken][_remoteToken] on L1StandardBridge so finalizeBridgeERC20 won't underflow + // deposits mapping is at base slot 2 in L1StandardBridge storage layout + // In finalizeBridgeERC20 context: _localToken = remoteToken (L1), _remoteToken = localToken (L2) + const depositsBaseSlot = 2; + const innerSlot = utils.keccak256( + utils.defaultAbiCoder.encode(['address', 'uint256'], [remoteToken, depositsBaseSlot]) + ); + const depositsSlot = utils.keccak256( + utils.defaultAbiCoder.encode(['address', 'bytes32'], [localToken, innerSlot]) + ); + + console.log(`Setting deposits[${remoteToken}][${localToken}] to ${amount.toString()} at slot ${depositsSlot} on ${baseL1Bridge.address}`); + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + baseL1Bridge.address, + depositsSlot, + utils.hexZeroPad(amount.toHexString(), 32) + ]); + + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + baseL1CrossDomainMessenger.address, + '0xcc', + utils.hexZeroPad(baseL2Bridge.address, 32) + ]); + + const domainMessengerSigner = await impersonateAddress( + governanceDeploymentManager, + baseL1CrossDomainMessenger.address + ); + + await governanceDeploymentManager.hre.network.provider.send('hardhat_setBalance', [ + domainMessengerSigner.address, + ethers.utils.hexStripZeros(ethers.utils.parseEther('1').toHexString()), + ]); + + await ( + await baseL1Bridge.connect(domainMessengerSigner).finalizeBridgeERC20( + remoteToken, localToken, bridgeReceiver.address, to, amount, extraData, + { gasPrice: 0, gasLimit: 2_500_000 } + ) + ).wait(); + + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + BASE_L1_PORTAL, + utils.hexZeroPad('0x32', 32), + utils.hexZeroPad('0xdead', 32) + ]); + } + } + } +} diff --git a/scenario/utils/relayLineaMessage.ts b/scenario/utils/relayLineaMessage.ts index 78f2b0f14..74da86b7a 100644 --- a/scenario/utils/relayLineaMessage.ts +++ b/scenario/utils/relayLineaMessage.ts @@ -6,7 +6,7 @@ import { OpenBridgedProposal } from '../context/Gov'; import { impersonateAddress } from '../../plugins/scenario/utils'; import { isTenderlyLog } from './index'; -const LINEA_SETTER_ROLE_ACCOUNT = '0xc1C6B09D1eB6fCA0fF3cA11027E5Bc4AeDb47F67'; +const LINEA_SETTER_ROLE_ACCOUNT = '0x2b0F9C76970975aec03784EFd763623757EF7652'; export default async function relayLineaMessage( governanceDeploymentManager: DeploymentManager, diff --git a/scenario/utils/relayMessage.ts b/scenario/utils/relayMessage.ts index 0bccd6467..52c7d7fca 100644 --- a/scenario/utils/relayMessage.ts +++ b/scenario/utils/relayMessage.ts @@ -1,9 +1,9 @@ import { DeploymentManager } from '../../plugins/deployment_manager'; import relayPolygonMessage from './relayPolygonMessage'; -import { relayArbitrumMessage, relayArbitrumCCTPMint } from './relayArbitrumMessage'; +import { relayArbitrumMessage, relayArbitrumCCTPMint, simulateL2ToL1TokenBridging } from './relayArbitrumMessage'; import relayBaseMessage from './relayBaseMessage'; import relayLineaMessage from './relayLineaMessage'; -import relayOptimismMessage from './relayOptimismMessage'; +import relayOptimismMessage, { simulateL2ToL1TokenBridging as simulateOptimismL2ToL1TokenBridging } from './relayOptimismMessage'; import relayMantleMessage from './relayMantleMessage'; import { relayUnichainMessage, relayUnichainCCTPMint } from './relayUnichainMessage'; import relayScrollMessage from './relayScrollMessage'; @@ -16,23 +16,36 @@ export default async function relayMessage( tenderlyLogs?: any[] ) { const bridgeNetwork = bridgeDeploymentManager.network; - console.log(`Relaying messages from ${bridgeNetwork} -> ${governanceDeploymentManager.network}`); + if(bridgeNetwork === governanceDeploymentManager.network) return; // no need to relay if the proposal is on the same network + console.log(`Relaying messages from ${governanceDeploymentManager.network} -> ${bridgeNetwork}`); let proposal; switch (bridgeNetwork) { case 'base': - return await relayBaseMessage( + proposal = await relayBaseMessage( governanceDeploymentManager, bridgeDeploymentManager, startingBlockNumber, tenderlyLogs ); + await simulateOptimismL2ToL1TokenBridging( + governanceDeploymentManager, + bridgeDeploymentManager, + tenderlyLogs + ); + return proposal; case 'optimism': - return await relayOptimismMessage( + proposal = await relayOptimismMessage( governanceDeploymentManager, bridgeDeploymentManager, startingBlockNumber, tenderlyLogs ); + await simulateOptimismL2ToL1TokenBridging( + governanceDeploymentManager, + bridgeDeploymentManager, + tenderlyLogs + ); + return proposal; case 'mantle': return await relayMantleMessage( governanceDeploymentManager, @@ -74,6 +87,11 @@ export default async function relayMessage( startingBlockNumber, tenderlyLogs ); + await simulateL2ToL1TokenBridging( + governanceDeploymentManager, + bridgeDeploymentManager, + tenderlyLogs + ); return proposal; case 'linea': return await relayLineaMessage( diff --git a/scenario/utils/relayOptimismMessage.ts b/scenario/utils/relayOptimismMessage.ts index 5ce1d076d..02b2d9cd8 100644 --- a/scenario/utils/relayOptimismMessage.ts +++ b/scenario/utils/relayOptimismMessage.ts @@ -1,7 +1,7 @@ import { DeploymentManager } from '../../plugins/deployment_manager'; import { impersonateAddress } from '../../plugins/scenario/utils'; import { setNextBaseFeeToZero, setNextBlockTimestamp } from './hreUtils'; -import { BigNumber, ethers } from 'ethers'; +import { BigNumber, ethers, utils } from 'ethers'; import { Log } from '@ethersproject/abstract-provider'; import { OpenBridgedProposal } from '../context/Gov'; import { applyL1ToL2Alias, isTenderlyLog } from './index'; @@ -176,3 +176,113 @@ export default async function relayOptimismMessage( return openBridgedProposals; } } + +export async function simulateL2ToL1TokenBridging( + governanceDeploymentManager: DeploymentManager, + bridgeDeploymentManager: DeploymentManager, + tenderlyLogs?: any[], + proposalId?: BigNumber +) { + if(tenderlyLogs) { + return; + } + console.log('Simulating L2→L1 token bridging for any executed Optimism proposals...'); + + // L2 contracts + const bridgeReceiver = await bridgeDeploymentManager.getContractOrThrow('bridgeReceiver'); + const optimismL2Bridge = await bridgeDeploymentManager.getContractOrThrow('l2StandardBridge'); + const l2CrossDomainMessenger = await bridgeDeploymentManager.getContractOrThrow('l2CrossDomainMessenger'); + + // L1 contracts + const opL1CrossDomainMessenger = await governanceDeploymentManager.getContractOrThrow('opL1CrossDomainMessenger'); + const optimismL1Bridge = await governanceDeploymentManager.getContractOrThrow('opL1StandardBridge'); + const OPTIMISM_L1_PORTAL = '0xbEb5Fc579115071764c7423A4f12eDde41f106Ed'; + + // Parse recent ProposalCreated events to find actions that bridge tokens from L2 to L1 + // ProposalCreated(address indexed rootMessageSender, uint256 id, address[] targets, uint256[] values, string[] signatures, bytes[] calldatas, uint256 eta) + console.log('Fetching recent ProposalCreated events from BridgeReceiver...'); + const latestBlockNumber = await bridgeDeploymentManager.hre.ethers.provider.getBlockNumber(); + const proposalCreatedEvents = await bridgeDeploymentManager.hre.ethers.provider.getLogs({ + fromBlock: latestBlockNumber - 1000, // look back 1000 blocks for ProposalCreated events, which should be sufficient to cover any recent proposals given typical block times on Optimism + toBlock: 'latest', + address: bridgeReceiver.address, + topics: [utils.id('ProposalCreated(address,uint256,address[],uint256[],string[],bytes[],uint256)')] + }); + + const bridgeERC20ToSignature = 'bridgeERC20To(address,address,address,uint256,uint32,bytes)'; + + for (const event of proposalCreatedEvents) { + const decodedEvent = bridgeReceiver.interface.parseLog(event); + const { id, signatures, calldatas } = decodedEvent.args; + + if (proposalId && id.toString() !== proposalId.toString()) { + continue; + } + + for (let i = 0; i < signatures.length; i++) { + if (signatures[i] === bridgeERC20ToSignature) { + const [localToken, remoteToken, to, amount, , extraData] = utils.defaultAbiCoder.decode( + ['address', 'address', 'address', 'uint256', 'uint32', 'bytes'], + calldatas[i] + ); + + console.log(`Simulating L2→L1 bridgeERC20To: ${amount.toString()} of ${remoteToken} to ${to}`); + + console.log('Setting up L1 state to simulate finalizeBridgeERC20...'); + console.log('Optimism L1 Portal address:', OPTIMISM_L1_PORTAL); + console.log('Overriding slot', utils.hexZeroPad('0x32', 32)); + console.log('l2CrossDomainMessenger:', utils.hexZeroPad(l2CrossDomainMessenger.address, 32)); + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + OPTIMISM_L1_PORTAL, + utils.hexZeroPad('0x32', 32), + utils.hexZeroPad(l2CrossDomainMessenger.address, 32) + ]); + + // Set deposits[_localToken][_remoteToken] on L1StandardBridge so finalizeBridgeERC20 won't underflow + // deposits mapping is at base slot 2 in L1StandardBridge storage layout + // In finalizeBridgeERC20 context: _localToken = remoteToken (L1), _remoteToken = localToken (L2) + const depositsBaseSlot = 2; + const innerSlot = utils.keccak256( + utils.defaultAbiCoder.encode(['address', 'uint256'], [remoteToken, depositsBaseSlot]) + ); + const depositsSlot = utils.keccak256( + utils.defaultAbiCoder.encode(['address', 'bytes32'], [localToken, innerSlot]) + ); + + console.log(`Setting deposits[${remoteToken}][${localToken}] to ${amount.toString()} at slot ${depositsSlot} on ${optimismL1Bridge.address}`); + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + optimismL1Bridge.address, + depositsSlot, + utils.hexZeroPad(amount.toHexString(), 32) + ]); + + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + opL1CrossDomainMessenger.address, + '0xcc', + utils.hexZeroPad(optimismL2Bridge.address, 32) + ]); + + const domainMessengerSigner = await impersonateAddress( + governanceDeploymentManager, + opL1CrossDomainMessenger.address + ); + await governanceDeploymentManager.hre.network.provider.send('hardhat_setBalance', [ + domainMessengerSigner.address, + ethers.utils.hexStripZeros(ethers.utils.parseEther('1').toHexString()), + ]); + + await ( + await optimismL1Bridge.connect(domainMessengerSigner).finalizeBridgeERC20( + remoteToken, localToken, bridgeReceiver.address, to, amount, extraData, + { gasPrice: 0, gasLimit: 2_500_000 } + ) + ).wait(); + await governanceDeploymentManager.hre.network.provider.send('hardhat_setStorageAt', [ + OPTIMISM_L1_PORTAL, + utils.hexZeroPad('0x32', 32), + utils.hexZeroPad('0xdead', 32) + ]); + } + } + } +} diff --git a/scenario/utils/relayRoninMessage.ts b/scenario/utils/relayRoninMessage.ts index 4a72cee87..e86c760b9 100644 --- a/scenario/utils/relayRoninMessage.ts +++ b/scenario/utils/relayRoninMessage.ts @@ -4,9 +4,13 @@ import { setNextBaseFeeToZero, setNextBlockTimestamp } from './hreUtils'; import { BigNumber, ethers } from 'ethers'; import { Log } from '@ethersproject/abstract-provider'; import { OpenBridgedProposal } from '../context/Gov'; -import { isTenderlyLog } from './index'; +import { isTenderlyLog, updateCCIPStats } from './index'; const roninChainSelector = '6916147374840168594'; +const mainnetChainSelector = '5009297550715157269'; + +const MAINNET_CCIP_ROUTER = '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D'; +const MAINNET_RONIN_OFF_RAMP = '0x9a3Ed7007809CfD666999e439076B4Ce4120528D'; export default async function relayRoninMessage( governanceDeploymentManager: DeploymentManager, @@ -20,6 +24,7 @@ export default async function relayRoninMessage( const l2CCIPOffRamp = (await bridgeDeploymentManager.getContractOrThrow('l2CCIPOffRamp')); const bridgeReceiver = (await bridgeDeploymentManager.getContractOrThrow('bridgeReceiver')); const l1TokenAdminRegistry = await governanceDeploymentManager.getContractOrThrow('l1TokenAdminRegistry'); + const timelockMainnet = await governanceDeploymentManager.getContractOrThrow('timelock'); const l2TokenAdminRegistry = await bridgeDeploymentManager.existing( 'l2TokenAdminRegistry', @@ -27,7 +32,12 @@ export default async function relayRoninMessage( 'ronin' ); + const l2CCIPOnRamp = await bridgeDeploymentManager.getContractOrThrow('l2CCIPOnRamp'); + const l1CCIPRouter = await governanceDeploymentManager.existing('l1CCIPRouter', MAINNET_CCIP_ROUTER, 'mainnet'); + const l1CCIPOffRamp = await governanceDeploymentManager.existing('roninl1CCIPOffRamp', MAINNET_RONIN_OFF_RAMP, 'mainnet'); + const offRampSigner = await impersonateAddress(bridgeDeploymentManager, l2CCIPOffRamp.address); + const l1OffRampSigner = await impersonateAddress(governanceDeploymentManager, l1CCIPOffRamp.address); const openBridgedProposals: OpenBridgedProposal[] = []; @@ -80,7 +90,7 @@ export default async function relayRoninMessage( await bridgeDeploymentManager.hre.network.provider.request({ method: 'hardhat_setBalance', - params: [l2CCIPOffRamp.address, '0x1000000000000000000000'] + params: [offRampSigner.address, '0x1000000000000000000000'] }); await setNextBaseFeeToZero(bridgeDeploymentManager); @@ -99,7 +109,7 @@ export default async function relayRoninMessage( const callData = l2Router.interface.encodeFunctionData('routeMessage', [ any2EVMMessage, 25_000, - 2_000_000, + 10_000_000, internalMsg.receiver, ]); bridgeDeploymentManager.stashRelayMessage( @@ -136,7 +146,7 @@ export default async function relayRoninMessage( const routeTx = await l2Router.connect(offRampSigner).routeMessage( any2EVMMessage, 25_000, - 2_000_000, + 10_000_000, internalMsg.receiver, ); @@ -207,23 +217,6 @@ export default async function relayRoninMessage( } } - if (tenderlyLogs) { - const proposalFilter = bridgeReceiver.filters.ProposalCreated(); - const proposalEvents = await bridgeDeploymentManager.hre.ethers.provider.getLogs({ - fromBlock: 'latest', - toBlock: 'latest', - address: bridgeReceiver.address, - topics: proposalFilter.topics - }); - - for (let event of proposalEvents) { - const { - args: { id, eta }, - } = bridgeReceiver.interface.parseLog(event); - openBridgedProposals.push({ id, eta }); - } - } - for (const proposal of openBridgedProposals) { const { id, eta } = proposal; await setNextBlockTimestamp(bridgeDeploymentManager, eta.toNumber() + 1); @@ -231,6 +224,7 @@ export default async function relayRoninMessage( if (tenderlyLogs) { const callData = bridgeReceiver.interface.encodeFunctionData('executeProposal', [id]); + await updateCCIPStats(bridgeDeploymentManager, tenderlyLogs); const signer = await bridgeDeploymentManager.getSigner(); bridgeDeploymentManager.stashRelayMessage( bridgeReceiver.address, @@ -238,9 +232,100 @@ export default async function relayRoninMessage( await signer.getAddress() ); } else { - await bridgeReceiver.executeProposal(id, { gasPrice: 0 }); + await updateCCIPStats(bridgeDeploymentManager); + const signer = await bridgeDeploymentManager.getSigner(); + await bridgeReceiver.connect(signer).executeProposal(id, { gasPrice: 0 }); + console.log(`[CCIP L2] Executed bridged proposal ${id.toString()}`); + } + } + if (tenderlyLogs) return openBridgedProposals; + + // Process L2→L1 (Ronin→Mainnet) messages + const filterCCIPL2ToL1 = l2CCIPOnRamp.filters.CCIPSendRequested(); + let logsCCIPL2ToL1: Log[] = []; + + const latestBlock = (await bridgeDeploymentManager.hre.ethers.provider.getBlock('latest')).number; + logsCCIPL2ToL1 = await bridgeDeploymentManager.hre.ethers.provider.getLogs({ + fromBlock: latestBlock - 500, + toBlock: 'latest', + address: l2CCIPOnRamp.address, + topics: filterCCIPL2ToL1.topics || [] + }); + + const targetReceivers = [ + timelockMainnet.address.toLowerCase() + ]; + + for (const log of logsCCIPL2ToL1) { + const parsedLog = l2CCIPOnRamp.interface.parseLog(log); + + const internalMsg = parsedLog.args.message; + if (!targetReceivers.includes(internalMsg.receiver.toLowerCase())) continue; + console.log(`[CCIP L2->L1] Found CCIPSendRequested with messageId=${internalMsg.messageId}, receiver=${internalMsg.receiver}`); + + await governanceDeploymentManager.hre.network.provider.request({ + method: 'hardhat_setBalance', + params: [l1CCIPOffRamp.address, '0x1000000000000000000000'] + }); + + await setNextBaseFeeToZero(governanceDeploymentManager); + + const any2EVMMessage = { + messageId: internalMsg.messageId, + sourceChainSelector: internalMsg.sourceChainSelector, + sender: ethers.utils.defaultAbiCoder.encode(['address'], [internalMsg.sender]), + data: internalMsg.data, + destTokenAmounts: internalMsg.tokenAmounts.map((t: any) => ({ + token: t.token as string, + amount: BigNumber.from(t.amount) + })), + }; + + const routeTx = await l1CCIPRouter.connect(l1OffRampSigner).routeMessage( + any2EVMMessage, + 25_000, + 2_000_000, + internalMsg.receiver, + ); + + await routeTx.wait(); + + if (internalMsg.tokenAmounts.length) { + for (const tokenTransferData of internalMsg.tokenAmounts) { + const l2TokenPoolAddress = await l2TokenAdminRegistry.getPool(tokenTransferData.token); + const l2TokenPool = new ethers.Contract( + l2TokenPoolAddress, + ['function getRemoteToken(uint64) external view returns (bytes)'], + bridgeDeploymentManager.hre.ethers.provider + ); + const l1Token64 = await l2TokenPool.getRemoteToken(mainnetChainSelector); + const l1TokenAddress = ethers.utils.defaultAbiCoder.decode(['address'], l1Token64)[0]; + const l1TokenPool = await l1TokenAdminRegistry.getPool(l1TokenAddress); + const l1Token = new ethers.Contract( + l1TokenAddress, + [ + 'function balanceOf(address) external view returns (uint256)', + 'function transfer(address, uint256) external returns (bool)' + ], + governanceDeploymentManager.hre.ethers.provider + ); + + const poolSigner = await impersonateAddress(governanceDeploymentManager, l1TokenPool); + await governanceDeploymentManager.hre.network.provider.request({ + method: 'hardhat_setBalance', + params: [l1TokenPool, '0x1000000000000000000000'] + }); + + const poolBalance = await l1Token.balanceOf(l1TokenPool); + console.log(`[CCIP L2->L1] Token pool ${l1TokenPool} balance: ${poolBalance.toString()}, transferring ${tokenTransferData.amount.toString()} to ${internalMsg.receiver}`); + + const transferTx = await l1Token.connect(poolSigner).transfer(internalMsg.receiver, tokenTransferData.amount); + await transferTx.wait(); + console.log(`[CCIP L2->L1] Transferred ${tokenTransferData.amount.toString()} of ${l1TokenAddress} to ${internalMsg.receiver}`); + } } - console.log(`[CCIP L2] Executed bridged proposal ${id.toString()}`); + + console.log(`[CCIP L2->L1] Routed message to ${internalMsg.receiver}`); } return openBridgedProposals; diff --git a/scenario/utils/scenarioHelper.ts b/scenario/utils/scenarioHelper.ts index a104a629c..c90a5c287 100644 --- a/scenario/utils/scenarioHelper.ts +++ b/scenario/utils/scenarioHelper.ts @@ -22,6 +22,7 @@ const config = { transferBase: 1000, transferAsset: 5000, transferAsset1: 5000, + transferAsset2: 50, interestSeconds: 110, withdrawBase: 1000, withdrawAsset: 3000, @@ -29,7 +30,8 @@ const config = { withdrawAsset1: 3000, withdrawCollateral: 100, transferCollateral: 100, - supplyCollateral: 100 + supplyCollateral: 100, + supplyBase: 1000, }; export function getConfigForScenario(ctx: CometContext, i?: number) { @@ -70,6 +72,14 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { config.liquidationAsset = 100; } + if (ctx.world.base.network === 'mainnet' && ctx.world.base.deployment === 'usdt') { + if(i == 12) { + config.supplyCollateral = 0; + config.transferCollateral = 0; + config.withdrawCollateral = 0; + } + } + if (ctx.world.base.network === 'base' && ctx.world.base.deployment === 'aero') { config.interestSeconds = 110; } @@ -93,10 +103,12 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'optimism' && ctx.world.base.deployment === 'usdc') { - config.bulkerAsset = 10000; - config.bulkerAsset1 = 10000; - config.transferAsset1 = 10000; - config.withdrawAsset = 7000; + config.bulkerAsset = 100000; + config.bulkerAsset1 = 100000; + config.transferAsset = 100000; + config.transferAsset1 = 100000; + config.rewardsAsset = 100000; + config.withdrawAsset = 100000; } if (ctx.world.base.network === 'optimism' && ctx.world.base.deployment === 'usdt') { @@ -111,11 +123,14 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'arbitrum' && ctx.world.base.deployment === 'usdc') { - config.bulkerAsset = 10000; - config.bulkerAsset1 = 10000; - config.withdrawAsset = 7000; + config.bulkerAsset = 100000; + config.bulkerAsset1 = 100000; + config.withdrawAsset = 10000; config.transferAsset = 500000; + config.transferAsset1 = 500000; config.transferBase = 100; + config.rewardsBase = 100; + config.withdrawBase = 100; if(i == 8) { // tBTC config.supplyCollateral = 2; config.transferCollateral = 2; @@ -124,11 +139,12 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'arbitrum' && ctx.world.base.deployment === 'usdt') { - config.withdrawAsset = 7000; - config.bulkerAsset = 10000; + config.rewardsAsset = 20000; + config.withdrawAsset = 20000; + config.bulkerAsset = 100000; config.bulkerAsset1 = 10000; - config.transferAsset = 10000; - config.transferAsset1 = 10000; + config.transferAsset = 100000; + config.transferAsset1 = 100000; if(i == 5) { // tBTC config.supplyCollateral = 2; config.transferCollateral = 2; @@ -137,11 +153,11 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'arbitrum' && ctx.world.base.deployment === 'usdc.e') { - config.withdrawAsset = 7000; - config.bulkerAsset = 10000; - config.bulkerAsset1 = 10000; - config.transferAsset = 10000; - config.transferAsset1 = 10000; + config.withdrawAsset = 10000; + config.bulkerAsset = 100000; + config.bulkerAsset1 = 100000; + config.transferAsset = 500000; + config.transferAsset1 = 500000; config.liquidationDenominator = 84; config.liquidationBase = 100000; config.liquidationBase1 = 50000; @@ -153,18 +169,19 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'ronin' && ctx.world.base.deployment === 'weth') { + config.supplyBase = 100; config.transferBase = 10; - config.transferAsset = 200000; - config.transferAsset1 = 200000; + config.transferAsset = 4000000; + config.transferAsset1 = 800000; config.rewardsAsset = 1000000; config.rewardsBase = 200; config.withdrawBase = 10; - config.withdrawBase1 = 10; - config.withdrawAsset = 100000; - config.withdrawAsset1 = 10000; - config.liquidationBase = 150; + config.withdrawBase1 = 50; + config.withdrawAsset = 4000000; + config.withdrawAsset1 = 4000000; + config.liquidationBase = 200; config.liquidationBase1 = 50; - config.liquidationAsset = 5; + config.liquidationAsset = 6; config.bulkerAsset = 100000; config.bulkerAsset1 = 100000; config.bulkerComet = 100; diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 3757ac900..c6ed9019b 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -79,12 +79,11 @@ export type TestnetProposal = [ // Ideally these wouldn't be hardcoded, but other solutions are much more complex, and slower export const COMP_WHALES = { mainnet: [ - '0x9aa835bc7b8ce13b9b0c9764a52fbf71ac62ccf1', - '0x683a4f9915d6216f73d6df50151725036bd26c02', + '0x66cD62c6F8A4BB0Cd8720488BCBd1A6221B765F9', + '0xb06df4dd01a5c5782f360ada9345c87e86adae3d', + '0x3FB19771947072629C8EEE7995a2eF23B72d4C8A', '0x8169522c2C57883E8EF80C498aAB7820dA539806', - '0x8d07D225a769b7Af3A923481E1FdF49180e6A265', - '0x7d1a02C0ebcF06E1A36231A54951E061673ab27f', - '0x54A37d93E57c5DA659F508069Cf65A381b61E189', + '0x36cc7B13029B5DEe4034745FB4F24034f3F2ffc6', ], testnet: ['0xbbfe34e868343e6f4f5e8b5308de980d7bd88c46'] @@ -113,6 +112,7 @@ export const WHALES = { '0x34C0bD5877A5Ee7099D0f5688D65F4bB9158BDE2', // sFRAX whale '0x9152e9C04e8fE8373EDaa8f5841E25d4015658B7', // pumpBTC whale '0x65906988ADEe75306021C417a1A3458040239602', // LBTC whale + '0x7667095Caa12b79fCa489ff6E2198Ca01fDAe057', ], polygon: [ '0xF977814e90dA44bFA03b6295A0616a897441aceC', // USDT whale @@ -149,6 +149,7 @@ export const WHALES = { '0x54b5569deC8A6A8AE61A36Fd34e5c8945810db8b', // tBTC whale '0xDBD974Eb5360d053ea0c56B4DaCF4A9D3E894Ee2', // tETH whale '0xbA1333333333a1BA1108E8412f11850A5C319bA9', // tETH whale + '0xEA1132120ddcDDA2F119e99Fa7A27a0d036F7Ac9', // ezETH whale ], base: [ '0x6D3c5a4a7aC4B1428368310E4EC3bB1350d01455', // USDbC whale @@ -159,6 +160,7 @@ export const WHALES = { '0xcf3D55c10DB69f28fD1A75Bd73f3D8A2d9c595ad', // cbETH whale '0xb125E6687d4313864e53df431d5425969c15Eb2F', // cbETH whale '0x1539A4611f16a139891c14365Cab86599F3A8AFC', // tBTC whale + '0x0a1d576f3eFeF75b330424287a95A366e8281D54', // USDbC whale ], scroll: [ '0xaaaaAAAACB71BF2C8CaE522EA5fa455571A74106', // USDC whale @@ -280,7 +282,7 @@ export async function proposal( const { target, value, signature, calldata: cd } = action as TargetAction; targets.push(target); values.push(value ?? 0); - calldatas.push(utils.id(signature).slice(0, 10) + cd.slice(2)); + calldatas.push(signature ? utils.id(signature).slice(0, 10) + cd.slice(2) : cd); signatures.push(''); } } diff --git a/tasks/deployment_manager/task.ts b/tasks/deployment_manager/task.ts index d4378cb64..47bfc06c7 100644 --- a/tasks/deployment_manager/task.ts +++ b/tasks/deployment_manager/task.ts @@ -4,7 +4,7 @@ import { writeEnacted } from '../../plugins/deployment_manager/Enacted'; import { HardhatRuntimeEnvironment, HardhatConfig } from 'hardhat/types'; import { DeploymentManager, VerifyArgs } from '../../plugins/deployment_manager'; import { impersonateAddress } from '../../plugins/scenario/utils'; -import hreForBase from '../../plugins/scenario/utils/hreForBase'; +import hreForBase, { migrationStarted} from '../../plugins/scenario/utils/hreForBase'; // TODO: Don't depend on scenario's hreForBase async function getForkEnv(env: HardhatRuntimeEnvironment, deployment: string): Promise { @@ -248,6 +248,7 @@ task('migrate', 'Runs migration') governanceDm = dm; } + migrationStarted(); if (impersonate && !simulate) { throw new Error('Cannot impersonate an address if not simulating a migration. Please specify --simulate to simulate.'); } else if (impersonate && simulate) { @@ -376,6 +377,7 @@ task('deploy_and_migrate', 'Runs deploy and migration') governanceDm = dm; } + migrationStarted(); if (impersonate && !simulate) { throw new Error('Cannot impersonate an address if not simulating a migration. Please specify --simulate to simulate.'); } else if (impersonate && simulate) { diff --git a/test/allow-by-sig-test.ts b/test/allow-by-sig-test.ts index 82195d7e6..2d422d449 100644 --- a/test/allow-by-sig-test.ts +++ b/test/allow-by-sig-test.ts @@ -29,7 +29,7 @@ const types = { describe('allowBySig', function () { beforeEach(async () => { - comet = (await makeProtocol()).comet; + comet = (await makeProtocol()).cometWithExtendedAssetList; [_admin, pauseGuardian, signer, manager] = await ethers.getSigners(); domain = { diff --git a/test/helpers.ts b/test/helpers.ts index c343729b7..53fcda4a4 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -156,7 +156,12 @@ export function dfn(x: T | undefined | null, dflt: T): T { } export function exp(i: number, d: Numeric = 0, r: Numeric = 6): bigint { - return (BigInt(Math.floor(i * 10 ** Number(r))) * 10n ** BigInt(d)) / 10n ** BigInt(r); + const sign = i < 0 ? -1n : 1n; + const parts = Math.abs(i).toString().split('.'); + const intPart = parts[0]; + const fracPart = (parts[1] || '').padEnd(Number(r), '0').slice(0, Number(r)); + const scaled = BigInt(intPart + fracPart); + return sign * (scaled * 10n ** BigInt(d)) / 10n ** BigInt(r); } export function factor(f: number): bigint {