From 96cddfdbc618994134d55433d82fcefec4536774 Mon Sep 17 00:00:00 2001 From: Jared Flatow Date: Tue, 27 Dec 2022 20:54:02 -0800 Subject: [PATCH 1/2] Refactor deploy to support second markets * Add a fromDep to deployment manager which can be used for sharing dependency contracts from another deployment * Always attempt to initialize storage if needed, and not necessarily as gov * Always deploy an initial implementation contract instead of using the factory marker * Only take gov admin actions if we own the cometAdmin * Expose the primitives necessary for a proposal to configurate later, if/when admin cannot * Add an env flag for migration constraint to skip non-migration scenarios * Defer reading current config in modern constraint (bugfix) --- deployments/fuji/usdc/configuration.json | 4 +- deployments/goerli/usdc/configuration.json | 6 +- deployments/kovan/usdc/configuration.json | 10 ++-- deployments/mainnet/usdc/configuration.json | 10 ++-- deployments/mainnet/weth/configuration.json | 1 + deployments/mainnet/weth/deploy.ts | 7 +++ deployments/mumbai/usdc/configuration.json | 8 +-- plugins/deployment_manager/Cache.ts | 6 ++ .../deployment_manager/DeploymentManager.ts | 27 ++++++++- scenario/constraints/MigrationConstraint.ts | 2 + scenario/constraints/ModernConstraint.ts | 26 +++------ src/deploy/Network.ts | 58 ++++++++++++------- src/deploy/NetworkConfiguration.ts | 17 +++++- src/deploy/index.ts | 1 + 14 files changed, 119 insertions(+), 64 deletions(-) diff --git a/deployments/fuji/usdc/configuration.json b/deployments/fuji/usdc/configuration.json index 29cd80c46..6bc29e95c 100644 --- a/deployments/fuji/usdc/configuration.json +++ b/deployments/fuji/usdc/configuration.json @@ -29,7 +29,7 @@ "borrowCF": 0.7, "liquidateCF": 0.75, "liquidationFactor": 0.93, - "supplyCap": "35000e8" + "supplyCap": "35000" }, "WAVAX": { "priceFeed": "0x5498BB86BC934c8D34FDA08E81D444153d0D06aD", @@ -37,7 +37,7 @@ "borrowCF": 0.82, "liquidateCF": 0.85, "liquidationFactor": 0.93, - "supplyCap": "1000000e18" + "supplyCap": "1000000" } } } diff --git a/deployments/goerli/usdc/configuration.json b/deployments/goerli/usdc/configuration.json index 6f632ad4c..41fed8c8f 100644 --- a/deployments/goerli/usdc/configuration.json +++ b/deployments/goerli/usdc/configuration.json @@ -30,7 +30,7 @@ "borrowCF": 0.65, "liquidateCF": 0.7, "liquidationFactor": 0.92, - "supplyCap": "500000e18" + "supplyCap": "500000" }, "WBTC": { "priceFeed": "0xA39434A63A52E749F02807ae27335515BA4b07F7", @@ -38,7 +38,7 @@ "borrowCF": 0.7, "liquidateCF": 0.75, "liquidationFactor": 0.93, - "supplyCap": "35000e8" + "supplyCap": "35000" }, "WETH": { "priceFeed": "0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e", @@ -46,7 +46,7 @@ "borrowCF": 0.82, "liquidateCF": 0.85, "liquidationFactor": 0.93, - "supplyCap": "1000000e18" + "supplyCap": "1000000" } } } \ No newline at end of file diff --git a/deployments/kovan/usdc/configuration.json b/deployments/kovan/usdc/configuration.json index d784e5bbf..9181b6570 100644 --- a/deployments/kovan/usdc/configuration.json +++ b/deployments/kovan/usdc/configuration.json @@ -30,7 +30,7 @@ "borrowCF": 0.65, "liquidateCF": 0.7, "liquidationFactor": 0.92, - "supplyCap": "500000e18" + "supplyCap": "500000" }, "WBTC": { "priceFeed": "0x6135b13325bfC4B00278B4abC5e20bbce2D6580e", @@ -38,7 +38,7 @@ "borrowCF": 0.7, "liquidateCF": 0.75, "liquidationFactor": 0.93, - "supplyCap": "35000e8" + "supplyCap": "35000" }, "WETH": { "priceFeed": "0x9326BFA02ADD2366b30bacB125260Af641031331", @@ -46,7 +46,7 @@ "borrowCF": 0.82, "liquidateCF": 0.85, "liquidationFactor": 0.93, - "supplyCap": "1000000e18" + "supplyCap": "1000000" }, "UNI": { "priceFeed": "0xDA5904BdBfB4EF12a3955aEcA103F51dc87c7C39", @@ -54,7 +54,7 @@ "borrowCF": 0.75, "liquidateCF": 0.8, "liquidationFactor": 0.92, - "supplyCap": "50000000e18" + "supplyCap": "50000000" }, "LINK": { "priceFeed": "0x396c5E36DD0a0F5a5D33dae44368D4193f69a1F0", @@ -62,7 +62,7 @@ "borrowCF": 0.75, "liquidateCF": 0.8, "liquidationFactor": 0.92, - "supplyCap": "50000000e18" + "supplyCap": "50000000" } } } diff --git a/deployments/mainnet/usdc/configuration.json b/deployments/mainnet/usdc/configuration.json index 433a0e052..28e585b2f 100644 --- a/deployments/mainnet/usdc/configuration.json +++ b/deployments/mainnet/usdc/configuration.json @@ -34,7 +34,7 @@ "borrowCF": 0.65, "liquidateCF": 0.70, "liquidationFactor": 0.93, - "supplyCap": "0e18" + "supplyCap": "0" }, "WBTC": { "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", @@ -43,7 +43,7 @@ "borrowCF": 0.70, "liquidateCF": 0.77, "liquidationFactor": 0.95, - "supplyCap": "0e8" + "supplyCap": "0" }, "WETH": { "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", @@ -52,7 +52,7 @@ "borrowCF": 0.825, "liquidateCF": 0.895, "liquidationFactor": 0.95, - "supplyCap": "0e18" + "supplyCap": "0" }, "UNI": { "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", @@ -61,7 +61,7 @@ "borrowCF": 0.75, "liquidateCF": 0.81, "liquidationFactor": 0.93, - "supplyCap": "0e18" + "supplyCap": "0" }, "LINK": { "address": "0x514910771af9ca656af840dff83e8264ecf986ca", @@ -70,7 +70,7 @@ "borrowCF": 0.79, "liquidateCF": 0.85, "liquidationFactor": 0.93, - "supplyCap": "0e18" + "supplyCap": "0" } } } diff --git a/deployments/mainnet/weth/configuration.json b/deployments/mainnet/weth/configuration.json index 571680564..7400afaf6 100644 --- a/deployments/mainnet/weth/configuration.json +++ b/deployments/mainnet/weth/configuration.json @@ -24,6 +24,7 @@ "baseBorrowSpeed": "0e15", "baseMinForRewards": "1000000e6" }, + "rewardTokenAddress": "0xc00e94cb662c3520282e6f5717214004a7f26888", "assets": { "cbETH": { "address": "0xBe9895146f7AF43049ca1c1AE358B0541Ea49704", diff --git a/deployments/mainnet/weth/deploy.ts b/deployments/mainnet/weth/deploy.ts index 7ca3d599f..e0bbd2f34 100644 --- a/deployments/mainnet/weth/deploy.ts +++ b/deployments/mainnet/weth/deploy.ts @@ -36,6 +36,13 @@ export default async function deploy(deploymentManager: DeploymentManager, deplo ] ); + // Import shared contracts from cUSDCv3 + const cometAdmin = await deploymentManager.fromDep('cometAdmin', 'mainnet', 'usdc'); + const cometFactory = await deploymentManager.fromDep('cometFactory', 'mainnet', 'usdc'); + const $configuratorImpl = await deploymentManager.fromDep('configurator:implementation', 'mainnet', 'usdc'); + const configurator = await deploymentManager.fromDep('configurator', 'mainnet', 'usdc'); + const rewards = await deploymentManager.fromDep('rewards', 'mainnet', 'usdc'); + // Deploy all Comet-related contracts const deployed = await deployComet(deploymentManager, deploySpec); const { comet } = deployed; diff --git a/deployments/mumbai/usdc/configuration.json b/deployments/mumbai/usdc/configuration.json index b809399ba..198cf2a60 100644 --- a/deployments/mumbai/usdc/configuration.json +++ b/deployments/mumbai/usdc/configuration.json @@ -29,7 +29,7 @@ "borrowCF": 0.79, "liquidateCF": 0.85, "liquidationFactor": 0.93, - "supplyCap": "500000e18" + "supplyCap": "500000" }, "WETH": { "priceFeed": "0x0715A7794a1dc8e42615F059dD6e406A6594651A", @@ -37,7 +37,7 @@ "borrowCF": 0.82, "liquidateCF": 0.85, "liquidationFactor": 0.93, - "supplyCap": "1000000e18" + "supplyCap": "1000000" }, "WBTC": { "priceFeed": "0x007A22900a3B98143368Bd5906f8E17e9867581b", @@ -45,7 +45,7 @@ "borrowCF": 0.7, "liquidateCF": 0.75, "liquidationFactor": 0.93, - "supplyCap": "35000e8" + "supplyCap": "35000" }, "WMATIC": { "priceFeed": "0xd0D5e3DB44DE05E9F294BB0a3bEEaF030DE24Ada", @@ -53,7 +53,7 @@ "borrowCF": 0.82, "liquidateCF": 0.85, "liquidationFactor": 0.93, - "supplyCap": "1000000e18" + "supplyCap": "1000000" } } } \ No newline at end of file diff --git a/plugins/deployment_manager/Cache.ts b/plugins/deployment_manager/Cache.ts index 74cf77fbc..52a9b7723 100644 --- a/plugins/deployment_manager/Cache.ts +++ b/plugins/deployment_manager/Cache.ts @@ -46,6 +46,12 @@ export class Cache { this.writeCacheToDisk = writeCacheToDisk ?? false; } + asDeployment(network: string, deployment: string): Cache { + const cache = new Cache(network, deployment, this.writeCacheToDisk, this.deploymentDir); + cache.cache = this.cache; + return cache; + } + private getPath(spec: FileSpec): string[] { if (typeof spec === 'string') { return [spec.toLowerCase()]; diff --git a/plugins/deployment_manager/DeploymentManager.ts b/plugins/deployment_manager/DeploymentManager.ts index eb01deca5..68b1858a3 100644 --- a/plugins/deployment_manager/DeploymentManager.ts +++ b/plugins/deployment_manager/DeploymentManager.ts @@ -3,7 +3,7 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; import { Contract, providers } from 'ethers'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { Alias, Address, BuildFile, TraceFn } from './Types'; -import { putAlias, storeAliases } from './Aliases'; +import { getAliases, storeAliases, putAlias } from './Aliases'; import { Cache } from './Cache'; import { ContractMap } from './ContractMap'; import { DeployOpts, deploy, deployBuild } from './Deploy'; @@ -206,6 +206,25 @@ export class DeploymentManager { return maybeExisting; } + async fromDep( + alias: Alias, + network: string, + deployment: string, + otherAlias = alias + ): Promise { + const maybeExisting = await this.contract(alias); + if (!maybeExisting) { + const trace = this.tracer(); + const address = await this.readAlias(network, deployment, otherAlias); + const buildFile = await this.import(address, network); + const contract = getEthersContract(address, buildFile, this.hre); + await this.putAlias(alias, contract); + trace(`Loaded ${buildFile.contract} from ${network}/${deployment}:${otherAlias} (${address}) as '${alias}'`); + return contract; + } + return maybeExisting; + } + /* Deploys a contract from Hardhat artifacts */ async _deploy(contractFile: string, deployArgs: any[], retries?: number): Promise { const contract = await this.retry( @@ -294,6 +313,12 @@ export class DeploymentManager { this.contractsCache.set(alias, contract); } + /* Read an alias from another deployment */ + async readAlias(network: string, deployment: string, alias: Alias): Promise
{ + const aliases = await getAliases(this.cache.asDeployment(network, deployment)); + return aliases.get(alias); + } + /* Returns a memory-cached map of contracts indexed by alias. * Note: this map is cached in-memory and updated when aliases change, * so call this as often as you would like. diff --git a/scenario/constraints/MigrationConstraint.ts b/scenario/constraints/MigrationConstraint.ts index 65c778bc9..2696c3ae9 100644 --- a/scenario/constraints/MigrationConstraint.ts +++ b/scenario/constraints/MigrationConstraint.ts @@ -23,6 +23,8 @@ export class MigrationConstraint const solutions: Solution[] = []; for (const migrationList of subsets(await getMigrations(context, requirements))) { + if (migrationList.length == 0 && process.env['MIGRATIONS_ONLY']) + continue; solutions.push(async function (ctx: T): Promise { const proposer = await ctx.getProposer(); diff --git a/scenario/constraints/ModernConstraint.ts b/scenario/constraints/ModernConstraint.ts index 1522c4319..757299528 100644 --- a/scenario/constraints/ModernConstraint.ts +++ b/scenario/constraints/ModernConstraint.ts @@ -1,30 +1,18 @@ import { Constraint } from '../../plugins/scenario'; import { CometContext } from '../context/CometContext'; -import { ProtocolConfiguration } from '../../src/deploy'; import { getFuzzedRequirements } from './Fuzzing'; import { Requirements } from './Requirements'; -interface ModernConfig { - // Whether to upgrade or Comet config overrides to use for an upgrade - upgrade: ProtocolConfiguration; -} - -async function getModernConfigs(context: CometContext, requirements: Requirements): Promise { - const currentConfig = await context.getConfiguration(); - const fuzzedConfigs = getFuzzedRequirements(requirements).map((r) => ({ - upgrade: r.upgrade && Object.assign({}, currentConfig, r.upgrade), - })); - return fuzzedConfigs; -} - export class ModernConstraint implements Constraint { - async solve(requirements: R, context: T) { - const configs = await getModernConfigs(context, requirements); + async solve(requirements: R, _context: T) { + const fuzzed = await getFuzzedRequirements(requirements); const solutions = []; - for (const config of configs) { - if (config.upgrade) { + for (const req of fuzzed) { + if (req.upgrade) { solutions.push(async function solution(ctx: T): Promise { - return await ctx.upgrade(config.upgrade) as T; // It's been modified + const current = await ctx.getConfiguration(); + const upgrade = Object.assign({}, current, req.upgrade); + return await ctx.upgrade(upgrade) as T; // It's been modified }); } } diff --git a/src/deploy/Network.ts b/src/deploy/Network.ts index 0ec66089d..5ee66a52a 100644 --- a/src/deploy/Network.ts +++ b/src/deploy/Network.ts @@ -146,13 +146,6 @@ export async function deployNetworkComet( maybeForce(deploySpec.cometMain) ); - const cometProxy = await deploymentManager.deploy( - 'comet', - 'vendor/proxy/transparent/TransparentUpgradeableProxy.sol', - [cometFactory.address, cometAdmin.address, []], // NB: temporary implementation contract - maybeForce(), - ); - const configuration = { governor, pauseGuardian, @@ -177,6 +170,19 @@ export async function deployNetworkComet( assetConfigs, }; + const tmpCometImpl = await deploymentManager.deploy( + 'comet:implementation', + 'Comet.sol', + [configuration], + maybeForce(), + ); + const cometProxy = await deploymentManager.deploy( + 'comet', + 'vendor/proxy/transparent/TransparentUpgradeableProxy.sol', + [tmpCometImpl.address, cometAdmin.address, []], // NB: temporary implementation contract + maybeForce(), + ); + const configuratorImpl = await deploymentManager.deploy( 'configurator:implementation', 'Configurator.sol', @@ -211,13 +217,27 @@ export async function deployNetworkComet( // Also get a handle for Comet, although it may not *actually* support the interface yet const comet = await deploymentManager.cast(cometProxy.address, 'contracts/CometInterface.sol:CometInterface'); - // Get the currently impl addresses for the proxies, and determine if this is the first deploy + // Call initializeStorage if storage not initialized + // Note: we now rely on the fact that anyone may call, which helps separate the proposal + await deploymentManager.idempotent( + async () => (await comet.totalsBasic()).lastAccrualTime == 0, + async () => { + trace(`Initializing Comet at ${comet.address}`); + trace(await wait(comet.connect(admin).initializeStorage())); + } + ); + + // If we aren't admin, we'll need proposals to configure things + const amAdmin = sameAddress(await cometAdmin.owner(), admin.address); + + // Get the current impl addresses for the proxies, and determine if we've configurated const $configuratorImpl = await cometAdmin.getProxyImplementation(configurator.address); const $cometImpl = await cometAdmin.getProxyImplementation(comet.address); - const isFirstDeploy = sameAddress($cometImpl, cometFactory.address); + const isTmpImpl = sameAddress($cometImpl, tmpCometImpl.address); + // Note: these next setup steps may require a follow-up proposal to complete, if we cannot admin here await deploymentManager.idempotent( - async () => !sameAddress($configuratorImpl, configuratorImpl.address), + async () => amAdmin && !sameAddress($configuratorImpl, configuratorImpl.address), async () => { trace(`Setting Configurator implementation to ${configuratorImpl.address}`); trace(await wait(cometAdmin.connect(admin).upgrade(configurator.address, configuratorImpl.address))); @@ -225,7 +245,7 @@ export async function deployNetworkComet( ); await deploymentManager.idempotent( - async () => !sameAddress(await configurator.factory(comet.address), cometFactory.address), + async () => amAdmin && !sameAddress(await configurator.factory(comet.address), cometFactory.address), async () => { trace(`Setting factory in Configurator to ${cometFactory.address}`); trace(await wait(configurator.connect(admin).setFactory(comet.address, cometFactory.address))); @@ -233,26 +253,20 @@ export async function deployNetworkComet( ); await deploymentManager.idempotent( - async () => isFirstDeploy || deploySpec.all || deploySpec.cometMain || deploySpec.cometExt, + async () => amAdmin && (isTmpImpl || deploySpec.all || deploySpec.cometMain || deploySpec.cometExt), async () => { - trace(`Setting configuration in Configurator for ${comet.address}`); + trace(`Setting configuration in Configurator for ${comet.address} (${isTmpImpl})`); trace(await wait(configurator.connect(admin).setConfiguration(comet.address, configuration))); - if (isFirstDeploy) { - trace(`Deploying first implementation of Comet and initializing...`); - const data = (await comet.populateTransaction.initializeStorage()).data; - trace(await wait(cometAdmin.connect(admin).deployUpgradeToAndCall(configurator.address, comet.address, data))); - } else { - trace(`Upgrading implementation of Comet...`); - trace(await wait(cometAdmin.connect(admin).deployAndUpgradeTo(configurator.address, comet.address))); - } + trace(`Upgrading implementation of Comet...`); + trace(await wait(cometAdmin.connect(admin).deployAndUpgradeTo(configurator.address, comet.address))); trace(`New Comet implementation at ${await cometAdmin.getProxyImplementation(comet.address)}`); } ); await deploymentManager.idempotent( - async () => !sameAddress((await rewards.rewardConfig(comet.address)).token, rewardTokenAddress), + async () => amAdmin && !sameAddress((await rewards.rewardConfig(comet.address)).token, rewardTokenAddress), async () => { trace(`Setting reward token in CometRewards to ${rewardTokenAddress} for ${comet.address}`); trace(await wait(rewards.connect(admin).setRewardConfig(comet.address, rewardTokenAddress))); diff --git a/src/deploy/NetworkConfiguration.ts b/src/deploy/NetworkConfiguration.ts index af18c667d..32004e94f 100644 --- a/src/deploy/NetworkConfiguration.ts +++ b/src/deploy/NetworkConfiguration.ts @@ -1,4 +1,5 @@ import { AssetConfigStruct } from '../../build/types/Comet'; +import { ConfigurationStruct } from '../../build/types/Configurator'; import { ProtocolConfiguration } from './index'; import { ContractMap } from '../../plugins/deployment_manager/ContractMap'; import { DeploymentManager } from '../../plugins/deployment_manager/DeploymentManager'; @@ -100,7 +101,7 @@ function getAssetConfigs( borrowCollateralFactor: percentage(assetConfig.borrowCF), liquidateCollateralFactor: percentage(assetConfig.liquidateCF), liquidationFactor: percentage(assetConfig.liquidationFactor), - supplyCap: number(assetConfig.supplyCap), // TODO: Decimals + supplyCap: BigInt(number(assetConfig.supplyCap)) * (10n ** BigInt(assetConfig.decimals)), })); } @@ -123,7 +124,7 @@ function getOverridesOrConfig( trackingIndexScale: _ => number(tracking.indexScale), baseTrackingSupplySpeed: _ => number(tracking.baseSupplySpeed), baseTrackingBorrowSpeed: _ => number(tracking.baseBorrowSpeed), - baseMinForRewards: _ => number(tracking.baseMinForRewards), + baseMinForRewards: _ => number(tracking.baseMinForRewards), // TODO: in token units (?) }); const mapping = () => ({ name: _ => config.name, @@ -134,7 +135,7 @@ function getOverridesOrConfig( baseTokenPriceFeed: _ => getContractAddress(`${config.baseToken}:priceFeed`, contracts, config.baseTokenPriceFeed), baseBorrowMin: _ => number(config.borrowMin), // TODO: in token units (?) storeFrontPriceFactor: _ => percentage(config.storeFrontPriceFactor), - targetReserves: _ => number(config.targetReserves), + targetReserves: _ => number(config.targetReserves), // TODO: in token units (?) ...interestRateInfoMapping(config.rates), ...trackingInfoMapping(config.tracking), assetConfigs: _ => getAssetConfigs(config.assets, contracts), @@ -155,3 +156,13 @@ export async function getConfiguration( const contracts = await deploymentManager.contracts(); return getOverridesOrConfig(configOverrides, config, contracts); } + +export async function getConfigurationStruct( + deploymentManager: DeploymentManager, + configOverrides: ProtocolConfiguration = {}, +): Promise { + const contracts = await deploymentManager.contracts(); + const configuration = await getConfiguration(deploymentManager, configOverrides); + const extensionDelegate = configOverrides.extensionDelegate ?? getContractAddress('comet:implementation:implementation', contracts); + return { ...configuration, extensionDelegate }; +} diff --git a/src/deploy/index.ts b/src/deploy/index.ts index e65b90800..e128d28b1 100644 --- a/src/deploy/index.ts +++ b/src/deploy/index.ts @@ -2,6 +2,7 @@ import { AssetConfigStruct } from '../../build/types/Comet'; import { BigNumberish, Contract, PopulatedTransaction } from 'ethers'; export { cloneGov, deployNetworkComet as deployComet, sameAddress } from './Network'; +export { getConfiguration, getConfigurationStruct } from './NetworkConfiguration'; export { exp, getBlock, wait } from '../../test/helpers'; export { debug } from '../../plugins/deployment_manager/Utils'; From f766f9c452a5bc725b1a50c44b3ddca3406dfbe5 Mon Sep 17 00:00:00 2001 From: Jared Flatow Date: Thu, 29 Dec 2022 12:21:38 -0800 Subject: [PATCH 2/2] Add recipient to token sourcing blacklist --- plugins/scenario/utils/TokenSourcer.ts | 6 +++--- scenario/constraints/UtilizationConstraint.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/scenario/utils/TokenSourcer.ts b/plugins/scenario/utils/TokenSourcer.ts index 4cc415726..c0e3191f2 100644 --- a/plugins/scenario/utils/TokenSourcer.ts +++ b/plugins/scenario/utils/TokenSourcer.ts @@ -11,7 +11,7 @@ interface SourceTokenParameters { amount: number | bigint; asset: string; address: string; - blacklist: string[] | undefined; + blacklist: string[]; } export async function fetchQuery( @@ -62,7 +62,7 @@ export async function sourceTokens({ } else if (amount.isNegative()) { await removeTokens(dm, amount.abs(), asset, address); } else { - await addTokens(dm, amount, asset, address, blacklist); + await addTokens(dm, amount, asset, address, [address].concat(blacklist)); } } @@ -94,7 +94,7 @@ async function addTokens( amount: BigNumber, asset: string, address: string, - blacklist?: string[], + blacklist: string[], block?: number, offsetBlocks?: number, MAX_SEARCH_BLOCKS = 40000, diff --git a/scenario/constraints/UtilizationConstraint.ts b/scenario/constraints/UtilizationConstraint.ts index 6c52c97aa..322d6e5fd 100644 --- a/scenario/constraints/UtilizationConstraint.ts +++ b/scenario/constraints/UtilizationConstraint.ts @@ -126,7 +126,6 @@ export class UtilizationConstraint