diff --git a/deployments/base/aero/migrations/1779459694_update_cbbtc_to_svr.ts b/deployments/base/aero/migrations/1779459694_update_cbbtc_to_svr.ts new file mode 100644 index 000000000..2955b0ebc --- /dev/null +++ b/deployments/base/aero/migrations/1779459694_update_cbbtc_to_svr.ts @@ -0,0 +1,279 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, proposal } from '../../../../src/deploy'; +import { utils, Contract } from 'ethers'; + +const CBBTC_USD_SVR_PRICE_FEED_ADDRESS = '0x2231aEdEf63Bef7F32fA1cEc5851C5CD72746263'; +const ETH_USD_SVR_PRICE_FEED_ADDRESS = '0x1428C9E908e32dD2839F99D63C242c91329A58C0'; + +const AERO_COMET_ADDRESS = '0x784efeB622244d2348d4F2522f8860B96fbEcE89'; +const USDC_COMET_ADDRESS = '0xb125E6687d4313864e53df431d5425969c15Eb2F'; +const USDS_COMET_ADDRESS = '0x2c776041CCFe903071AF44aa147368a9c8EEA518'; +const WETH_COMET_ADDRESS = '0x46e6b214b524310239732D51387075E0e70970bf'; + +const abi = [ + 'function getAssetInfoByAddress(address asset) public view returns((uint8 offset, address asset, address priceFeed, uint64 scale, uint64 borrowCollateralFactor, uint64 liquidateCollateralFactor, uint64 liquidationFactor, uint128 supplyCap))', + 'function getPrice(address priceFeed) public view returns (uint256)' +]; + +let newCbEthToUsdPriceFeed: string; +let newCbEthToEthPriceFeed: string; + +let oldAeroCbBTCToUsdPriceFeed: string; +let oldUsdcCbBTCToUsdPriceFeed: string; +let oldUsdsCbBTCToUsdPriceFeed: string; +let oldWethCbBTCToEthPriceFeed: string; + +export default migration('1779459694_update_cbbtc_to_svr', { + async prepare(deploymentManager: DeploymentManager) { + const cbBTCToUsdPriceFeed = await deploymentManager.deploy( + 'cbBTC:priceFeed', + 'pricefeeds/ScalingPriceFeedWithCustomDescription.sol', + [ + CBBTC_USD_SVR_PRICE_FEED_ADDRESS, // BTC / USD price feed + 8, // decimals + 'cbBTC / USD SVR price feed', // description + ], + true + ); + + const cbBTCToEthPriceFeed = await deploymentManager.deploy( + 'cbBTC:priceFeed', + 'pricefeeds/ReverseMultiplicativePriceFeed.sol', + [ + CBBTC_USD_SVR_PRICE_FEED_ADDRESS, // cbBTC / USD price feed + ETH_USD_SVR_PRICE_FEED_ADDRESS, // USD / ETH price feed + 8, // decimals + 'cbBTC / ETH SVR price feed', // description + ], + true + ); + + return { + cbBTCToUsdPriceFeed: cbBTCToUsdPriceFeed.address, + cbBTCToEthPriceFeed: cbBTCToEthPriceFeed.address + }; + }, + + enact: async ( + deploymentManager: DeploymentManager, + govDeploymentManager: DeploymentManager, + { + cbBTCToUsdPriceFeed, + cbBTCToEthPriceFeed, + } + ) => { + const trace = deploymentManager.tracer(); + + const { + bridgeReceiver, + cbBTC, + cometAdmin, + configurator, + } = await deploymentManager.getContracts(); + + const { governor, baseL1CrossDomainMessenger } = await govDeploymentManager.getContracts(); + + newCbEthToUsdPriceFeed = cbBTCToUsdPriceFeed; + newCbEthToEthPriceFeed = cbBTCToEthPriceFeed; + + const updateAssetPriceFeedCalldataAero = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + AERO_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataAero = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, AERO_COMET_ADDRESS] + ); + + const updateAssetPriceFeedCalldataWeth = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + WETH_COMET_ADDRESS, + cbBTC.address, + cbBTCToEthPriceFeed + ) + ); + const deployAndUpgradeToCalldataWeth = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, WETH_COMET_ADDRESS] + ); + + const l2ProposalDataPart1 = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, cometAdmin.address, // AERO + configurator.address, cometAdmin.address, // WETH + ], + [ + 0, 0, // AERO + 0, 0, // WETH + ], + [ + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // AERO + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // WETH + ], + [ + updateAssetPriceFeedCalldataAero, deployAndUpgradeToCalldataAero, // AERO + updateAssetPriceFeedCalldataWeth, deployAndUpgradeToCalldataWeth, // WETH + ], + ] + ); + + const updateAssetPriceFeedCalldataUsdc = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + USDC_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataUsdc = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, USDC_COMET_ADDRESS] + ); + + const updateAssetPriceFeedCalldataUsds = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + USDS_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataUsds = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, USDS_COMET_ADDRESS] + ); + + const l2ProposalDataPart2 = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, cometAdmin.address, // USDC + configurator.address, cometAdmin.address, // USDS + ], + [ + 0, 0, // USDC + 0, 0, // USDS + ], + [ + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // USDC + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // USDS + ], + [ + updateAssetPriceFeedCalldataUsdc, deployAndUpgradeToCalldataUsdc, // USDC + updateAssetPriceFeedCalldataUsds, deployAndUpgradeToCalldataUsds, // USDS + ], + ] + ); + + const aeroComet = new Contract(AERO_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdcComet = new Contract(USDC_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdsComet = new Contract(USDS_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const wethComet = new Contract(WETH_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + + const cbBTCAssetInfoAero = await aeroComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsdc = await usdcComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsds = await usdsComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoWeth = await wethComet.getAssetInfoByAddress(cbBTC.address); + + oldAeroCbBTCToUsdPriceFeed = cbBTCAssetInfoAero.priceFeed; + oldUsdcCbBTCToUsdPriceFeed = cbBTCAssetInfoUsdc.priceFeed; + oldUsdsCbBTCToUsdPriceFeed = cbBTCAssetInfoUsds.priceFeed; + oldWethCbBTCToEthPriceFeed = cbBTCAssetInfoWeth.priceFeed; + + const mainnetActions = [ + // 1. Update price feed for cbBTC in AERO and WETH markets + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalDataPart1, 3_000_000] + }, + // 2. Update price feed for cbBTC in USDC and USDS markets + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalDataPart2, 3_000_000] + }, + ]; + + const description = `# Update cbBTC price feeds on Base to SVR + +## Proposal summary + +This proposal updates the cbBTC price feeds in the Compound III AERO, USDC, USDS, and WETH markets on Base to use the new SVR price feeds. + +Further detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/1129) and [forum discussion for SVR](https://www.comp.xyz/t/request-for-proposal-rfp-oracle-extractable-value-oev-solution-for-compound-protocol/6786). + +### SVR fee recipient + +SVR generates revenue from liquidators and Compound DAO will receive that revenue as part of the protocol fee. The fee recipient for SVR on Base is set to Compound DAO multisig: 0xb3e79c7cac540ca833015e63d96d3032ba0c4129. + +## Proposal Actions + +The first action updates the cbBTC price feeds in the AERO and WETH markets. + +The second action updates the cbBTC price feeds in the USDC and USDS markets.`; + + const txn = await govDeploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ), 0, 300_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) { + const { + cbBTC, + } = await deploymentManager.getContracts(); + + const aeroComet = new Contract(AERO_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdcComet = new Contract(USDC_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdsComet = new Contract(USDS_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const wethComet = new Contract(WETH_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + + const cbBTCAssetInfoAero = await aeroComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsdc = await usdcComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsds = await usdsComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoWeth = await wethComet.getAssetInfoByAddress(cbBTC.address); + + expect(cbBTCAssetInfoAero.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoUsdc.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoUsds.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoWeth.priceFeed).to.equal(newCbEthToEthPriceFeed); + + expect(cbBTCAssetInfoAero.priceFeed).to.not.equal(oldAeroCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoUsdc.priceFeed).to.not.equal(oldUsdcCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoUsds.priceFeed).to.not.equal(oldUsdsCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoWeth.priceFeed).to.not.equal(oldWethCbBTCToEthPriceFeed); + + const oldPriceCbBTCToUsdAero = await aeroComet.getPrice(oldAeroCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdAero = await aeroComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdAero).to.be.closeTo(newPriceCbBTCToUsdAero, exp(400, 8)); // within $400 + + const oldPriceCbBTCToUsdUsdc = await usdcComet.getPrice(oldUsdcCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdUsdc = await usdcComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdUsdc).to.be.closeTo(newPriceCbBTCToUsdUsdc, exp(400, 8)); // within $400 + + const oldPriceCbBTCToUsdUsds = await usdsComet.getPrice(oldUsdsCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdUsds = await usdsComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdUsds).to.be.closeTo(newPriceCbBTCToUsdUsds, exp(400, 8)); // within $400 + + const oldPriceCbBTCToEthWeth = await wethComet.getPrice(oldWethCbBTCToEthPriceFeed); + const newPriceCbBTCToEthWeth = await wethComet.getPrice(newCbEthToEthPriceFeed); + expect(oldPriceCbBTCToEthWeth).to.be.closeTo(newPriceCbBTCToEthWeth, exp(0.188, 8)); // within 0.188 ETH ~ $400 + }, +}); diff --git a/deployments/base/usdc/migrations/1779459694_update_cbbtc_to_svr.ts b/deployments/base/usdc/migrations/1779459694_update_cbbtc_to_svr.ts new file mode 100644 index 000000000..91a0f1dd4 --- /dev/null +++ b/deployments/base/usdc/migrations/1779459694_update_cbbtc_to_svr.ts @@ -0,0 +1,279 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, proposal } from '../../../../src/deploy'; +import { utils, Contract } from 'ethers'; + +const CBBTC_USD_SVR_PRICE_FEED_ADDRESS = '0x2231aEdEf63Bef7F32fA1cEc5851C5CD72746263'; +const ETH_USD_SVR_PRICE_FEED_ADDRESS = '0x1428C9E908e32dD2839F99D63C242c91329A58C0'; + +const AERO_COMET_ADDRESS = '0x784efeB622244d2348d4F2522f8860B96fbEcE89'; +const USDC_COMET_ADDRESS = '0xb125E6687d4313864e53df431d5425969c15Eb2F'; +const USDS_COMET_ADDRESS = '0x2c776041CCFe903071AF44aa147368a9c8EEA518'; +const WETH_COMET_ADDRESS = '0x46e6b214b524310239732D51387075E0e70970bf'; + +const abi = [ + 'function getAssetInfoByAddress(address asset) public view returns((uint8 offset, address asset, address priceFeed, uint64 scale, uint64 borrowCollateralFactor, uint64 liquidateCollateralFactor, uint64 liquidationFactor, uint128 supplyCap))', + 'function getPrice(address priceFeed) public view returns (uint256)' +]; + +let newCbEthToUsdPriceFeed: string; +let newCbEthToEthPriceFeed: string; + +let oldAeroCbBTCToUsdPriceFeed: string; +let oldUsdcCbBTCToUsdPriceFeed: string; +let oldUsdsCbBTCToUsdPriceFeed: string; +let oldWethCbBTCToEthPriceFeed: string; + +export default migration('1779459694_update_cbbtc_to_svr', { + async prepare(deploymentManager: DeploymentManager) { + const cbBTCToUsdPriceFeed = await deploymentManager.deploy( + 'cbBTC:priceFeed', + 'pricefeeds/ScalingPriceFeedWithCustomDescription.sol', + [ + CBBTC_USD_SVR_PRICE_FEED_ADDRESS, // BTC / USD price feed + 8, // decimals + 'cbBTC / USD SVR price feed', // description + ], + true + ); + + const cbBTCToEthPriceFeed = await deploymentManager.deploy( + 'cbBTC:priceFeed', + 'pricefeeds/ReverseMultiplicativePriceFeed.sol', + [ + CBBTC_USD_SVR_PRICE_FEED_ADDRESS, // cbBTC / USD price feed + ETH_USD_SVR_PRICE_FEED_ADDRESS, // USD / ETH price feed + 8, // decimals + 'cbBTC / ETH SVR price feed', // description + ], + true + ); + + return { + cbBTCToUsdPriceFeed: cbBTCToUsdPriceFeed.address, + cbBTCToEthPriceFeed: cbBTCToEthPriceFeed.address + }; + }, + + enact: async ( + deploymentManager: DeploymentManager, + govDeploymentManager: DeploymentManager, + { + cbBTCToUsdPriceFeed, + cbBTCToEthPriceFeed, + } + ) => { + const trace = deploymentManager.tracer(); + + const { + bridgeReceiver, + cbBTC, + cometAdmin, + configurator, + } = await deploymentManager.getContracts(); + + const { governor, baseL1CrossDomainMessenger } = await govDeploymentManager.getContracts(); + + newCbEthToUsdPriceFeed = cbBTCToUsdPriceFeed; + newCbEthToEthPriceFeed = cbBTCToEthPriceFeed; + + const updateAssetPriceFeedCalldataAero = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + AERO_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataAero = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, AERO_COMET_ADDRESS] + ); + + const updateAssetPriceFeedCalldataWeth = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + WETH_COMET_ADDRESS, + cbBTC.address, + cbBTCToEthPriceFeed + ) + ); + const deployAndUpgradeToCalldataWeth = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, WETH_COMET_ADDRESS] + ); + + const l2ProposalDataPart1 = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, cometAdmin.address, // AERO + configurator.address, cometAdmin.address, // WETH + ], + [ + 0, 0, // AERO + 0, 0, // WETH + ], + [ + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // AERO + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // WETH + ], + [ + updateAssetPriceFeedCalldataAero, deployAndUpgradeToCalldataAero, // AERO + updateAssetPriceFeedCalldataWeth, deployAndUpgradeToCalldataWeth, // WETH + ], + ] + ); + + const updateAssetPriceFeedCalldataUsdc = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + USDC_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataUsdc = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, USDC_COMET_ADDRESS] + ); + + const updateAssetPriceFeedCalldataUsds = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + USDS_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataUsds = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, USDS_COMET_ADDRESS] + ); + + const l2ProposalDataPart2 = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, cometAdmin.address, // USDC + configurator.address, cometAdmin.address, // USDS + ], + [ + 0, 0, // USDC + 0, 0, // USDS + ], + [ + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // USDC + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // USDS + ], + [ + updateAssetPriceFeedCalldataUsdc, deployAndUpgradeToCalldataUsdc, // USDC + updateAssetPriceFeedCalldataUsds, deployAndUpgradeToCalldataUsds, // USDS + ], + ] + ); + + const aeroComet = new Contract(AERO_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdcComet = new Contract(USDC_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdsComet = new Contract(USDS_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const wethComet = new Contract(WETH_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + + const cbBTCAssetInfoAero = await aeroComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsdc = await usdcComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsds = await usdsComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoWeth = await wethComet.getAssetInfoByAddress(cbBTC.address); + + oldAeroCbBTCToUsdPriceFeed = cbBTCAssetInfoAero.priceFeed; + oldUsdcCbBTCToUsdPriceFeed = cbBTCAssetInfoUsdc.priceFeed; + oldUsdsCbBTCToUsdPriceFeed = cbBTCAssetInfoUsds.priceFeed; + oldWethCbBTCToEthPriceFeed = cbBTCAssetInfoWeth.priceFeed; + + const mainnetActions = [ + // 1. Update price feed for cbBTC in AERO and WETH markets + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalDataPart1, 3_000_000] + }, + // 2. Update price feed for cbBTC in USDC and USDS markets + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalDataPart2, 3_000_000] + }, + ]; + + const description = `# Update cbBTC price feeds on Base to SVR + +## Proposal summary + +This proposal updates the cbBTC price feeds in the Compound III AERO, USDC, USDS, and WETH markets on Base to use the new SVR price feeds. + +Further detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/1129) and [forum discussion for SVR](https://www.comp.xyz/t/request-for-proposal-rfp-oracle-extractable-value-oev-solution-for-compound-protocol/6786). + +### SVR fee recipient + +SVR generates revenue from liquidators and Compound DAO will receive that revenue as part of the protocol fee. The fee recipient for SVR on Base is set to Compound DAO multisig: 0xb3e79c7cac540ca833015e63d96d3032ba0c4129. + +## Proposal Actions + +The first action updates the cbBTC price feeds in the AERO and WETH markets. + +The second action updates the cbBTC price feeds in the USDC and USDS markets.`; + + const txn = await govDeploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ), 0, 300_000 + ); + + 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 { + cbBTC, + } = await deploymentManager.getContracts(); + + const aeroComet = new Contract(AERO_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdcComet = new Contract(USDC_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdsComet = new Contract(USDS_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const wethComet = new Contract(WETH_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + + const cbBTCAssetInfoAero = await aeroComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsdc = await usdcComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsds = await usdsComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoWeth = await wethComet.getAssetInfoByAddress(cbBTC.address); + + expect(cbBTCAssetInfoAero.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoUsdc.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoUsds.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoWeth.priceFeed).to.equal(newCbEthToEthPriceFeed); + + expect(cbBTCAssetInfoAero.priceFeed).to.not.equal(oldAeroCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoUsdc.priceFeed).to.not.equal(oldUsdcCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoUsds.priceFeed).to.not.equal(oldUsdsCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoWeth.priceFeed).to.not.equal(oldWethCbBTCToEthPriceFeed); + + const oldPriceCbBTCToUsdAero = await aeroComet.getPrice(oldAeroCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdAero = await aeroComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdAero).to.be.closeTo(newPriceCbBTCToUsdAero, exp(400, 8)); // within $400 + + const oldPriceCbBTCToUsdUsdc = await usdcComet.getPrice(oldUsdcCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdUsdc = await usdcComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdUsdc).to.be.closeTo(newPriceCbBTCToUsdUsdc, exp(400, 8)); // within $400 + + const oldPriceCbBTCToUsdUsds = await usdsComet.getPrice(oldUsdsCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdUsds = await usdsComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdUsds).to.be.closeTo(newPriceCbBTCToUsdUsds, exp(400, 8)); // within $400 + + const oldPriceCbBTCToEthWeth = await wethComet.getPrice(oldWethCbBTCToEthPriceFeed); + const newPriceCbBTCToEthWeth = await wethComet.getPrice(newCbEthToEthPriceFeed); + expect(oldPriceCbBTCToEthWeth).to.be.closeTo(newPriceCbBTCToEthWeth, exp(0.188, 8)); // within 0.188 ETH ~ $400 + }, +}); diff --git a/deployments/base/usds/migrations/1779459694_update_cbbtc_to_svr.ts b/deployments/base/usds/migrations/1779459694_update_cbbtc_to_svr.ts new file mode 100644 index 000000000..2955b0ebc --- /dev/null +++ b/deployments/base/usds/migrations/1779459694_update_cbbtc_to_svr.ts @@ -0,0 +1,279 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, proposal } from '../../../../src/deploy'; +import { utils, Contract } from 'ethers'; + +const CBBTC_USD_SVR_PRICE_FEED_ADDRESS = '0x2231aEdEf63Bef7F32fA1cEc5851C5CD72746263'; +const ETH_USD_SVR_PRICE_FEED_ADDRESS = '0x1428C9E908e32dD2839F99D63C242c91329A58C0'; + +const AERO_COMET_ADDRESS = '0x784efeB622244d2348d4F2522f8860B96fbEcE89'; +const USDC_COMET_ADDRESS = '0xb125E6687d4313864e53df431d5425969c15Eb2F'; +const USDS_COMET_ADDRESS = '0x2c776041CCFe903071AF44aa147368a9c8EEA518'; +const WETH_COMET_ADDRESS = '0x46e6b214b524310239732D51387075E0e70970bf'; + +const abi = [ + 'function getAssetInfoByAddress(address asset) public view returns((uint8 offset, address asset, address priceFeed, uint64 scale, uint64 borrowCollateralFactor, uint64 liquidateCollateralFactor, uint64 liquidationFactor, uint128 supplyCap))', + 'function getPrice(address priceFeed) public view returns (uint256)' +]; + +let newCbEthToUsdPriceFeed: string; +let newCbEthToEthPriceFeed: string; + +let oldAeroCbBTCToUsdPriceFeed: string; +let oldUsdcCbBTCToUsdPriceFeed: string; +let oldUsdsCbBTCToUsdPriceFeed: string; +let oldWethCbBTCToEthPriceFeed: string; + +export default migration('1779459694_update_cbbtc_to_svr', { + async prepare(deploymentManager: DeploymentManager) { + const cbBTCToUsdPriceFeed = await deploymentManager.deploy( + 'cbBTC:priceFeed', + 'pricefeeds/ScalingPriceFeedWithCustomDescription.sol', + [ + CBBTC_USD_SVR_PRICE_FEED_ADDRESS, // BTC / USD price feed + 8, // decimals + 'cbBTC / USD SVR price feed', // description + ], + true + ); + + const cbBTCToEthPriceFeed = await deploymentManager.deploy( + 'cbBTC:priceFeed', + 'pricefeeds/ReverseMultiplicativePriceFeed.sol', + [ + CBBTC_USD_SVR_PRICE_FEED_ADDRESS, // cbBTC / USD price feed + ETH_USD_SVR_PRICE_FEED_ADDRESS, // USD / ETH price feed + 8, // decimals + 'cbBTC / ETH SVR price feed', // description + ], + true + ); + + return { + cbBTCToUsdPriceFeed: cbBTCToUsdPriceFeed.address, + cbBTCToEthPriceFeed: cbBTCToEthPriceFeed.address + }; + }, + + enact: async ( + deploymentManager: DeploymentManager, + govDeploymentManager: DeploymentManager, + { + cbBTCToUsdPriceFeed, + cbBTCToEthPriceFeed, + } + ) => { + const trace = deploymentManager.tracer(); + + const { + bridgeReceiver, + cbBTC, + cometAdmin, + configurator, + } = await deploymentManager.getContracts(); + + const { governor, baseL1CrossDomainMessenger } = await govDeploymentManager.getContracts(); + + newCbEthToUsdPriceFeed = cbBTCToUsdPriceFeed; + newCbEthToEthPriceFeed = cbBTCToEthPriceFeed; + + const updateAssetPriceFeedCalldataAero = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + AERO_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataAero = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, AERO_COMET_ADDRESS] + ); + + const updateAssetPriceFeedCalldataWeth = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + WETH_COMET_ADDRESS, + cbBTC.address, + cbBTCToEthPriceFeed + ) + ); + const deployAndUpgradeToCalldataWeth = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, WETH_COMET_ADDRESS] + ); + + const l2ProposalDataPart1 = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, cometAdmin.address, // AERO + configurator.address, cometAdmin.address, // WETH + ], + [ + 0, 0, // AERO + 0, 0, // WETH + ], + [ + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // AERO + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // WETH + ], + [ + updateAssetPriceFeedCalldataAero, deployAndUpgradeToCalldataAero, // AERO + updateAssetPriceFeedCalldataWeth, deployAndUpgradeToCalldataWeth, // WETH + ], + ] + ); + + const updateAssetPriceFeedCalldataUsdc = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + USDC_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataUsdc = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, USDC_COMET_ADDRESS] + ); + + const updateAssetPriceFeedCalldataUsds = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + USDS_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataUsds = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, USDS_COMET_ADDRESS] + ); + + const l2ProposalDataPart2 = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, cometAdmin.address, // USDC + configurator.address, cometAdmin.address, // USDS + ], + [ + 0, 0, // USDC + 0, 0, // USDS + ], + [ + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // USDC + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // USDS + ], + [ + updateAssetPriceFeedCalldataUsdc, deployAndUpgradeToCalldataUsdc, // USDC + updateAssetPriceFeedCalldataUsds, deployAndUpgradeToCalldataUsds, // USDS + ], + ] + ); + + const aeroComet = new Contract(AERO_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdcComet = new Contract(USDC_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdsComet = new Contract(USDS_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const wethComet = new Contract(WETH_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + + const cbBTCAssetInfoAero = await aeroComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsdc = await usdcComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsds = await usdsComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoWeth = await wethComet.getAssetInfoByAddress(cbBTC.address); + + oldAeroCbBTCToUsdPriceFeed = cbBTCAssetInfoAero.priceFeed; + oldUsdcCbBTCToUsdPriceFeed = cbBTCAssetInfoUsdc.priceFeed; + oldUsdsCbBTCToUsdPriceFeed = cbBTCAssetInfoUsds.priceFeed; + oldWethCbBTCToEthPriceFeed = cbBTCAssetInfoWeth.priceFeed; + + const mainnetActions = [ + // 1. Update price feed for cbBTC in AERO and WETH markets + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalDataPart1, 3_000_000] + }, + // 2. Update price feed for cbBTC in USDC and USDS markets + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalDataPart2, 3_000_000] + }, + ]; + + const description = `# Update cbBTC price feeds on Base to SVR + +## Proposal summary + +This proposal updates the cbBTC price feeds in the Compound III AERO, USDC, USDS, and WETH markets on Base to use the new SVR price feeds. + +Further detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/1129) and [forum discussion for SVR](https://www.comp.xyz/t/request-for-proposal-rfp-oracle-extractable-value-oev-solution-for-compound-protocol/6786). + +### SVR fee recipient + +SVR generates revenue from liquidators and Compound DAO will receive that revenue as part of the protocol fee. The fee recipient for SVR on Base is set to Compound DAO multisig: 0xb3e79c7cac540ca833015e63d96d3032ba0c4129. + +## Proposal Actions + +The first action updates the cbBTC price feeds in the AERO and WETH markets. + +The second action updates the cbBTC price feeds in the USDC and USDS markets.`; + + const txn = await govDeploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ), 0, 300_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) { + const { + cbBTC, + } = await deploymentManager.getContracts(); + + const aeroComet = new Contract(AERO_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdcComet = new Contract(USDC_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdsComet = new Contract(USDS_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const wethComet = new Contract(WETH_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + + const cbBTCAssetInfoAero = await aeroComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsdc = await usdcComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsds = await usdsComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoWeth = await wethComet.getAssetInfoByAddress(cbBTC.address); + + expect(cbBTCAssetInfoAero.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoUsdc.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoUsds.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoWeth.priceFeed).to.equal(newCbEthToEthPriceFeed); + + expect(cbBTCAssetInfoAero.priceFeed).to.not.equal(oldAeroCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoUsdc.priceFeed).to.not.equal(oldUsdcCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoUsds.priceFeed).to.not.equal(oldUsdsCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoWeth.priceFeed).to.not.equal(oldWethCbBTCToEthPriceFeed); + + const oldPriceCbBTCToUsdAero = await aeroComet.getPrice(oldAeroCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdAero = await aeroComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdAero).to.be.closeTo(newPriceCbBTCToUsdAero, exp(400, 8)); // within $400 + + const oldPriceCbBTCToUsdUsdc = await usdcComet.getPrice(oldUsdcCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdUsdc = await usdcComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdUsdc).to.be.closeTo(newPriceCbBTCToUsdUsdc, exp(400, 8)); // within $400 + + const oldPriceCbBTCToUsdUsds = await usdsComet.getPrice(oldUsdsCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdUsds = await usdsComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdUsds).to.be.closeTo(newPriceCbBTCToUsdUsds, exp(400, 8)); // within $400 + + const oldPriceCbBTCToEthWeth = await wethComet.getPrice(oldWethCbBTCToEthPriceFeed); + const newPriceCbBTCToEthWeth = await wethComet.getPrice(newCbEthToEthPriceFeed); + expect(oldPriceCbBTCToEthWeth).to.be.closeTo(newPriceCbBTCToEthWeth, exp(0.188, 8)); // within 0.188 ETH ~ $400 + }, +}); diff --git a/deployments/base/weth/migrations/1779459694_update_cbbtc_to_svr.ts b/deployments/base/weth/migrations/1779459694_update_cbbtc_to_svr.ts new file mode 100644 index 000000000..2955b0ebc --- /dev/null +++ b/deployments/base/weth/migrations/1779459694_update_cbbtc_to_svr.ts @@ -0,0 +1,279 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, exp, proposal } from '../../../../src/deploy'; +import { utils, Contract } from 'ethers'; + +const CBBTC_USD_SVR_PRICE_FEED_ADDRESS = '0x2231aEdEf63Bef7F32fA1cEc5851C5CD72746263'; +const ETH_USD_SVR_PRICE_FEED_ADDRESS = '0x1428C9E908e32dD2839F99D63C242c91329A58C0'; + +const AERO_COMET_ADDRESS = '0x784efeB622244d2348d4F2522f8860B96fbEcE89'; +const USDC_COMET_ADDRESS = '0xb125E6687d4313864e53df431d5425969c15Eb2F'; +const USDS_COMET_ADDRESS = '0x2c776041CCFe903071AF44aa147368a9c8EEA518'; +const WETH_COMET_ADDRESS = '0x46e6b214b524310239732D51387075E0e70970bf'; + +const abi = [ + 'function getAssetInfoByAddress(address asset) public view returns((uint8 offset, address asset, address priceFeed, uint64 scale, uint64 borrowCollateralFactor, uint64 liquidateCollateralFactor, uint64 liquidationFactor, uint128 supplyCap))', + 'function getPrice(address priceFeed) public view returns (uint256)' +]; + +let newCbEthToUsdPriceFeed: string; +let newCbEthToEthPriceFeed: string; + +let oldAeroCbBTCToUsdPriceFeed: string; +let oldUsdcCbBTCToUsdPriceFeed: string; +let oldUsdsCbBTCToUsdPriceFeed: string; +let oldWethCbBTCToEthPriceFeed: string; + +export default migration('1779459694_update_cbbtc_to_svr', { + async prepare(deploymentManager: DeploymentManager) { + const cbBTCToUsdPriceFeed = await deploymentManager.deploy( + 'cbBTC:priceFeed', + 'pricefeeds/ScalingPriceFeedWithCustomDescription.sol', + [ + CBBTC_USD_SVR_PRICE_FEED_ADDRESS, // BTC / USD price feed + 8, // decimals + 'cbBTC / USD SVR price feed', // description + ], + true + ); + + const cbBTCToEthPriceFeed = await deploymentManager.deploy( + 'cbBTC:priceFeed', + 'pricefeeds/ReverseMultiplicativePriceFeed.sol', + [ + CBBTC_USD_SVR_PRICE_FEED_ADDRESS, // cbBTC / USD price feed + ETH_USD_SVR_PRICE_FEED_ADDRESS, // USD / ETH price feed + 8, // decimals + 'cbBTC / ETH SVR price feed', // description + ], + true + ); + + return { + cbBTCToUsdPriceFeed: cbBTCToUsdPriceFeed.address, + cbBTCToEthPriceFeed: cbBTCToEthPriceFeed.address + }; + }, + + enact: async ( + deploymentManager: DeploymentManager, + govDeploymentManager: DeploymentManager, + { + cbBTCToUsdPriceFeed, + cbBTCToEthPriceFeed, + } + ) => { + const trace = deploymentManager.tracer(); + + const { + bridgeReceiver, + cbBTC, + cometAdmin, + configurator, + } = await deploymentManager.getContracts(); + + const { governor, baseL1CrossDomainMessenger } = await govDeploymentManager.getContracts(); + + newCbEthToUsdPriceFeed = cbBTCToUsdPriceFeed; + newCbEthToEthPriceFeed = cbBTCToEthPriceFeed; + + const updateAssetPriceFeedCalldataAero = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + AERO_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataAero = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, AERO_COMET_ADDRESS] + ); + + const updateAssetPriceFeedCalldataWeth = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + WETH_COMET_ADDRESS, + cbBTC.address, + cbBTCToEthPriceFeed + ) + ); + const deployAndUpgradeToCalldataWeth = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, WETH_COMET_ADDRESS] + ); + + const l2ProposalDataPart1 = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, cometAdmin.address, // AERO + configurator.address, cometAdmin.address, // WETH + ], + [ + 0, 0, // AERO + 0, 0, // WETH + ], + [ + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // AERO + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // WETH + ], + [ + updateAssetPriceFeedCalldataAero, deployAndUpgradeToCalldataAero, // AERO + updateAssetPriceFeedCalldataWeth, deployAndUpgradeToCalldataWeth, // WETH + ], + ] + ); + + const updateAssetPriceFeedCalldataUsdc = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + USDC_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataUsdc = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, USDC_COMET_ADDRESS] + ); + + const updateAssetPriceFeedCalldataUsds = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + USDS_COMET_ADDRESS, + cbBTC.address, + cbBTCToUsdPriceFeed + ) + ); + const deployAndUpgradeToCalldataUsds = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, USDS_COMET_ADDRESS] + ); + + const l2ProposalDataPart2 = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, cometAdmin.address, // USDC + configurator.address, cometAdmin.address, // USDS + ], + [ + 0, 0, // USDC + 0, 0, // USDS + ], + [ + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // USDC + 'updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)', // USDS + ], + [ + updateAssetPriceFeedCalldataUsdc, deployAndUpgradeToCalldataUsdc, // USDC + updateAssetPriceFeedCalldataUsds, deployAndUpgradeToCalldataUsds, // USDS + ], + ] + ); + + const aeroComet = new Contract(AERO_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdcComet = new Contract(USDC_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdsComet = new Contract(USDS_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const wethComet = new Contract(WETH_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + + const cbBTCAssetInfoAero = await aeroComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsdc = await usdcComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsds = await usdsComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoWeth = await wethComet.getAssetInfoByAddress(cbBTC.address); + + oldAeroCbBTCToUsdPriceFeed = cbBTCAssetInfoAero.priceFeed; + oldUsdcCbBTCToUsdPriceFeed = cbBTCAssetInfoUsdc.priceFeed; + oldUsdsCbBTCToUsdPriceFeed = cbBTCAssetInfoUsds.priceFeed; + oldWethCbBTCToEthPriceFeed = cbBTCAssetInfoWeth.priceFeed; + + const mainnetActions = [ + // 1. Update price feed for cbBTC in AERO and WETH markets + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalDataPart1, 3_000_000] + }, + // 2. Update price feed for cbBTC in USDC and USDS markets + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [bridgeReceiver.address, l2ProposalDataPart2, 3_000_000] + }, + ]; + + const description = `# Update cbBTC price feeds on Base to SVR + +## Proposal summary + +This proposal updates the cbBTC price feeds in the Compound III AERO, USDC, USDS, and WETH markets on Base to use the new SVR price feeds. + +Further detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/1129) and [forum discussion for SVR](https://www.comp.xyz/t/request-for-proposal-rfp-oracle-extractable-value-oev-solution-for-compound-protocol/6786). + +### SVR fee recipient + +SVR generates revenue from liquidators and Compound DAO will receive that revenue as part of the protocol fee. The fee recipient for SVR on Base is set to Compound DAO multisig: 0xb3e79c7cac540ca833015e63d96d3032ba0c4129. + +## Proposal Actions + +The first action updates the cbBTC price feeds in the AERO and WETH markets. + +The second action updates the cbBTC price feeds in the USDC and USDS markets.`; + + const txn = await govDeploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ), 0, 300_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) { + const { + cbBTC, + } = await deploymentManager.getContracts(); + + const aeroComet = new Contract(AERO_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdcComet = new Contract(USDC_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const usdsComet = new Contract(USDS_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + const wethComet = new Contract(WETH_COMET_ADDRESS, abi, await deploymentManager.getSigner()); + + const cbBTCAssetInfoAero = await aeroComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsdc = await usdcComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoUsds = await usdsComet.getAssetInfoByAddress(cbBTC.address); + const cbBTCAssetInfoWeth = await wethComet.getAssetInfoByAddress(cbBTC.address); + + expect(cbBTCAssetInfoAero.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoUsdc.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoUsds.priceFeed).to.equal(newCbEthToUsdPriceFeed); + expect(cbBTCAssetInfoWeth.priceFeed).to.equal(newCbEthToEthPriceFeed); + + expect(cbBTCAssetInfoAero.priceFeed).to.not.equal(oldAeroCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoUsdc.priceFeed).to.not.equal(oldUsdcCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoUsds.priceFeed).to.not.equal(oldUsdsCbBTCToUsdPriceFeed); + expect(cbBTCAssetInfoWeth.priceFeed).to.not.equal(oldWethCbBTCToEthPriceFeed); + + const oldPriceCbBTCToUsdAero = await aeroComet.getPrice(oldAeroCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdAero = await aeroComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdAero).to.be.closeTo(newPriceCbBTCToUsdAero, exp(400, 8)); // within $400 + + const oldPriceCbBTCToUsdUsdc = await usdcComet.getPrice(oldUsdcCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdUsdc = await usdcComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdUsdc).to.be.closeTo(newPriceCbBTCToUsdUsdc, exp(400, 8)); // within $400 + + const oldPriceCbBTCToUsdUsds = await usdsComet.getPrice(oldUsdsCbBTCToUsdPriceFeed); + const newPriceCbBTCToUsdUsds = await usdsComet.getPrice(newCbEthToUsdPriceFeed); + expect(oldPriceCbBTCToUsdUsds).to.be.closeTo(newPriceCbBTCToUsdUsds, exp(400, 8)); // within $400 + + const oldPriceCbBTCToEthWeth = await wethComet.getPrice(oldWethCbBTCToEthPriceFeed); + const newPriceCbBTCToEthWeth = await wethComet.getPrice(newCbEthToEthPriceFeed); + expect(oldPriceCbBTCToEthWeth).to.be.closeTo(newPriceCbBTCToEthWeth, exp(0.188, 8)); // within 0.188 ETH ~ $400 + }, +}); diff --git a/deployments/relations.ts b/deployments/relations.ts index 0c676f9a7..f5c71c0a7 100644 --- a/deployments/relations.ts +++ b/deployments/relations.ts @@ -56,13 +56,14 @@ const relationConfigMap: RelationConfigMap = { // This is a workaround for contracts that don't work in Hardhat fork // invalid opcode when calling symbol() + // Arbitrum-specific known contract mappings if (address === '0xd09acb80c1e8f2291862c4978a008791c9167003') { return 'tETH'; } if (address === '0x5a7facb970d094b6c7ff1df0ea68d99e6e73cbff') { return 'weETH'; } - if (address.toLowerCase() === '0x87eee96d50fb761ad85b1c982d28a042169d61b1') { + if (address === '0x87eee96d50fb761ad85b1c982d28a042169d61b1') { return 'wrsETH'; } // Mantle-specific known contract mapping diff --git a/plugins/scenario/utils/hreForBase.ts b/plugins/scenario/utils/hreForBase.ts index 00f2c28f9..b3d7d139d 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 @@ -89,6 +90,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(); @@ -101,16 +108,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; } @@ -124,7 +136,7 @@ export async function forkedHreForBase(base: ForkSpec): Promise { const { admin } = actors; - const assetIndex = -1; + const assetIndex = 0; const assetConfig = normalizeStructOutput(await configurator.getConfiguration(comet.address)).assetConfigs.at( assetIndex ); @@ -2734,7 +2734,7 @@ scenario( normalizeStructOutput(await configurator.getConfiguration(comet.address)).assetConfigs.at(assetIndex).supplyCap ).to.be.equal(newAssetSupplyCap); - await admin.deployAndUpgradeTo(configurator.address, comet.address); + console.log(await admin.deployAndUpgradeTo(configurator.address, comet.address)); const assetInfo = normalizeStructOutput(await comet.getAssetInfoByAddress(assetConfig.asset)); diff --git a/scenario/LiquidationScenario.ts b/scenario/LiquidationScenario.ts index be64732a7..c22c0e133 100644 --- a/scenario/LiquidationScenario.ts +++ b/scenario/LiquidationScenario.ts @@ -212,7 +212,7 @@ scenario( { filter: async (ctx) => !matchesDeployment(ctx, [ { network: 'base', deployment: 'usds' }, - { network: 'ronin', deployment: 'weth' }, + { network: 'ronin' }, ]), tokenBalances: async (ctx) => ( { diff --git a/scenario/constraints/ProposalConstraint.ts b/scenario/constraints/ProposalConstraint.ts index 36956771c..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 { @@ -87,15 +87,11 @@ export class ProposalConstraint implements StaticConstra 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 4562f920b..05cdf9433 100644 --- a/scenario/utils/index.ts +++ b/scenario/utils/index.ts @@ -12,10 +12,10 @@ 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'; +import { ethers, exp } from '../../test/helpers'; import { DeploymentManager } from '../../plugins/deployment_manager'; import { impersonateAddress } from '../../plugins/scenario/utils'; import { ProposalState, OpenProposal } from '../context/Gov'; @@ -66,7 +66,7 @@ export async function getSignerForProposal( usedSigners.set(key, []); } const signers = usedSigners.get(key); - if (signers.length == 0) { + if(signers.length == 0){ const signer = (await gm.getSigners())[0]; signers.push(signer.address); return signer; @@ -446,7 +446,7 @@ export async function fetchLogs( filter: EventFilter, fromBlock: number, toBlock: number, - BLOCK_SPAN = 2047 + BLOCK_SPAN = 2047 // NB: sadly max for fuji is LESS than 2048 ): Promise { if (toBlock - fromBlock > BLOCK_SPAN) { const midBlock = fromBlock + BLOCK_SPAN; @@ -928,8 +928,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}...`); @@ -939,54 +937,93 @@ export async function tenderlyExecute( const exec1 = bundle[bundle.length - 1].simulation; - console.log(` >>> PROPOSAL EXECUTED ${id} \n`); + console.log(` >>> PROPOSAL EXECUTED ${id}`); 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.messenger, - block_number: Number(block), - block_header: { - timestamp: bdm.hre.ethers.utils.hexlify(Number(timestamp)) - }, - input: msg.callData, - save: true, - save_if_fails: true, - gas_price: 0, - gas_limit: 16_777_215, - }; - }); + console.log(`Link: https://www.tdly.co/shared/simulation/${exec1.id} \n`); + + const bdms = [bdm]; + for (const dm of gdm.bridgedDeploymentManagers.values()) { + if (!bdms.includes(dm)) { + bdms.push(dm); + } + } + + // make bdm contain only 1 dm per network + const uniqueBdms = new Map(); + for (const dm of bdms) { + const chainId = dm.hre.ethers.provider.network.chainId; + if (!uniqueBdms.has(chainId)) { + uniqueBdms.set(chainId, dm); + } + } + bdms.length = 0; + bdms.push(...uniqueBdms.values()); + + + 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); + + 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 executeProposalSig = ethers.utils.id('executeProposal(uint256)').substring(0, 10); + + const latestL2 = await currentBdm.hre.ethers.provider.getBlock('latest'); + const maxEta = Math.max(...proposals.map(p => Number(p.eta || 0))) + delay.toNumber(); + const T0L2 = Math.max(latestL2.timestamp, maxEta + 1); + const B0L2 = Number(latestL2.number) + 1; + + let previousBlock = latestL2.number; + let previousTimestamp = T0L2; + const simsL2 = relayMessages.map((msg) => { + let block = previousBlock; + let timestamp = previousTimestamp; + + if (msg.callData.startsWith(executeProposalSig) && !msg.eta) { + block = block + 1; + timestamp = timestamp + delay.toNumber() + 1; + } + + previousBlock = block; + previousTimestamp = timestamp; - 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}`); + 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)); + + // filter from bundle every entry with simulation.input that starts with 0x0d61b519 i.e. executeProposal(uint256) + const filteredBundle = bundle2.filter(entry => entry.simulation.input.startsWith(executeProposalSig)); + for (const sim of filteredBundle) { + 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} \n`); + } + } + } } } @@ -1002,10 +1039,12 @@ async function simulateBundle( const results = []; for (const sim of simulations) { - 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 || ''; // Merge rolling state changes with simulation's own state_objects - const stateObjects = sim.state_objects + const stateObjects = sim.state_objects ? { ...rollingStateChanges, ...sim.state_objects } : rollingStateChanges; @@ -1055,12 +1094,15 @@ async function simulateBundle( 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`, {}, @@ -1522,25 +1564,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; + } } } @@ -1642,4 +1685,4 @@ export async function supportsMarketAdminPermissionChecker(ctx: CometContext): P } catch (e) { return false; } -} \ No newline at end of file +} diff --git a/scenario/utils/isBridgeProposal.ts b/scenario/utils/isBridgeProposal.ts index f2d4ac362..63957eccf 100644 --- a/scenario/utils/isBridgeProposal.ts +++ b/scenario/utils/isBridgeProposal.ts @@ -1,142 +1,155 @@ 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())); - } - 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 '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); - // } - 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); + 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 '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)); + + let deploymentToken: string; + + let dm: DeploymentManager; + let existingBridgedDm: DeploymentManager | undefined; + for (const cachedDm of governanceDeploymentManager.bridgedDeploymentManagers.values()) { + if (cachedDm.network === bridgeNetwork) { + existingBridgedDm = cachedDm; + break; + } } - default: { - const tag = `[${bridgeNetwork} -> ${governanceDeploymentManager.network}]`; - throw new Error(`${tag} Unable to determine whether to relay Proposal ${openProposal.id}`); + + 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); } + + existingBridgeManagers[bridgeNetwork] = dm; + bridgeManagers.push(dm); } + 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 e357eceb4..195f13fc6 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'; @@ -142,7 +142,7 @@ export default async function relayBaseMessage( } } else if (target === bridgeReceiver.address) { // Cross-chain message passing - if (!tenderlyLogs && relayMessageTxn) { + if (relayMessageTxn) { const proposalCreatedEvent = relayMessageTxn.events.find( (event) => event.address === bridgeReceiver.address ); @@ -154,28 +154,9 @@ export default async function relayBaseMessage( openBridgedProposals.push({ id, eta }); } } else { - throw new Error( - `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message` - ); - } - } - - // Handle proposal creation for tenderly - if (tenderlyLogs) { - // We need to check for ProposalCreated events since we don't get them in the loop above - 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 }); + // throw error only on last relay message and no proposal created event found + if(sentMessageEvents.indexOf(sentMessageEvent) === sentMessageEvents.length - 1 && openBridgedProposals.length === 0) + throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); } } @@ -207,4 +188,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..0f3986550 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, @@ -245,9 +245,9 @@ export default async function relayLineaMessage( // Add the proposal to the list of open bridged proposals to be executed after all the messages have been relayed openBridgedProposals.push({ id, eta }); } else { - throw new Error( - `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message` - ); + // throw error only on last relay message and no proposal created event found + if(messageSentEvents.indexOf(messageSentEvent) === messageSentEvents.length - 1 && openBridgedProposals.length === 0) + throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); } } @@ -276,6 +276,7 @@ export default async function relayLineaMessage( `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Executed bridged proposal ${id}` ); } + return openBridgedProposals; } diff --git a/scenario/utils/relayMantleMessage.ts b/scenario/utils/relayMantleMessage.ts index 812255b75..c052e0184 100644 --- a/scenario/utils/relayMantleMessage.ts +++ b/scenario/utils/relayMantleMessage.ts @@ -127,7 +127,9 @@ export default async function relayMantleMessage( // Add the proposal to the list of open bridged proposals to be executed after all the messages have been relayed openBridgedProposals.push({ id, eta }); } else { - throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); + // throw error only on last relay message and no proposal created event found + if(sentMessageEvents.indexOf(sentMessageEvent) === sentMessageEvents.length - 1 && openBridgedProposals.length === 0) + throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); } } @@ -158,5 +160,6 @@ export default async function relayMantleMessage( `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Executed bridged proposal ${id}` ); } + return openBridgedProposals; } diff --git a/scenario/utils/relayMessage.ts b/scenario/utils/relayMessage.ts index fa0523665..aa3b49f74 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'; @@ -20,19 +20,31 @@ export default async function relayMessage( 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 +86,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..0ceea8092 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'; @@ -144,7 +144,9 @@ export default async function relayOptimismMessage( throw e; } } else { - throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); + // throw error only on last relay message and no proposal created event found + if(sentMessageEvents.indexOf(sentMessageEvent) === sentMessageEvents.length - 1 && openBridgedProposals.length === 0) + throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); } // Execute open bridged proposals now that all messages have been bridged @@ -172,7 +174,117 @@ export default async function relayOptimismMessage( `[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Executed bridged proposal ${id}` ); } + } + + 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; + } - return openBridgedProposals; + 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/relayScrollMessage.ts b/scenario/utils/relayScrollMessage.ts index 686658cd0..3d6b5b7bf 100644 --- a/scenario/utils/relayScrollMessage.ts +++ b/scenario/utils/relayScrollMessage.ts @@ -167,7 +167,9 @@ export default async function relayScrollMessage( // Add the proposal to the list of open bridged proposals to be executed after all the messages have been relayed openBridgedProposals.push({ id, eta }); } else { - throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); + // throw error only on last relay message and no proposal created event found + if(messageSentEvents.indexOf(messageSentEvent) === messageSentEvents.length - 1 && openBridgedProposals.length === 0) + throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); } } diff --git a/scenario/utils/relayUnichainMessage.ts b/scenario/utils/relayUnichainMessage.ts index 5b3db9b5b..5f49b9773 100644 --- a/scenario/utils/relayUnichainMessage.ts +++ b/scenario/utils/relayUnichainMessage.ts @@ -124,7 +124,7 @@ export async function relayUnichainMessage( } } else if (target === bridgeReceiver.address) { // Cross-chain message passing - if (!tenderlyLogs && relayMessageTxn) { + if (relayMessageTxn) { const proposalCreatedEvent = relayMessageTxn.events.find(event => event.address === bridgeReceiver.address); const { args: { id, eta } } = bridgeReceiver.interface.parseLog(proposalCreatedEvent); @@ -132,26 +132,9 @@ export async function relayUnichainMessage( openBridgedProposals.push({ id, eta }); } } else { - throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); - } - } - - // Handle proposal creation for tenderly - if (tenderlyLogs) { - // We need to check for ProposalCreated events since we don't get them in the loop above - 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 }); + // throw error only on last relay message and no proposal created event found + if(sentMessageEvents.indexOf(sentMessageEvent) === sentMessageEvents.length - 1 && openBridgedProposals.length === 0) + throw new Error(`[${governanceDeploymentManager.network} -> ${bridgeDeploymentManager.network}] Unrecognized target for cross-chain message`); } } 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) {