diff --git a/deployments/base/aero/migrations/1761125221_upgrade_to_capo_price_feeds.ts b/deployments/base/aero/migrations/1761125221_upgrade_to_capo_price_feeds.ts new file mode 100644 index 000000000..d58fd5fb7 --- /dev/null +++ b/deployments/base/aero/migrations/1761125221_upgrade_to_capo_price_feeds.ts @@ -0,0 +1,160 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, proposal, exp } from '../../../../src/deploy'; +import { utils } from 'ethers'; +import { AggregatorV3Interface } from '../../../../build/types'; + +const ETH_USD_PRICE_FEED = '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70'; + +const WSTETH_ADDRESS = '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452'; +const WSTETH_STETH_PRICE_FEED_ADDRESS = '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061'; + +const FEED_DECIMALS = 8; +const blockToFetch = 36000000; + +let newWstETHPriceFeed: string; +let oldWstETHPriceFeed: string; + +export default migration('1761125221_upgrade_to_capo_price_feeds', { + async prepare(deploymentManager: DeploymentManager) { + const { timelock } = await deploymentManager.getContracts(); + const blockToFetchTimestamp = (await deploymentManager.hre.ethers.provider.getBlock(blockToFetch))!.timestamp; + + //1. wstEth + const rateProviderWstEth = await deploymentManager.existing('wstEth:priceFeed', WSTETH_STETH_PRICE_FEED_ADDRESS, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioWstEth] = await rateProviderWstEth.latestRoundData({ blockTag: blockToFetch }); + + const wstEthCapoPriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + ETH_USD_PRICE_FEED, + WSTETH_STETH_PRICE_FEED_ADDRESS, + 'wstETH / USD CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioWstEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0404, 4) + } + ], + true + ); + + return { + wstEthCapoPriceFeedAddress: wstEthCapoPriceFeed.address + }; + }, + + async enact(deploymentManager: DeploymentManager, govDeploymentManager: DeploymentManager, { + wstEthCapoPriceFeedAddress + }) { + newWstETHPriceFeed = wstEthCapoPriceFeedAddress; + + const trace = deploymentManager.tracer(); + + const { + configurator, + comet, + bridgeReceiver, + cometAdmin + } = await deploymentManager.getContracts(); + + const { + governor, + baseL1CrossDomainMessenger + } = await govDeploymentManager.getContracts(); + + const updateWstEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + WSTETH_ADDRESS, + wstEthCapoPriceFeedAddress + ) + ); + + const deployAndUpgradeToCalldata = await calldata( + cometAdmin.populateTransaction.deployAndUpgradeTo( + configurator.address, + comet.address + ) + ); + + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [configurator.address, cometAdmin.address], + [0, 0], + ['updateAssetPriceFeed(address,address,address)', 'deployAndUpgradeTo(address,address)'], + [updateWstEthPriceFeedCalldata, deployAndUpgradeToCalldata], + ] + ); + + [,, oldWstETHPriceFeed] = await comet.getAssetInfoByAddress(WSTETH_ADDRESS); + + const mainnetActions = [ + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [ + bridgeReceiver.address, + l2ProposalData, + 3_000_000 + ] + }, + ]; + + const description = `# Update wstETH price feed in cAEROv3 on Base with CAPO implementation. + +## Proposal summary + +This proposal updates existing price feeds for wstETH on the AERO market on Base. + +### CAPO summary + +CAPO is a price oracle adapter designed to support assets that grow gradually relative to a base asset - such as liquid staking tokens that accumulate yield over time. It provides a mechanism to track this expected growth while protecting downstream protocol from sudden or manipulated price spikes. wstETH price feed is updated to their CAPO implementations. + +Further detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/1038) and [forum discussion for CAPO](https://www.comp.xyz/t/woof-correlated-assets-price-oracle-capo/6245). + +### CAPO audit + +CAPO has been audited by [OpenZeppelin](https://www.comp.xyz/t/capo-price-feed-audit/6631, as well as the LST / LRT implementation [here](https://www.comp.xyz/t/capo-lst-lrt-audit/7118). + +## Proposal actions + +The first action updates wstETH price feed to the CAPO implementation. This sends the encoded 'updateAssetPriceFeed' and 'deployAndUpgradeTo' calls across the bridge to the governance receiver on Base. +`; + + const txn = await deploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ) + ); + const event = txn.events.find( + (event: { event: string }) => event.event === 'ProposalCreated' + ); + const [proposalId] = event.args; + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify(deploymentManager: DeploymentManager) { + const { comet, configurator } = await deploymentManager.getContracts(); + + const wstETHIndexInComet = await configurator.getAssetIndex(comet.address, WSTETH_ADDRESS); + + // Check if the price feeds are set correctly. + const wstETHInCometInfo = await comet.getAssetInfoByAddress(WSTETH_ADDRESS); + const wstETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[wstETHIndexInComet]; + + expect(wstETHInCometInfo.priceFeed).to.eq(newWstETHPriceFeed); + expect(wstETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newWstETHPriceFeed); + expect(await comet.getPrice(newWstETHPriceFeed)).to.equal(await comet.getPrice(oldWstETHPriceFeed)); + }, +}); diff --git a/deployments/base/weth/migrations/1761228877_upgrade_to_capo_price_feeds.ts b/deployments/base/weth/migrations/1761228877_upgrade_to_capo_price_feeds.ts new file mode 100644 index 000000000..56cffa4f0 --- /dev/null +++ b/deployments/base/weth/migrations/1761228877_upgrade_to_capo_price_feeds.ts @@ -0,0 +1,319 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { calldata, proposal, exp } from '../../../../src/deploy'; +import { utils } from 'ethers'; +import { AggregatorV3Interface } from '../../../../build/types'; + +const WSTETH_ADDRESS = '0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452'; +const WSTETH_STETH_PRICE_FEED_ADDRESS = '0xB88BAc61a4Ca37C43a3725912B1f472c9A5bc061'; + +const EZETH_ADDRESS = '0x2416092f143378750bb29b79ed961ab195cceea5'; +const EZETH_TO_ETH_PRICE_FEED_ADDRESS = '0xC4300B7CF0646F0Fe4C5B2ACFCCC4dCA1346f5d8'; + +const WRSETH_ADDRESS = '0xEDfa23602D0EC14714057867A78d01e94176BEA0'; +const WRSETH_ORACLE = '0xe8dD07CCf5BC4922424140E44Eb970F5950725ef'; + +const WEETH_ADDRESS = '0x04C0599Ae5A44757c0af6F9eC3b93da8976c150A'; +const WEETH_STETH_PRICE_FEED_ADDRESS = '0x35e9D7001819Ea3B39Da906aE6b06A62cfe2c181'; + +const blockToFetch = 36000000; + +let newWstETHToETHPriceFeed: string; +let newEzETHToETHPriceFeed: string; +let newWrsEthToETHPriceFeed: string; +let newWeEthToETHPriceFeed: string; + +let oldWstETHToETHPriceFeed: string; +let oldEzETHToETHPriceFeed: string; +let oldWrsEthToETHPriceFeed: string; +let oldWeEthToETHPriceFeed: string; + +const FEED_DECIMALS = 8; +export default migration('1761228877_upgrade_to_capo_price_feeds', { + async prepare(deploymentManager: DeploymentManager) { + const { timelock } = await deploymentManager.getContracts(); + const blockToFetchTimestamp = (await deploymentManager.hre.ethers.provider.getBlock(blockToFetch))!.timestamp; + const constantPriceFeed = await deploymentManager.fromDep('WETH:priceFeed', 'base', 'weth'); + + //1. wstEth + const rateProviderWstEth = await deploymentManager.existing('wstETH:_rateProvider', WSTETH_STETH_PRICE_FEED_ADDRESS, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioWstEth] = await rateProviderWstEth.latestRoundData({blockTag: blockToFetch}); + + const wstEthCapoPriceFeed = await deploymentManager.deploy( + 'wstETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + constantPriceFeed.address, + WSTETH_STETH_PRICE_FEED_ADDRESS, + 'wstETH / ETH CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioWstEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0404, 4) + } + ], + true + ); + + //2. ezEth + const rateProviderEzEth = await deploymentManager.existing('ezETH:_rateProvider', EZETH_TO_ETH_PRICE_FEED_ADDRESS, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioEzEth] = await rateProviderEzEth.latestRoundData({blockTag: blockToFetch}); + const ezEthCapoPriceFeed = await deploymentManager.deploy( + 'ezETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + constantPriceFeed.address, + EZETH_TO_ETH_PRICE_FEED_ADDRESS, + 'ezETH / ETH CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioEzEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0707, 4) + } + ], + true + ); + + const rateProviderRsEth = await deploymentManager.existing('rsETH:_rateProvider', WRSETH_ORACLE, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioWrsEth] = await rateProviderRsEth.latestRoundData({blockTag: blockToFetch}); + const rsEthCapoPriceFeed = await deploymentManager.deploy( + 'rsETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + constantPriceFeed.address, + WRSETH_ORACLE, + 'rsETH / ETH CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioWrsEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0554, 4) + } + ], + true + ); + + + const rateProviderWeEth = await deploymentManager.existing('weETH:_rateProvider', WEETH_STETH_PRICE_FEED_ADDRESS, 'base', 'contracts/capo/contracts/interfaces/AggregatorV3Interface.sol:AggregatorV3Interface') as AggregatorV3Interface; + const [, currentRatioWeEth] = await rateProviderWeEth.latestRoundData({blockTag: blockToFetch}); + const weEthCapoPriceFeed = await deploymentManager.deploy( + 'weETH:priceFeed', + 'capo/contracts/ChainlinkCorrelatedAssetsPriceOracle.sol', + [ + timelock.address, + constantPriceFeed.address, + WEETH_STETH_PRICE_FEED_ADDRESS, + 'weETH / ETH CAPO Price Feed', + FEED_DECIMALS, + 3600, + { + snapshotRatio: currentRatioWeEth, + snapshotTimestamp: blockToFetchTimestamp, + maxYearlyRatioGrowthPercent: exp(0.0323, 4) + } + ], + true + ); + + return { + wstEthCapoPriceFeedAddress: wstEthCapoPriceFeed.address, + ezEthCapoPriceFeedAddress: ezEthCapoPriceFeed.address, + rsEthCapoPriceFeedAddress: rsEthCapoPriceFeed.address, + weEthCapoPriceFeedAddress: weEthCapoPriceFeed.address + }; + }, + + async enact(deploymentManager: DeploymentManager, govDeploymentManager, { + wstEthCapoPriceFeedAddress, + ezEthCapoPriceFeedAddress, + rsEthCapoPriceFeedAddress, + weEthCapoPriceFeedAddress + }) { + + newWstETHToETHPriceFeed = wstEthCapoPriceFeedAddress; + newEzETHToETHPriceFeed = ezEthCapoPriceFeedAddress; + newWrsEthToETHPriceFeed = rsEthCapoPriceFeedAddress; + newWeEthToETHPriceFeed = weEthCapoPriceFeedAddress; + + const trace = deploymentManager.tracer(); + + const { + configurator, + comet, + bridgeReceiver, + cometAdmin + } = await deploymentManager.getContracts(); + + const { + governor, + baseL1CrossDomainMessenger + } = await govDeploymentManager.getContracts(); + + const updateEzEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + EZETH_ADDRESS, + ezEthCapoPriceFeedAddress + ) + ); + + const updateWstEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + WSTETH_ADDRESS, + wstEthCapoPriceFeedAddress + ) + ); + + const updateRsEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + WRSETH_ADDRESS, + rsEthCapoPriceFeedAddress + ) + ); + + const updateWeEthPriceFeedCalldata = await calldata( + configurator.populateTransaction.updateAssetPriceFeed( + comet.address, + WEETH_ADDRESS, + weEthCapoPriceFeedAddress + ) + ); + + const deployAndUpgradeToCalldata = utils.defaultAbiCoder.encode( + ['address', 'address'], + [configurator.address, comet.address] + ); + + const l2ProposalData = utils.defaultAbiCoder.encode( + ['address[]', 'uint256[]', 'string[]', 'bytes[]'], + [ + [ + configurator.address, + configurator.address, + configurator.address, + configurator.address, + cometAdmin.address + ], + [0, 0, 0, 0, 0], + [ + 'updateAssetPriceFeed(address,address,address)', + 'updateAssetPriceFeed(address,address,address)', + 'updateAssetPriceFeed(address,address,address)', + 'updateAssetPriceFeed(address,address,address)', + 'deployAndUpgradeTo(address,address)' + ], + [ + updateWstEthPriceFeedCalldata, + updateEzEthPriceFeedCalldata, + updateRsEthPriceFeedCalldata, + updateWeEthPriceFeedCalldata, + deployAndUpgradeToCalldata + ], + ] + ); + + [,, oldWstETHToETHPriceFeed] = await comet.getAssetInfoByAddress(WSTETH_ADDRESS); + [,, oldEzETHToETHPriceFeed] = await comet.getAssetInfoByAddress(EZETH_ADDRESS); + [,, oldWrsEthToETHPriceFeed] = await comet.getAssetInfoByAddress(WRSETH_ADDRESS); + [,, oldWeEthToETHPriceFeed] = await comet.getAssetInfoByAddress(WEETH_ADDRESS); + + const mainnetActions = [ + { + contract: baseL1CrossDomainMessenger, + signature: 'sendMessage(address,bytes,uint32)', + args: [ + bridgeReceiver.address, + l2ProposalData, + 3_000_000 + ] + }, + ]; + + const description = `# Update price feeds in cWETHv3 on Base with CAPO implementation. + +## Proposal summary + +This proposal updates existing price feeds for wstETH, ezETH, rsETH, and weETH on the WETH market on Base. + +### CAPO summary + +CAPO is a price oracle adapter designed to support assets that grow gradually relative to a base asset - such as liquid staking tokens that accumulate yield over time. It provides a mechanism to track this expected growth while protecting downstream protocol from sudden or manipulated price spikes. wstETH, ezETH, rsETH, and weETH price feeds are updated to their CAPO implementations. + +Further detailed information can be found on the corresponding [proposal pull request](https://github.com/compound-finance/comet/pull/1040) and [forum discussion for CAPO](https://www.comp.xyz/t/woof-correlated-assets-price-oracle-capo/6245). + +### CAPO audit + +CAPO has been audited by [OpenZeppelin](https://www.comp.xyz/t/capo-price-feed-audit/6631, as well as the LST / LRT implementation [here](https://www.comp.xyz/t/capo-lst-lrt-audit/7118). + +## Proposal actions + +The first action updates wstETH, ezETH, rsETH, and weETH price feeds to the CAPO implementation. This sends the encoded 'updateAssetPriceFeed' and 'deployAndUpgradeTo' calls across the bridge to the governance receiver on Base. +`; + const txn = await govDeploymentManager.retry(async () => + trace( + await governor.propose(...(await proposal(mainnetActions, description))) + ) + ); + + const event = txn.events.find( + (event: { event: string }) => event.event === 'ProposalCreated' + ); + const [proposalId] = event.args; + trace(`Created proposal ${proposalId}.`); + }, + + async enacted(deploymentManager: DeploymentManager): Promise { + return true; + }, + + async verify(deploymentManager: DeploymentManager) { + const { comet, configurator } = await deploymentManager.getContracts(); + + // 1. wstETH + const wstETHIndexInComet = await configurator.getAssetIndex(comet.address, WSTETH_ADDRESS); + const wstETHInCometInfo = await comet.getAssetInfoByAddress(WSTETH_ADDRESS); + const wstETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[wstETHIndexInComet]; + + expect(wstETHInCometInfo.priceFeed).to.eq(newWstETHToETHPriceFeed); + expect(wstETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newWstETHToETHPriceFeed); + expect(await comet.getPrice(newWstETHToETHPriceFeed)).to.be.closeTo(await comet.getPrice(oldWstETHToETHPriceFeed), 1e6); + + // 2. ezETH + const ezETHIndexInComet = await configurator.getAssetIndex(comet.address, EZETH_ADDRESS); + const ezETHInCometInfo = await comet.getAssetInfoByAddress(EZETH_ADDRESS); + const ezETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[ezETHIndexInComet]; + + expect(ezETHInCometInfo.priceFeed).to.eq(newEzETHToETHPriceFeed); + expect(ezETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newEzETHToETHPriceFeed); + expect(await comet.getPrice(newEzETHToETHPriceFeed)).to.equal(await comet.getPrice(oldEzETHToETHPriceFeed)); + + // 3. wrsETH + const wrsETHIndexInComet = await configurator.getAssetIndex(comet.address, WRSETH_ADDRESS); + const wrsETHInCometInfo = await comet.getAssetInfoByAddress(WRSETH_ADDRESS); + const wrsETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[wrsETHIndexInComet]; + + expect(wrsETHInCometInfo.priceFeed).to.eq(newWrsEthToETHPriceFeed); + expect(wrsETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newWrsEthToETHPriceFeed); + expect(await comet.getPrice(newWrsEthToETHPriceFeed)).to.equal(await comet.getPrice(oldWrsEthToETHPriceFeed)); + + // 4. weETH + const weETHIndexInComet = await configurator.getAssetIndex(comet.address, WEETH_ADDRESS); + const weETHInCometInfo = await comet.getAssetInfoByAddress(WEETH_ADDRESS); + const weETHInConfiguratorInfoWETHComet = (await configurator.getConfiguration(comet.address)).assetConfigs[weETHIndexInComet]; + + expect(weETHInCometInfo.priceFeed).to.eq(newWeEthToETHPriceFeed); + expect(weETHInConfiguratorInfoWETHComet.priceFeed).to.eq(newWeEthToETHPriceFeed); + expect(await comet.getPrice(newWeEthToETHPriceFeed)).to.equal(await comet.getPrice(oldWeEthToETHPriceFeed)); + }, +}); diff --git a/deployments/mainnet/usdt/migrations/1735299664_upgrade_to_capo_price_feeds.ts b/deployments/mainnet/usdt/migrations/1735299664_upgrade_to_capo_price_feeds.ts index 399fe8401..e724eec36 100644 --- a/deployments/mainnet/usdt/migrations/1735299664_upgrade_to_capo_price_feeds.ts +++ b/deployments/mainnet/usdt/migrations/1735299664_upgrade_to_capo_price_feeds.ts @@ -1,15 +1,10 @@ import { expect } from 'chai'; import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; import { migration } from '../../../../plugins/deployment_manager/Migration'; -import { proposal } from '../../../../src/deploy'; -import { Numeric } from '../../../../test/helpers'; +import { proposal, exp } from '../../../../src/deploy'; import { IWstETH, IRateProvider, AggregatorV3Interface } from '../../../../build/types'; import { constants } from 'ethers'; -export function exp(i: number, d: Numeric = 0, r: Numeric = 6): bigint { - return (BigInt(Math.floor(i * 10 ** Number(r))) * 10n ** BigInt(d)) / 10n ** BigInt(r); -} - const WETH_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; const ETH_USD_SVR_PRICE_FEED = '0xc0053f3FBcCD593758258334Dfce24C2A9A673aD'; @@ -262,11 +257,11 @@ export default migration('1735299664_upgrade_to_capo_price_feeds', { This proposal updates existing price feeds for wstETH, sFRAX, weETH, WBTC, WETH, mETH, COMP, and LINK on the USDT market on Mainnet. -SVR summery +## SVR summary [RFP process](https://www.comp.xyz/t/oev-rfp-process-update-july-2025/6945) and community [vote](https://snapshot.box/#/s:comp-vote.eth/proposal/0x98a3873319cdb5a4c66b6f862752bdcfb40d443a5b9c2f9472188d7ed5f9f2e0) passed and decided to implement Chainlink's SVR solution for Mainnet markets, this proposal updates wstETH, WBTC, WETH, LINK, weETH, mETH, COMP price feeds to support SVR implementations. -CAPO summery +## CAPO summary CAPO is a price oracle adapter designed to support assets that grow gradually relative to a base asset - such as liquid staking tokens that accumulate yield over time. It provides a mechanism to track this expected growth while protecting downstream protocol from sudden or manipulated price spikes. wstETH, sFRAX, weETH, mETH price feeds are updated to their CAPO implementations. @@ -307,7 +302,7 @@ The ninth action deploys and upgrades Comet to a new version. }, async enacted(): Promise { - return false; + return true; }, async verify(deploymentManager: DeploymentManager) { diff --git a/deployments/mainnet/usdt/migrations/1770826596_rescue_funds.ts b/deployments/mainnet/usdt/migrations/1770826596_rescue_funds.ts new file mode 100644 index 000000000..b4528b41b --- /dev/null +++ b/deployments/mainnet/usdt/migrations/1770826596_rescue_funds.ts @@ -0,0 +1,75 @@ +import { expect } from 'chai'; +import { DeploymentManager } from '../../../../plugins/deployment_manager/DeploymentManager'; +import { migration } from '../../../../plugins/deployment_manager/Migration'; +import { exp, proposal } from '../../../../src/deploy'; +import { BigNumber } from 'ethers'; + +const amountOfAccidentalTransfers = exp(19_958.414155, 6); + +const USER_ADDRESS = '0x34074a8706e952a02f8a3def416bab745b54a3ed'; + +let balanceBefore: BigNumber; + +export default migration('1770826596_rescue_funds', { + async prepare() { + return {}; + }, + + async enact(deploymentManager: DeploymentManager) { + const trace = deploymentManager.tracer(); + + const { + governor, + comet, + USDT + } = await deploymentManager.getContracts(); + + const mainnetActions = [ + // 1. Return accidentally sent funds to the user + { + contract: comet, + signature: 'withdrawReserves(address,uint256)', + args: [USER_ADDRESS, amountOfAccidentalTransfers], + }, + ]; + + const description = `# Return accidentally send funds + +## Proposal summary + +This proposal returns accidentally sent funds to a cUSDTv3 on Mainnet. Funds will be sent back to the user. + +Further detailed information can be found on the corresponding [forum discussion](https://www.comp.xyz/t/compound-s-policy-on-accidental-erc-20-transfersto-comet-compound-v3-contracts/7577). + +Accidental transfer tx hash: 0x92b03289ad09323ec897e106442096f3fc07f4c9cee4e02add7ee2b672d865e3 +Amount sent: 19_958.414155 +User address: 0x34074a8706e952a02f8a3def416bab745b54a3ed + +## Proposal actions + +The first action withdraws accidentally sent funds from a comet and transfers them back to the user. +`; + balanceBefore = await USDT.balanceOf(USER_ADDRESS); + const txn = await deploymentManager.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 { USDT } = await deploymentManager.getContracts(); + + expect(balanceBefore.add('19958414155')).to.equal(await USDT.balanceOf(USER_ADDRESS)); + }, +}); diff --git a/deployments/ronin/weth/deploy.ts b/deployments/ronin/weth/deploy.ts index 06bebf97e..383b9b10a 100644 --- a/deployments/ronin/weth/deploy.ts +++ b/deployments/ronin/weth/deploy.ts @@ -73,6 +73,11 @@ async function deployContracts( 'ronin' ); + const l2CCIPOnRamp = await deploymentManager.existing( + 'l2CCIPOnRamp', + '0x02b60267bceeaFDC45005e0Fa0dd783eFeBc9F1b', + 'ronin' + ); // Deploy Local Timelock const localTimelock = await deploymentManager.deploy( @@ -195,6 +200,7 @@ async function deployContracts( bridgeReceiver, l2CCIPRouter, l2CCIPOffRamp, + l2CCIPOnRamp, roninl2NativeBridge, bulker, // COMP diff --git a/deployments/ronin/weth/roots.json b/deployments/ronin/weth/roots.json index 7756d849c..86fa59c10 100644 --- a/deployments/ronin/weth/roots.json +++ b/deployments/ronin/weth/roots.json @@ -6,6 +6,7 @@ "bridgeReceiver": "0x2c7EfA766338D33B9192dB1fB5D170Bdc03ef3F9", "l2CCIPRouter": "0x46527571D5D1B68eE7Eb60B18A32e6C60DcEAf99", "l2CCIPOffRamp": "0x320A10449556388503Fd71D74A16AB52e0BD1dEb", + "l2CCIPOnRamp": "0x02b60267bceeaFDC45005e0Fa0dd783eFeBc9F1b", "roninl2NativeBridge": "0x0cf8ff40a508bdbc39fbe1bb679dcba64e65c7df", "bulker": "0x840281FaD56DD88afba052B7F18Be2A65796Ecc6", "l2TokenAdminRegistry": "0x90e83d532A4aD13940139c8ACE0B93b0DdbD323a" diff --git a/deployments/ronin/wron/deploy.ts b/deployments/ronin/wron/deploy.ts index eec9022bb..1aae0a786 100644 --- a/deployments/ronin/wron/deploy.ts +++ b/deployments/ronin/wron/deploy.ts @@ -79,6 +79,12 @@ async function deployContracts( 'ronin' ); + const l2CCIPOnRamp = await deploymentManager.existing( + 'l2CCIPOnRamp', + '0x02b60267bceeaFDC45005e0Fa0dd783eFeBc9F1b', + 'ronin' + ); + // Deploy all Comet-related contracts const deployed = await deployComet(deploymentManager, deploySpec, {}, true); @@ -86,6 +92,7 @@ async function deployContracts( ...deployed, bridgeReceiver, l2CCIPRouter, + l2CCIPOnRamp, l2CCIPOffRamp, l2TokenAdminRegistry, bulker diff --git a/deployments/ronin/wron/roots.json b/deployments/ronin/wron/roots.json index 13f7b0b98..a616419a3 100644 --- a/deployments/ronin/wron/roots.json +++ b/deployments/ronin/wron/roots.json @@ -5,6 +5,7 @@ "cometFactory": "0x4DF9E0f8e94a7A8A9aEa6010CD9d341F8Ecfe4c6", "bridgeReceiver": "0x2c7EfA766338D33B9192dB1fB5D170Bdc03ef3F9", "l2CCIPRouter": "0x46527571D5D1B68eE7Eb60B18A32e6C60DcEAf99", + "l2CCIPOnRamp": "0x02b60267bceeaFDC45005e0Fa0dd783eFeBc9F1b", "l2CCIPOffRamp": "0x320A10449556388503Fd71D74A16AB52e0BD1dEb", "roninl2NativeBridge": "0x0cf8ff40a508bdbc39fbe1bb679dcba64e65c7df", "l2TokenAdminRegistry": "0x90e83d532A4aD13940139c8ACE0B93b0DdbD323a", diff --git a/forge/script/marketupdates/helpers/GovernanceHelper.sol b/forge/script/marketupdates/helpers/GovernanceHelper.sol index eaaf77422..f56257cfb 100644 --- a/forge/script/marketupdates/helpers/GovernanceHelper.sol +++ b/forge/script/marketupdates/helpers/GovernanceHelper.sol @@ -210,7 +210,7 @@ library GovernanceHelper { } function voteOnProposal(Vm vm, uint256 proposalId, address proposalCreator) public { - address[12] memory voters = getTopDelegates(); + address[11] memory voters = getTopDelegates(); console.log("Voting on proposal with ID: ", proposalId); console.log("Proposal Creator: ", proposalCreator); @@ -225,12 +225,11 @@ library GovernanceHelper { } } - function getTopDelegates() public pure returns (address[12] memory) { + function getTopDelegates() public pure returns (address[11] memory) { return [ 0x070341aA5Ed571f0FB2c4a5641409B1A46b4961b, 0x0579A616689f7ed748dC07692A3F150D44b0CA09, - 0x9AA835Bc7b8cE13B9B0C9764A52FbF71AC62cCF1, - 0x7E959eAB54932f5cFd10239160a7fd6474171318, + 0x66cD62c6F8A4BB0Cd8720488BCBd1A6221B765F9, 0x2210dc066aacB03C9676C4F1b36084Af14cCd02E, 0x88F659b4B6D5614B991c6404b34f821e10390eC0, 0xb06DF4dD01a5c5782f360aDA9345C87E86ADAe3D, diff --git a/hardhat.config.ts b/hardhat.config.ts index 63d2caae8..480c078b3 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -342,6 +342,7 @@ const config: HardhatUserConfig = { hardforkHistory: { berlin: 1, london: 2, + shanghai: 3, } }; return acc; diff --git a/plugins/deployment_manager/DeploymentManager.ts b/plugins/deployment_manager/DeploymentManager.ts index 2bcfc2357..5ddf68d71 100644 --- a/plugins/deployment_manager/DeploymentManager.ts +++ b/plugins/deployment_manager/DeploymentManager.ts @@ -282,7 +282,7 @@ export class DeploymentManager { } } - stashRelayMessage(messanger: string, callData: string, signer: string) { + stashRelayMessage(messenger: string, callData: string, signer: string) { try { const cacheDir = path.resolve(__dirname, '../..', 'cache'); mkdirSync(cacheDir, { recursive: true }); @@ -301,7 +301,7 @@ export class DeploymentManager { } } - const newEntry = { messanger, callData, signer }; + const newEntry = { messenger, callData, signer }; if (!data.some(entry => JSON.stringify(entry) === JSON.stringify(newEntry))) { data.push(newEntry); writeFileSync(file, JSON.stringify(data, null, 2), 'utf8'); diff --git a/plugins/deployment_manager/Import.ts b/plugins/deployment_manager/Import.ts index 2431acc89..1c3fe593e 100644 --- a/plugins/deployment_manager/Import.ts +++ b/plugins/deployment_manager/Import.ts @@ -44,7 +44,7 @@ export async function importContract( console.warn(`Import failed for ${network}@${address} (${e.message}), retrying in ${retryDelay / 1000}s; ${retries} retries left`); await new Promise(ok => setTimeout(ok, retryDelay)); - return importContract(network, address, retries - 1, retryDelay * 2); + return importContract(network, address, retries - 1, retryDelay * 2 > 10000 ? 10000 : retryDelay * 2); } } @@ -61,7 +61,7 @@ export async function importContract( console.warn(`Import failed for ${network}@${address} (${e.message}), retrying in ${retryDelay / 1000}s; ${retries} retries left`); await new Promise(ok => setTimeout(ok, retryDelay)); - return importContract(network, address, retries - 1, retryDelay * 2); + return importContract(network, address, retries - 1, retryDelay * 2 > 10000 ? 10000 : retryDelay * 2); } } diff --git a/scenario/LiquidationScenario.ts b/scenario/LiquidationScenario.ts index e13612569..c22c0e133 100644 --- a/scenario/LiquidationScenario.ts +++ b/scenario/LiquidationScenario.ts @@ -210,7 +210,10 @@ scenario( scenario( 'Comet#liquidation > user can end up with a minted supply', { - filter: async (ctx) => !matchesDeployment(ctx, [{ network: 'base', deployment: 'usds' }]), + filter: async (ctx) => !matchesDeployment(ctx, [ + { network: 'base', deployment: 'usds' }, + { network: 'ronin' }, + ]), tokenBalances: async (ctx) => ( { $comet: { diff --git a/scenario/SupplyScenario.ts b/scenario/SupplyScenario.ts index be3be8537..10a02a33f 100644 --- a/scenario/SupplyScenario.ts +++ b/scenario/SupplyScenario.ts @@ -406,12 +406,20 @@ scenario( scenario( 'Comet#supplyFrom > repay borrow', { - tokenBalances: { - albert: { $base: 1010 } - }, - cometBalances: { - betty: { $base: '<= -1000' } // in units of asset, not wei - }, + tokenBalances: async (ctx) => ( + { + albert: { + $base: getConfigForScenario(ctx).supplyBase + (0.01 * getConfigForScenario(ctx).supplyBase) + } + } + ), + cometBalances: async (ctx) => ( + { + betty: { + $base: `<= -${getConfigForScenario(ctx).supplyBase}` + } + } + ), }, async ({ comet, actors }, context) => { const { albert, betty } = actors; diff --git a/scenario/TransferScenario.ts b/scenario/TransferScenario.ts index e5c94310b..0039b9601 100644 --- a/scenario/TransferScenario.ts +++ b/scenario/TransferScenario.ts @@ -180,32 +180,35 @@ scenario( scenario( 'Comet#transferFrom > withdraw to repay', { - cometBalances: { - albert: { $base: 1000, $asset0: 50 }, // in units of asset, not wei - betty: { $base: -1000 }, - charles: { $base: 1000 }, // to give the protocol enough base for others to borrow from - }, + cometBalances: async (ctx) => ( + { + albert: { $base: getConfigForScenario(ctx).transferBase, $asset0: getConfigForScenario(ctx).transferAsset2 }, // in units of asset, not wei + betty: { $base: -getConfigForScenario(ctx).transferBase }, + charles: { $base: getConfigForScenario(ctx).transferBase }, // to give the protocol enough base for others to borrow from + } + ), }, async ({ comet, actors }, context) => { const { albert, betty } = actors; const baseAssetAddress = await comet.baseToken(); const baseAsset = context.getAssetByAddress(baseAssetAddress); const scale = (await comet.baseScale()).toBigInt(); + const amountTransferred = BigInt(getConfigForScenario(context).transferBase) * scale; const utilization = await comet.getUtilization(); const borrowRate = (await comet.getBorrowRate(utilization)).toBigInt(); // XXX 70 seconds?! - expectApproximately(await albert.getCometBaseBalance(), 1000n * scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); - expectApproximately(await betty.getCometBaseBalance(), -1000n * scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await albert.getCometBaseBalance(), amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await betty.getCometBaseBalance(), -amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); await albert.allow(betty, true); // Betty withdraws from Albert to repay her own borrows - const toTransfer = 999n * scale; // XXX cannot withdraw 1000 (to ~0) + const toTransfer = amountTransferred - scale; // XXX cannot withdraw 1000 (to ~0) const txn = await betty.transferAssetFrom({ src: albert.address, dst: betty.address, asset: baseAsset.address, amount: toTransfer }); - expectApproximately(await albert.getCometBaseBalance(), scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); - expectApproximately(await betty.getCometBaseBalance(), -scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await albert.getCometBaseBalance(), scale, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await betty.getCometBaseBalance(), -scale, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); return txn; // return txn to measure gas } @@ -214,26 +217,29 @@ scenario( scenario( 'Comet#transfer base reverts if undercollateralized', { - cometBalances: { - albert: { $base: 1000, $asset0: 0.000001 }, // in units of asset, not wei - betty: { $base: -1000 }, - charles: { $base: 1000 }, // to give the protocol enough base for others to borrow from - }, + cometBalances: async (ctx) => ( + { + albert: { $base: getConfigForScenario(ctx).transferBase, $asset0: 0.000001 }, // in units of asset, not wei + betty: { $base: -getConfigForScenario(ctx).transferBase }, + charles: { $base: getConfigForScenario(ctx).transferBase }, // to give the protocol enough base for others to borrow from + } + ), }, async ({ comet, actors }, context) => { const { albert, betty } = actors; const baseAssetAddress = await comet.baseToken(); const baseAsset = context.getAssetByAddress(baseAssetAddress); const scale = (await comet.baseScale()).toBigInt(); + const amountTransferred = BigInt(getConfigForScenario(context).transferBase) * scale; const utilization = await comet.getUtilization(); const borrowRate = (await comet.getBorrowRate(utilization)).toBigInt(); // XXX 100 seconds?! - expectApproximately(await albert.getCometBaseBalance(), 1000n * scale, getInterest(1000n * scale, borrowRate, 100n) + 2n); - expectApproximately(await betty.getCometBaseBalance(), -1000n * scale, getInterest(1000n * scale, borrowRate, 100n) + 2n); + expectApproximately(await albert.getCometBaseBalance(), amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await betty.getCometBaseBalance(), -amountTransferred, getInterest(amountTransferred, borrowRate, 100n) + 2n); // Albert with positive balance transfers to Betty with negative balance - const toTransfer = 2001n * scale; // XXX min borrow... + const toTransfer = 2n*amountTransferred + scale; // XXX min borrow... await expectRevertCustom( albert.transferAsset({ dst: betty.address, @@ -248,28 +254,31 @@ scenario( scenario( 'Comet#transferFrom base reverts if undercollateralized', { - cometBalances: { - albert: { $base: 1000, $asset0: 0.000001 }, // in units of asset, not wei - betty: { $base: -1000 }, - charles: { $base: 1000 }, // to give the protocol enough base for others to borrow from - }, + cometBalances: async (ctx) => ( + { + albert: { $base: getConfigForScenario(ctx).transferBase, $asset0: 0.000001 }, // in units of asset, not wei + betty: { $base: -getConfigForScenario(ctx).transferBase }, + charles: { $base: getConfigForScenario(ctx).transferBase }, // to give the protocol enough base for others to borrow from + } + ), }, async ({ comet, actors }, context) => { const { albert, betty } = actors; const baseAssetAddress = await comet.baseToken(); const baseAsset = context.getAssetByAddress(baseAssetAddress); const scale = (await comet.baseScale()).toBigInt(); + const amountTransferred = BigInt(getConfigForScenario(context).transferBase) * scale; const utilization = await comet.getUtilization(); const borrowRate = (await comet.getBorrowRate(utilization)).toBigInt(); // XXX 70 seconds?! - expectApproximately(await albert.getCometBaseBalance(), 1000n * scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); - expectApproximately(await betty.getCometBaseBalance(), -1000n * scale, getInterest(1000n * scale, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await albert.getCometBaseBalance(), amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); + expectApproximately(await betty.getCometBaseBalance(), -amountTransferred, getInterest(amountTransferred, borrowRate, BigInt(getConfigForScenario(context).interestSeconds)) + 2n); await albert.allow(betty, true); // Albert with positive balance transfers to Betty with negative balance - const toTransfer = 2001n * scale; // XXX min borrow... + const toTransfer = 2n*amountTransferred + scale; // XXX min borrow... await expectRevertCustom( betty.transferAssetFrom({ src: albert.address, diff --git a/scenario/constraints/ProposalConstraint.ts b/scenario/constraints/ProposalConstraint.ts index df76002d9..36956771c 100644 --- a/scenario/constraints/ProposalConstraint.ts +++ b/scenario/constraints/ProposalConstraint.ts @@ -78,9 +78,9 @@ export class ProposalConstraint implements StaticConstra ); } - // temporary hack to skip proposal 510 - if (proposal.id.eq(510)) { - console.log('Skipping proposal 510'); + // temporary hack to skip proposal 519 + if (proposal.id.eq(519)) { + console.log('Skipping proposal 519'); continue; } diff --git a/scenario/utils/index.ts b/scenario/utils/index.ts index 410d7c318..ed9cfd7c9 100644 --- a/scenario/utils/index.ts +++ b/scenario/utils/index.ts @@ -488,172 +488,213 @@ async function redeployRenzoOracle(dm: DeploymentManager) { } } -const tokens = new Map([ - ['WETH', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'], - ['LINK', '0x514910771AF9Ca656af840dff83E8264EcF986CA'], +const tokens = [ + ['mainnet', 'WETH', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'], + ['mainnet', 'LINK', '0x514910771AF9Ca656af840dff83E8264EcF986CA'], + ['mainnet', 'GHO', '0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f'], + ['ronin', 'WETH', '0xc99a6a985ed2cac1ef41640596c5a5f9f4e19ef5'], + ['ronin', 'WRON', '0xe514d9deb7966c8be0ca922de8a064264ea6bcd4'], + ['ronin', 'LINK', '0x3902228d6a3d2dc44731fd9d45fee6a61c722d0b'], +]; + +const dest = new Map([ + ['ronin', '6916147374840168594'], + ['mainnet', '5009297550715157269'], ]); -const dest = new Map([['ronin', '6916147374840168594']]); - -async function updateCCIPStats(dm: DeploymentManager) { - if (dm.network === 'mainnet') { - const commitStore = '0x2aa101bf99caef7fc1355d4c493a1fe187a007ce'; +export async function updateCCIPStats( + dm: DeploymentManager, + tenderlyLogs?: any[] +) { + const config = [ + { + network: 'mainnet', + commitStore: '0x2aa101bf99caef7fc1355d4c493a1fe187a007ce', + priceRegistry: '0x8c9b2Efb7c64C394119270bfecE7f54763b958Ad' + }, + { + network: 'ronin', + commitStore: '0x28c66d9693b2634b2f3b170f6d9584eec2f72ff0', + priceRegistry: '0xefCEa3CFA330adcDdeCe99219C57fd45cd166ac1' + } + ]; + const { commitStore, priceRegistry } = config.find(c => c.network === dm.network) || {}; + if (!commitStore || !priceRegistry) { + console.log(`No CCIP config for network ${dm.network}, skipping CCIP stats update.`); + return; + } + const abi = [ + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'sourceToken', + type: 'address', + }, + { + internalType: 'uint224', + name: 'usdPerToken', + type: 'uint224', + }, + ], + internalType: 'struct TokenPriceUpdate[]', + name: 'tokenPriceUpdates', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'uint64', + name: 'destChainSelector', + type: 'uint64', + }, + { + internalType: 'uint224', + name: 'usdPerUnitGas', + type: 'uint224', + }, + ], + internalType: 'struct GasPriceUpdate[]', + name: 'gasPriceUpdates', + type: 'tuple[]', + }, + ], + internalType: 'struct PriceUpdates', + name: 'priceUpdates', + type: 'tuple', + }, + ], + name: 'updatePrices', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint64', + name: 'destChainSelector', + type: 'uint64', + }, + ], + name: 'getDestinationChainGasPrice', + outputs: [ + { + components: [ + { + internalType: 'uint224', + name: 'value', + type: 'uint224', + }, + { + internalType: 'uint32', + name: 'timestamp', + type: 'uint32', + }, + ], + internalType: 'struct TimestampedPackedUint224', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + ], + name: 'getTokenPrice', + outputs: [ + { + components: [ + { + internalType: 'uint224', + name: 'value', + type: 'uint224', + }, + { + internalType: 'uint32', + name: 'timestamp', + type: 'uint32', + }, + ], + internalType: 'struct TimestampedPackedUint224', + name: '', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + ]; - const priceRegistry = '0x8c9b2Efb7c64C394119270bfecE7f54763b958Ad'; - const abi = [ - { - inputs: [ - { - components: [ - { - components: [ - { - internalType: 'address', - name: 'sourceToken', - type: 'address', - }, - { - internalType: 'uint224', - name: 'usdPerToken', - type: 'uint224', - }, - ], - internalType: 'struct TokenPriceUpdate[]', - name: 'tokenPriceUpdates', - type: 'tuple[]', - }, - { - components: [ - { - internalType: 'uint64', - name: 'destChainSelector', - type: 'uint64', - }, - { - internalType: 'uint224', - name: 'usdPerUnitGas', - type: 'uint224', - }, - ], - internalType: 'struct GasPriceUpdate[]', - name: 'gasPriceUpdates', - type: 'tuple[]', - }, - ], - internalType: 'struct PriceUpdates', - name: 'priceUpdates', - type: 'tuple', - }, - ], - name: 'updatePrices', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint64', - name: 'destChainSelector', - type: 'uint64', - }, - ], - name: 'getDestinationChainGasPrice', - outputs: [ - { - components: [ - { - internalType: 'uint224', - name: 'value', - type: 'uint224', - }, - { - internalType: 'uint32', - name: 'timestamp', - type: 'uint32', - }, - ], - internalType: 'struct TimestampedPackedUint224', - name: '', - type: 'tuple', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: 'token', - type: 'address', - }, - ], - name: 'getTokenPrice', - outputs: [ - { - components: [ - { - internalType: 'uint224', - name: 'value', - type: 'uint224', - }, - { - internalType: 'uint32', - name: 'timestamp', - type: 'uint32', - }, - ], - internalType: 'struct TimestampedPackedUint224', - name: '', - type: 'tuple', - }, - ], - stateMutability: 'view', - type: 'function', - }, - ]; + await dm.hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [commitStore], + }); - await dm.hre.network.provider.request({ - method: 'hardhat_impersonateAccount', - params: [commitStore], - }); + await dm.hre.network.provider.request({ + method: 'hardhat_setBalance', + params: [commitStore, '0x56bc75e2d63100000'], + }); + const commitStoreSigner = await dm.hre.ethers.getSigner(commitStore); - await dm.hre.network.provider.request({ - method: 'hardhat_setBalance', - params: [commitStore, '0x56bc75e2d63100000'], - }); - const commitStoreSigner = await dm.hre.ethers.getSigner(commitStore); + const registryContract = new Contract( + priceRegistry, + abi, + dm.hre.ethers.provider + ); - const registryContract = new Contract( - priceRegistry, - abi, - dm.hre.ethers.provider - ); + const tokenPrices = []; + const gasPrices = []; + for (const [network,, address] of tokens) { + if(network !== dm.network) continue; + const price = await registryContract.getTokenPrice(address); + tokenPrices.push([address, price.value]); + } - const tokenPrices = []; - const gasPrices = []; - for (const [, address] of tokens) { - const price = await registryContract.getTokenPrice(address); - tokenPrices.push([address, price.value]); - } - for (const [, address] of dest) { - const price = await registryContract.getDestinationChainGasPrice(address); - gasPrices.push([address, price.value]); + for (const [, chainSelector] of dest) { + try { + const price = await registryContract.getDestinationChainGasPrice(chainSelector); + gasPrices.push([chainSelector, price.value]); + } catch (e) { + continue; } + } - const tx0 = await commitStoreSigner.sendTransaction({ - to: priceRegistry, - data: registryContract.interface.encodeFunctionData('updatePrices', [ + if(tenderlyLogs) { + dm.stashRelayMessage( + priceRegistry, + registryContract.interface.encodeFunctionData('updatePrices', [ { tokenPriceUpdates: tokenPrices, gasPriceUpdates: gasPrices, }, ]), - }); - - await tx0.wait(); + commitStore + ); } + + const tx0 = await commitStoreSigner.sendTransaction({ + to: priceRegistry, + data: registryContract.interface.encodeFunctionData('updatePrices', [ + { + tokenPriceUpdates: tokenPrices, + gasPriceUpdates: gasPrices, + }, + ]), + }); + + await tx0.wait(); } const REDSTONE_FEEDS = { @@ -888,7 +929,7 @@ export async function tenderlyExecute( 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(); @@ -899,21 +940,21 @@ export async function tenderlyExecute( const B0L2 = Number(latestL2.number) + 1; const simsL2 = relayMessages.map((msg, i, arr) => { const isLast = i === arr.length - 1; - + const timestamp = isLast ? Number(T0L2) : latestL2.timestamp; - + const block = isLast ? B0L2 : latestL2.number; - + return { network_id: chainId2.toString(), from: msg.signer, - to: msg.messanger, + to: msg.messenger, block_number: Number(block), block_header: { - timestamp: gdm.hre.ethers.utils.hexlify(Number(timestamp)) + timestamp: bdm.hre.ethers.utils.hexlify(Number(timestamp)) }, input: msg.callData, save: true, @@ -921,14 +962,6 @@ export async function tenderlyExecute( gas_price: 0, }; }); - - - while (!simsL1[0]) { - simsL1.shift(); - if (simsL1.length == 0) { - break; - } - } if (simsL2.length > 0) { const bundle2 = await simulateBundle(bdm, simsL2, Number(B0L2)); @@ -948,25 +981,65 @@ async function simulateBundle( simulations: any[], blockNumber: number = 0 ): Promise { - const { username, project, accessKey } = (dm.hre.config as any).tenderly; - const body = { - simulations, - block_number: blockNumber, - simulation_type: 'full', - save: true, - }; + const rollingStateChanges = {}; + const results = []; + + for (const sim of simulations) { + const { username, project, accessKey } = (dm.hre.config as any).tenderly; + + // Merge rolling state changes with simulation's own state_objects + const stateObjects = sim.state_objects + ? { ...rollingStateChanges, ...sim.state_objects } + : rollingStateChanges; + + const body = { + simulations: [{ + ...sim, + state_objects: stateObjects, + block_number: sim.block_number || blockNumber, + simulation_type: 'full', + save: true, + save_if_fails: true, + }] + }; - const result = await axios.post( - `https://api.tenderly.co/api/v1/account/${username}/project/${project}/simulate-bundle`, - body, - { - headers: { - 'X-Access-Key': accessKey, - 'Content-Type': 'application/json', - }, + const result = await axios.post( + `https://api.tenderly.co/api/v1/account/${username}/project/${project}/simulate-bundle`, + body, + { + headers: { + 'X-Access-Key': accessKey, + 'Content-Type': 'application/json', + }, + } + ); + + // Extract and accumulate state changes from state_diff + const simResult = result.data.simulation_results[0]; + if (simResult?.transaction?.transaction_info?.call_trace?.state_diff) { + const stateDiff = simResult.transaction.transaction_info.call_trace.state_diff; + + // state_diff is an array of objects with { address, raw: [...] } + for (const stateDiffEntry of stateDiff) { + const address = stateDiffEntry.address; + + if (!rollingStateChanges[address]) { + rollingStateChanges[address] = { storage: {} }; + } + + if (stateDiffEntry.raw && Array.isArray(stateDiffEntry.raw)) { + for (const change of stateDiffEntry.raw) { + // Each change has: { address, key, original, dirty } + rollingStateChanges[address].storage[change.key] = change.dirty; + } + } + } } - ); - return result.data.simulation_results; + + results.push(simResult); + } + + return results; } async function shareSimulation(dm: DeploymentManager, simulationId: string) { diff --git a/scenario/utils/relayMessage.ts b/scenario/utils/relayMessage.ts index 0bccd6467..fa0523665 100644 --- a/scenario/utils/relayMessage.ts +++ b/scenario/utils/relayMessage.ts @@ -16,7 +16,7 @@ export default async function relayMessage( tenderlyLogs?: any[] ) { const bridgeNetwork = bridgeDeploymentManager.network; - console.log(`Relaying messages from ${bridgeNetwork} -> ${governanceDeploymentManager.network}`); + console.log(`Relaying messages from ${governanceDeploymentManager.network} -> ${bridgeNetwork}`); let proposal; switch (bridgeNetwork) { case 'base': diff --git a/scenario/utils/relayRoninMessage.ts b/scenario/utils/relayRoninMessage.ts index 4a72cee87..e86c760b9 100644 --- a/scenario/utils/relayRoninMessage.ts +++ b/scenario/utils/relayRoninMessage.ts @@ -4,9 +4,13 @@ import { setNextBaseFeeToZero, setNextBlockTimestamp } from './hreUtils'; import { BigNumber, ethers } from 'ethers'; import { Log } from '@ethersproject/abstract-provider'; import { OpenBridgedProposal } from '../context/Gov'; -import { isTenderlyLog } from './index'; +import { isTenderlyLog, updateCCIPStats } from './index'; const roninChainSelector = '6916147374840168594'; +const mainnetChainSelector = '5009297550715157269'; + +const MAINNET_CCIP_ROUTER = '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D'; +const MAINNET_RONIN_OFF_RAMP = '0x9a3Ed7007809CfD666999e439076B4Ce4120528D'; export default async function relayRoninMessage( governanceDeploymentManager: DeploymentManager, @@ -20,6 +24,7 @@ export default async function relayRoninMessage( const l2CCIPOffRamp = (await bridgeDeploymentManager.getContractOrThrow('l2CCIPOffRamp')); const bridgeReceiver = (await bridgeDeploymentManager.getContractOrThrow('bridgeReceiver')); const l1TokenAdminRegistry = await governanceDeploymentManager.getContractOrThrow('l1TokenAdminRegistry'); + const timelockMainnet = await governanceDeploymentManager.getContractOrThrow('timelock'); const l2TokenAdminRegistry = await bridgeDeploymentManager.existing( 'l2TokenAdminRegistry', @@ -27,7 +32,12 @@ export default async function relayRoninMessage( 'ronin' ); + const l2CCIPOnRamp = await bridgeDeploymentManager.getContractOrThrow('l2CCIPOnRamp'); + const l1CCIPRouter = await governanceDeploymentManager.existing('l1CCIPRouter', MAINNET_CCIP_ROUTER, 'mainnet'); + const l1CCIPOffRamp = await governanceDeploymentManager.existing('roninl1CCIPOffRamp', MAINNET_RONIN_OFF_RAMP, 'mainnet'); + const offRampSigner = await impersonateAddress(bridgeDeploymentManager, l2CCIPOffRamp.address); + const l1OffRampSigner = await impersonateAddress(governanceDeploymentManager, l1CCIPOffRamp.address); const openBridgedProposals: OpenBridgedProposal[] = []; @@ -80,7 +90,7 @@ export default async function relayRoninMessage( await bridgeDeploymentManager.hre.network.provider.request({ method: 'hardhat_setBalance', - params: [l2CCIPOffRamp.address, '0x1000000000000000000000'] + params: [offRampSigner.address, '0x1000000000000000000000'] }); await setNextBaseFeeToZero(bridgeDeploymentManager); @@ -99,7 +109,7 @@ export default async function relayRoninMessage( const callData = l2Router.interface.encodeFunctionData('routeMessage', [ any2EVMMessage, 25_000, - 2_000_000, + 10_000_000, internalMsg.receiver, ]); bridgeDeploymentManager.stashRelayMessage( @@ -136,7 +146,7 @@ export default async function relayRoninMessage( const routeTx = await l2Router.connect(offRampSigner).routeMessage( any2EVMMessage, 25_000, - 2_000_000, + 10_000_000, internalMsg.receiver, ); @@ -207,23 +217,6 @@ export default async function relayRoninMessage( } } - if (tenderlyLogs) { - const proposalFilter = bridgeReceiver.filters.ProposalCreated(); - const proposalEvents = await bridgeDeploymentManager.hre.ethers.provider.getLogs({ - fromBlock: 'latest', - toBlock: 'latest', - address: bridgeReceiver.address, - topics: proposalFilter.topics - }); - - for (let event of proposalEvents) { - const { - args: { id, eta }, - } = bridgeReceiver.interface.parseLog(event); - openBridgedProposals.push({ id, eta }); - } - } - for (const proposal of openBridgedProposals) { const { id, eta } = proposal; await setNextBlockTimestamp(bridgeDeploymentManager, eta.toNumber() + 1); @@ -231,6 +224,7 @@ export default async function relayRoninMessage( if (tenderlyLogs) { const callData = bridgeReceiver.interface.encodeFunctionData('executeProposal', [id]); + await updateCCIPStats(bridgeDeploymentManager, tenderlyLogs); const signer = await bridgeDeploymentManager.getSigner(); bridgeDeploymentManager.stashRelayMessage( bridgeReceiver.address, @@ -238,9 +232,100 @@ export default async function relayRoninMessage( await signer.getAddress() ); } else { - await bridgeReceiver.executeProposal(id, { gasPrice: 0 }); + await updateCCIPStats(bridgeDeploymentManager); + const signer = await bridgeDeploymentManager.getSigner(); + await bridgeReceiver.connect(signer).executeProposal(id, { gasPrice: 0 }); + console.log(`[CCIP L2] Executed bridged proposal ${id.toString()}`); + } + } + if (tenderlyLogs) return openBridgedProposals; + + // Process L2→L1 (Ronin→Mainnet) messages + const filterCCIPL2ToL1 = l2CCIPOnRamp.filters.CCIPSendRequested(); + let logsCCIPL2ToL1: Log[] = []; + + const latestBlock = (await bridgeDeploymentManager.hre.ethers.provider.getBlock('latest')).number; + logsCCIPL2ToL1 = await bridgeDeploymentManager.hre.ethers.provider.getLogs({ + fromBlock: latestBlock - 500, + toBlock: 'latest', + address: l2CCIPOnRamp.address, + topics: filterCCIPL2ToL1.topics || [] + }); + + const targetReceivers = [ + timelockMainnet.address.toLowerCase() + ]; + + for (const log of logsCCIPL2ToL1) { + const parsedLog = l2CCIPOnRamp.interface.parseLog(log); + + const internalMsg = parsedLog.args.message; + if (!targetReceivers.includes(internalMsg.receiver.toLowerCase())) continue; + console.log(`[CCIP L2->L1] Found CCIPSendRequested with messageId=${internalMsg.messageId}, receiver=${internalMsg.receiver}`); + + await governanceDeploymentManager.hre.network.provider.request({ + method: 'hardhat_setBalance', + params: [l1CCIPOffRamp.address, '0x1000000000000000000000'] + }); + + await setNextBaseFeeToZero(governanceDeploymentManager); + + const any2EVMMessage = { + messageId: internalMsg.messageId, + sourceChainSelector: internalMsg.sourceChainSelector, + sender: ethers.utils.defaultAbiCoder.encode(['address'], [internalMsg.sender]), + data: internalMsg.data, + destTokenAmounts: internalMsg.tokenAmounts.map((t: any) => ({ + token: t.token as string, + amount: BigNumber.from(t.amount) + })), + }; + + const routeTx = await l1CCIPRouter.connect(l1OffRampSigner).routeMessage( + any2EVMMessage, + 25_000, + 2_000_000, + internalMsg.receiver, + ); + + await routeTx.wait(); + + if (internalMsg.tokenAmounts.length) { + for (const tokenTransferData of internalMsg.tokenAmounts) { + const l2TokenPoolAddress = await l2TokenAdminRegistry.getPool(tokenTransferData.token); + const l2TokenPool = new ethers.Contract( + l2TokenPoolAddress, + ['function getRemoteToken(uint64) external view returns (bytes)'], + bridgeDeploymentManager.hre.ethers.provider + ); + const l1Token64 = await l2TokenPool.getRemoteToken(mainnetChainSelector); + const l1TokenAddress = ethers.utils.defaultAbiCoder.decode(['address'], l1Token64)[0]; + const l1TokenPool = await l1TokenAdminRegistry.getPool(l1TokenAddress); + const l1Token = new ethers.Contract( + l1TokenAddress, + [ + 'function balanceOf(address) external view returns (uint256)', + 'function transfer(address, uint256) external returns (bool)' + ], + governanceDeploymentManager.hre.ethers.provider + ); + + const poolSigner = await impersonateAddress(governanceDeploymentManager, l1TokenPool); + await governanceDeploymentManager.hre.network.provider.request({ + method: 'hardhat_setBalance', + params: [l1TokenPool, '0x1000000000000000000000'] + }); + + const poolBalance = await l1Token.balanceOf(l1TokenPool); + console.log(`[CCIP L2->L1] Token pool ${l1TokenPool} balance: ${poolBalance.toString()}, transferring ${tokenTransferData.amount.toString()} to ${internalMsg.receiver}`); + + const transferTx = await l1Token.connect(poolSigner).transfer(internalMsg.receiver, tokenTransferData.amount); + await transferTx.wait(); + console.log(`[CCIP L2->L1] Transferred ${tokenTransferData.amount.toString()} of ${l1TokenAddress} to ${internalMsg.receiver}`); + } } - console.log(`[CCIP L2] Executed bridged proposal ${id.toString()}`); + + console.log(`[CCIP L2->L1] Routed message to ${internalMsg.receiver}`); } return openBridgedProposals; diff --git a/scenario/utils/scenarioHelper.ts b/scenario/utils/scenarioHelper.ts index a104a629c..181a6e168 100644 --- a/scenario/utils/scenarioHelper.ts +++ b/scenario/utils/scenarioHelper.ts @@ -22,6 +22,7 @@ const config = { transferBase: 1000, transferAsset: 5000, transferAsset1: 5000, + transferAsset2: 50, interestSeconds: 110, withdrawBase: 1000, withdrawAsset: 3000, @@ -29,7 +30,8 @@ const config = { withdrawAsset1: 3000, withdrawCollateral: 100, transferCollateral: 100, - supplyCollateral: 100 + supplyCollateral: 100, + supplyBase: 1000, }; export function getConfigForScenario(ctx: CometContext, i?: number) { @@ -70,6 +72,14 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { config.liquidationAsset = 100; } + if (ctx.world.base.network === 'mainnet' && ctx.world.base.deployment === 'usdt') { + if(i == 12) { + config.supplyCollateral = 0; + config.transferCollateral = 0; + config.withdrawCollateral = 0; + } + } + if (ctx.world.base.network === 'base' && ctx.world.base.deployment === 'aero') { config.interestSeconds = 110; } @@ -111,10 +121,11 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'arbitrum' && ctx.world.base.deployment === 'usdc') { - config.bulkerAsset = 10000; - config.bulkerAsset1 = 10000; - config.withdrawAsset = 7000; + config.bulkerAsset = 100000; + config.bulkerAsset1 = 100000; + config.withdrawAsset = 10000; config.transferAsset = 500000; + config.transferAsset1 = 500000; config.transferBase = 100; if(i == 8) { // tBTC config.supplyCollateral = 2; @@ -124,11 +135,12 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'arbitrum' && ctx.world.base.deployment === 'usdt') { - config.withdrawAsset = 7000; - config.bulkerAsset = 10000; + config.rewardsAsset = 20000; + config.withdrawAsset = 20000; + config.bulkerAsset = 100000; config.bulkerAsset1 = 10000; - config.transferAsset = 10000; - config.transferAsset1 = 10000; + config.transferAsset = 100000; + config.transferAsset1 = 100000; if(i == 5) { // tBTC config.supplyCollateral = 2; config.transferCollateral = 2; @@ -137,11 +149,11 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'arbitrum' && ctx.world.base.deployment === 'usdc.e') { - config.withdrawAsset = 7000; - config.bulkerAsset = 10000; - config.bulkerAsset1 = 10000; - config.transferAsset = 10000; - config.transferAsset1 = 10000; + config.withdrawAsset = 10000; + config.bulkerAsset = 100000; + config.bulkerAsset1 = 100000; + config.transferAsset = 500000; + config.transferAsset1 = 500000; config.liquidationDenominator = 84; config.liquidationBase = 100000; config.liquidationBase1 = 50000; @@ -153,18 +165,19 @@ export function getConfigForScenario(ctx: CometContext, i?: number) { } if (ctx.world.base.network === 'ronin' && ctx.world.base.deployment === 'weth') { + config.supplyBase = 100; config.transferBase = 10; - config.transferAsset = 200000; - config.transferAsset1 = 200000; + config.transferAsset = 4000000; + config.transferAsset1 = 800000; config.rewardsAsset = 1000000; config.rewardsBase = 200; config.withdrawBase = 10; - config.withdrawBase1 = 10; - config.withdrawAsset = 100000; - config.withdrawAsset1 = 10000; - config.liquidationBase = 150; + config.withdrawBase1 = 50; + config.withdrawAsset = 4000000; + config.withdrawAsset1 = 4000000; + config.liquidationBase = 200; config.liquidationBase1 = 50; - config.liquidationAsset = 5; + config.liquidationAsset = 6; config.bulkerAsset = 100000; config.bulkerAsset1 = 100000; config.bulkerComet = 100; diff --git a/src/deploy/index.ts b/src/deploy/index.ts index 3757ac900..4e96469ca 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -79,12 +79,11 @@ export type TestnetProposal = [ // Ideally these wouldn't be hardcoded, but other solutions are much more complex, and slower export const COMP_WHALES = { mainnet: [ - '0x9aa835bc7b8ce13b9b0c9764a52fbf71ac62ccf1', - '0x683a4f9915d6216f73d6df50151725036bd26c02', + '0x66cD62c6F8A4BB0Cd8720488BCBd1A6221B765F9', + '0xb06df4dd01a5c5782f360ada9345c87e86adae3d', + '0x3FB19771947072629C8EEE7995a2eF23B72d4C8A', '0x8169522c2C57883E8EF80C498aAB7820dA539806', - '0x8d07D225a769b7Af3A923481E1FdF49180e6A265', - '0x7d1a02C0ebcF06E1A36231A54951E061673ab27f', - '0x54A37d93E57c5DA659F508069Cf65A381b61E189', + '0x36cc7B13029B5DEe4034745FB4F24034f3F2ffc6', ], testnet: ['0xbbfe34e868343e6f4f5e8b5308de980d7bd88c46'] @@ -113,6 +112,7 @@ export const WHALES = { '0x34C0bD5877A5Ee7099D0f5688D65F4bB9158BDE2', // sFRAX whale '0x9152e9C04e8fE8373EDaa8f5841E25d4015658B7', // pumpBTC whale '0x65906988ADEe75306021C417a1A3458040239602', // LBTC whale + '0x7667095Caa12b79fCa489ff6E2198Ca01fDAe057', ], polygon: [ '0xF977814e90dA44bFA03b6295A0616a897441aceC', // USDT whale @@ -149,6 +149,7 @@ export const WHALES = { '0x54b5569deC8A6A8AE61A36Fd34e5c8945810db8b', // tBTC whale '0xDBD974Eb5360d053ea0c56B4DaCF4A9D3E894Ee2', // tETH whale '0xbA1333333333a1BA1108E8412f11850A5C319bA9', // tETH whale + '0xEA1132120ddcDDA2F119e99Fa7A27a0d036F7Ac9', // ezETH whale ], base: [ '0x6D3c5a4a7aC4B1428368310E4EC3bB1350d01455', // USDbC whale diff --git a/test/allow-by-sig-test.ts b/test/allow-by-sig-test.ts index 82195d7e6..2d422d449 100644 --- a/test/allow-by-sig-test.ts +++ b/test/allow-by-sig-test.ts @@ -29,7 +29,7 @@ const types = { describe('allowBySig', function () { beforeEach(async () => { - comet = (await makeProtocol()).comet; + comet = (await makeProtocol()).cometWithExtendedAssetList; [_admin, pauseGuardian, signer, manager] = await ethers.getSigners(); domain = { diff --git a/test/helpers.ts b/test/helpers.ts index c343729b7..53fcda4a4 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -156,7 +156,12 @@ export function dfn(x: T | undefined | null, dflt: T): T { } export function exp(i: number, d: Numeric = 0, r: Numeric = 6): bigint { - return (BigInt(Math.floor(i * 10 ** Number(r))) * 10n ** BigInt(d)) / 10n ** BigInt(r); + const sign = i < 0 ? -1n : 1n; + const parts = Math.abs(i).toString().split('.'); + const intPart = parts[0]; + const fracPart = (parts[1] || '').padEnd(Number(r), '0').slice(0, Number(r)); + const scaled = BigInt(intPart + fracPart); + return sign * (scaled * 10n ** BigInt(d)) / 10n ** BigInt(r); } export function factor(f: number): bigint {