diff --git a/publish/deployed/local-ovm/config.json b/publish/deployed/local-ovm/config.json index f830511cca..ed2171cff2 100644 --- a/publish/deployed/local-ovm/config.json +++ b/publish/deployed/local-ovm/config.json @@ -62,9 +62,6 @@ "Liquidations": { "deploy": true }, - "SupplySchedule": { - "deploy": true - }, "Synthetix": { "deploy": true }, @@ -139,8 +136,5 @@ }, "EtherWrapper": { "deploy": true - }, - "NativeEtherWrapper": { - "deploy": true } } diff --git a/publish/src/Deployer.js b/publish/src/Deployer.js index 86e3119edf..b14ec1948b 100644 --- a/publish/src/Deployer.js +++ b/publish/src/Deployer.js @@ -48,9 +48,9 @@ class Deployer { this.ignoreSafetyChecks = ignoreSafetyChecks; /* - provider is defined here to hold backwards compatible web3 component as well as ethers - while the migration is completed. After all web3 references are replaced by ethers, - web3 provider will be removed. The aim is to get rid of all references to web3 and web3_utils + provider is defined here to hold backwards compatible web3 component as well as ethers + while the migration is completed. After all web3 references are replaced by ethers, + web3 provider will be removed. The aim is to get rid of all references to web3 and web3_utils in the project. web3 and/or ethers is needed to interact with the contracts and sing transactions @@ -392,7 +392,7 @@ class Deployer { arg.toLowerCase() === forbiddenAddress.toLowerCase() ) { throw Error( - `new ${name}(): Cannot use the AddressResolver as a constructor arg. Use ReadProxyForResolver instead.` + `new ${name}(): Cannot use the AddressResolver as a constructor arg. Use ReadProxyAddressResolver instead.` ); } } diff --git a/publish/src/commands/deploy.js b/publish/src/commands/deploy.js deleted file mode 100644 index eeefd050ad..0000000000 --- a/publish/src/commands/deploy.js +++ /dev/null @@ -1,2706 +0,0 @@ -'use strict'; - -const path = require('path'); -const { gray, green, yellow, redBright, red } = require('chalk'); -const { - utils: { parseUnits, formatUnits, isAddress }, - constants, -} = require('ethers'); -const Deployer = require('../Deployer'); -const NonceManager = require('../NonceManager'); -const { loadCompiledFiles, getLatestSolTimestamp } = require('../solidity'); -const checkAggregatorPrices = require('../check-aggregator-prices'); -const pLimit = require('p-limit'); - -const { - ensureDeploymentPath, - ensureNetwork, - getDeploymentPathForNetwork, - loadAndCheckRequiredSources, - loadConnections, - confirmAction, - performTransactionalStep, - parameterNotice, - reportDeployedContracts, -} = require('../util'); - -const { - toBytes32, - fromBytes32, - constants: { - BUILD_FOLDER, - CONFIG_FILENAME, - CONTRACTS_FOLDER, - SYNTHS_FILENAME, - DEPLOYMENT_FILENAME, - ZERO_ADDRESS, - OVM_MAX_GAS_LIMIT, - inflationStartTimestampInSecs, - }, - defaults, - nonUpgradeable, -} = require('../../../.'); - -const DEFAULTS = { - gasPrice: '1', - methodCallGasLimit: 250e3, // 250k - contractDeploymentGasLimit: 6.9e6, // TODO split out into separate limits for different contracts, Proxys, Synths, Synthetix - debtSnapshotMaxDeviation: 0.01, // a 1 percent deviation will trigger a snapshot - network: 'kovan', - buildPath: path.join(__dirname, '..', '..', '..', BUILD_FOLDER), -}; - -const deploy = async ({ - addNewSynths, - gasPrice = DEFAULTS.gasPrice, - methodCallGasLimit = DEFAULTS.methodCallGasLimit, - contractDeploymentGasLimit = DEFAULTS.contractDeploymentGasLimit, - network = DEFAULTS.network, - buildPath = DEFAULTS.buildPath, - deploymentPath, - oracleExrates, - privateKey, - yes, - dryRun = false, - forceUpdateInverseSynthsOnTestnet = false, - useFork, - providerUrl, - useOvm, - freshDeploy, - manageNonces, - ignoreSafetyChecks, - ignoreCustomParameters, - concurrency, - specifyContracts, -} = {}) => { - ensureNetwork(network); - deploymentPath = deploymentPath || getDeploymentPathForNetwork({ network, useOvm }); - ensureDeploymentPath(deploymentPath); - - // OVM uses a gas price of 0 (unless --gas explicitely defined). - if (useOvm && gasPrice === DEFAULTS.gasPrice) { - gasPrice = constants.Zero; - } - - const limitPromise = pLimit(concurrency); - - const { - config, - params, - configFile, - synths, - deployment, - deploymentFile, - ownerActions, - ownerActionsFile, - feeds, - } = loadAndCheckRequiredSources({ - deploymentPath, - network, - }); - - // Mark contracts for deployment specified via an argument - if (specifyContracts) { - // Ignore config.json - Object.keys(config).map(name => { - config[name].deploy = false; - }); - // Add specified contracts - specifyContracts.split(',').map(name => { - if (!config[name]) { - config[name] = { - deploy: true, - }; - } else { - config[name].deploy = true; - } - }); - } - - if (freshDeploy) { - deployment.targets = {}; - deployment.sources = {}; - } - - if (!ignoreSafetyChecks) { - // Using Goerli without manageNonces? - if (network.toLowerCase() === 'goerli' && !useOvm && !manageNonces) { - throw new Error(`Deploying on Goerli needs to be performed with --manage-nonces.`); - } - - // Cannot re-deploy legacy contracts - if (!freshDeploy) { - // Get list of contracts to be deployed - const contractsToDeploy = []; - Object.keys(config).map(contractName => { - if (config[contractName].deploy) { - contractsToDeploy.push(contractName); - } - }); - - // Check that no non-deployable is marked for deployment. - // Note: if nonDeployable = 'TokenState', this will match 'TokenStatesUSD' - nonUpgradeable.map(nonUpgradeableContract => { - contractsToDeploy.map(contractName => { - if (contractName.match(new RegExp(`^${nonUpgradeableContract}`, 'g'))) { - throw new Error( - `You are attempting to deploy a contract marked as non-upgradeable: ${contractName}. This action could result in loss of state. Please verify and use --ignore-safety-checks if you really know what you're doing.` - ); - } - }); - }); - } - - // Every transaction in Optimism needs to be below 9m gas, to ensure - // there are no deployment out of gas errors during fraud proofs. - if (useOvm) { - const maxOptimismGasLimit = OVM_MAX_GAS_LIMIT; - if ( - contractDeploymentGasLimit > maxOptimismGasLimit || - methodCallGasLimit > maxOptimismGasLimit - ) { - throw new Error( - `Maximum transaction gas limit for OVM is ${maxOptimismGasLimit} gas, and specified contractDeploymentGasLimit and/or methodCallGasLimit are over such limit. Please make sure that these values are below the maximum gas limit to guarantee that fraud proofs can be done in L1.` - ); - } - } - - // Deploying on OVM and not using an OVM deployment path? - const lastPathItem = deploymentPath.split('/').pop(); - const isOvmPath = lastPathItem.includes('ovm'); - const deploymentPathMismatch = (useOvm && !isOvmPath) || (!useOvm && isOvmPath); - if (deploymentPathMismatch) { - if (useOvm) { - throw new Error( - `You are deploying to a non-ovm path ${deploymentPath}, while --use-ovm is true.` - ); - } else { - throw new Error( - `You are deploying to an ovm path ${deploymentPath}, while --use-ovm is false.` - ); - } - } - - // Fresh deploy and deployment.json not empty? - if (freshDeploy && Object.keys(deployment.targets).length > 0 && network !== 'local') { - throw new Error( - `Cannot make a fresh deploy on ${deploymentPath} because a deployment has already been made on this path. If you intend to deploy a new instance, use a different path or delete the deployment files for this one.` - ); - } - } - - const standaloneFeeds = Object.values(feeds).filter(({ standalone }) => standalone); - - const getDeployParameter = async name => { - const defaultParam = defaults[name]; - if (ignoreCustomParameters) { - return defaultParam; - } - - let effectiveValue = defaultParam; - - const param = (params || []).find(p => p.name === name); - - if (param) { - if (!yes) { - try { - await confirmAction( - yellow( - `⚠⚠⚠ WARNING: Found an entry for ${param.name} in params.json. Specified value is ${param.value} and default is ${defaultParam}.` + - '\nDo you want to use the specified value (default otherwise)? (y/n) ' - ) - ); - - effectiveValue = param.value; - } catch (err) { - console.error(err); - } - } else { - // yes = true - effectiveValue = param.value; - } - } - - if (effectiveValue !== defaultParam) { - console.log( - yellow( - `PARAMETER OVERRIDE: Overriding default ${name} with ${effectiveValue}, specified in params.json.` - ) - ); - } - - return effectiveValue; - }; - - console.log( - gray('Checking all contracts not flagged for deployment have addresses in this network...') - ); - const missingDeployments = Object.keys(config).filter(name => { - return !config[name].deploy && (!deployment.targets[name] || !deployment.targets[name].address); - }); - - if (missingDeployments.length) { - throw Error( - `Cannot use existing contracts for deployment as addresses not found for the following contracts on ${network}:\n` + - missingDeployments.join('\n') + - '\n' + - gray(`Used: ${deploymentFile} as source`) - ); - } - - console.log(gray('Loading the compiled contracts locally...')); - const { earliestCompiledTimestamp, compiled } = loadCompiledFiles({ buildPath }); - - // now get the latest time a Solidity file was edited - const latestSolTimestamp = getLatestSolTimestamp(CONTRACTS_FOLDER); - - const { - providerUrl: envProviderUrl, - privateKey: envPrivateKey, - etherscanLinkPrefix, - } = loadConnections({ - network, - useFork, - }); - - if (!providerUrl) { - if (!envProviderUrl) { - throw new Error('Missing .env key of PROVIDER_URL. Please add and retry.'); - } - - providerUrl = envProviderUrl; - } - - // if not specified, or in a local network, override the private key passed as a CLI option, with the one specified in .env - if (network !== 'local' && !privateKey) { - privateKey = envPrivateKey; - } - - const nonceManager = new NonceManager({}); - - const deployer = new Deployer({ - compiled, - contractDeploymentGasLimit, - config, - configFile, - deployment, - deploymentFile, - gasPrice, - methodCallGasLimit, - network, - privateKey, - providerUrl, - dryRun, - useOvm, - useFork, - ignoreSafetyChecks, - nonceManager: manageNonces ? nonceManager : undefined, - }); - - const { account } = deployer; - - nonceManager.web3 = deployer.provider.web3; - nonceManager.account = account; - - let currentSynthetixSupply; - let oldExrates; - let currentLastMintEvent; - let currentWeekOfInflation; - let systemSuspended = false; - let systemSuspendedReason; - - try { - const oldSynthetix = deployer.getExistingContract({ contract: 'Synthetix' }); - currentSynthetixSupply = await oldSynthetix.methods.totalSupply().call(); - - // inflationSupplyToDate = total supply - 100m - const inflationSupplyToDate = parseUnits(currentSynthetixSupply, 'wei').sub( - parseUnits((100e6).toString(), 'wei') - ); - - // current weekly inflation 75m / 52 - const weeklyInflation = parseUnits((75e6 / 52).toString()).toString(); - currentWeekOfInflation = inflationSupplyToDate.div(weeklyInflation); - - // Check result is > 0 else set to 0 for currentWeek - currentWeekOfInflation = currentWeekOfInflation.gt(constants.Zero) - ? currentWeekOfInflation.toNumber() - : 0; - - // Calculate lastMintEvent as Inflation start date + number of weeks issued * secs in weeks - const mintingBuffer = 86400; - const secondsInWeek = 604800; - const inflationStartDate = inflationStartTimestampInSecs; - currentLastMintEvent = - inflationStartDate + currentWeekOfInflation * secondsInWeek + mintingBuffer; - } catch (err) { - if (freshDeploy) { - currentSynthetixSupply = await getDeployParameter('INITIAL_ISSUANCE'); - currentWeekOfInflation = 0; - currentLastMintEvent = 0; - } else { - console.error( - red( - 'Cannot connect to existing Synthetix contract. Please double check the deploymentPath is correct for the network allocated' - ) - ); - process.exitCode = 1; - return; - } - } - - try { - oldExrates = deployer.getExistingContract({ contract: 'ExchangeRates' }); - if (!oracleExrates) { - oracleExrates = await oldExrates.methods.oracle().call(); - } - } catch (err) { - if (freshDeploy) { - oracleExrates = oracleExrates || account; - oldExrates = undefined; // unset to signify that a fresh one will be deployed - } else { - console.error( - red( - 'Cannot connect to existing ExchangeRates contract. Please double check the deploymentPath is correct for the network allocated' - ) - ); - process.exitCode = 1; - return; - } - } - - try { - const oldSystemStatus = deployer.getExistingContract({ contract: 'SystemStatus' }); - - const systemSuspensionStatus = await oldSystemStatus.methods.systemSuspension().call(); - - systemSuspended = systemSuspensionStatus.suspended; - systemSuspendedReason = systemSuspensionStatus.reason; - } catch (err) { - if (!freshDeploy) { - console.error( - red( - 'Cannot connect to existing SystemStatus contract. Please double check the deploymentPath is correct for the network allocated' - ) - ); - process.exitCode = 1; - return; - } - } - - for (const address of [account, oracleExrates]) { - if (!isAddress(address)) { - console.error(red('Invalid address detected (please check your inputs):', address)); - process.exitCode = 1; - return; - } - } - - const newSynthsToAdd = synths - .filter(({ name }) => !config[`Synth${name}`]) - .map(({ name }) => name); - - let aggregatedPriceResults = 'N/A'; - - if (oldExrates && network !== 'local') { - const padding = '\n\t\t\t\t'; - const aggResults = await checkAggregatorPrices({ - network, - useOvm, - providerUrl, - synths, - oldExrates, - standaloneFeeds, - }); - aggregatedPriceResults = padding + aggResults.join(padding); - } - - const deployerBalance = parseInt( - formatUnits(await deployer.provider.web3.eth.getBalance(account), 'ether'), - 10 - ); - if (useFork) { - // Make sure the pwned account has ETH when using a fork - const accounts = await deployer.provider.web3.eth.getAccounts(); - - await deployer.provider.web3.eth.sendTransaction({ - from: accounts[0], - to: account, - value: parseUnits('10', 'ether').toString(), - }); - } else if (deployerBalance < 5) { - console.log( - yellow(`⚠ WARNING: Deployer account balance could be too low: ${deployerBalance} ETH`) - ); - } - - let ovmDeploymentPathWarning = false; - // OVM targets must end with '-ovm'. - if (useOvm) { - const lastPathElement = path.basename(deploymentPath); - ovmDeploymentPathWarning = !lastPathElement.includes('ovm'); - } - - parameterNotice({ - 'Dry Run': dryRun ? green('true') : yellow('⚠ NO'), - 'Using a fork': useFork ? green('true') : yellow('⚠ NO'), - Concurrency: `${concurrency} max parallel calls`, - Network: network, - 'OVM?': useOvm - ? ovmDeploymentPathWarning - ? red('⚠ No -ovm folder suffix!') - : green('true') - : 'false', - 'Gas price to use': `${gasPrice} GWEI`, - 'Method call gas limit': `${methodCallGasLimit} gas`, - 'Contract deployment gas limit': `${contractDeploymentGasLimit} gas`, - 'Deployment Path': new RegExp(network, 'gi').test(deploymentPath) - ? deploymentPath - : yellow('⚠⚠⚠ cant find network name in path. Please double check this! ') + deploymentPath, - Provider: providerUrl, - 'Local build last modified': `${new Date(earliestCompiledTimestamp)} ${yellow( - ((new Date().getTime() - earliestCompiledTimestamp) / 60000).toFixed(2) + ' mins ago' - )}`, - 'Last Solidity update': - new Date(latestSolTimestamp) + - (latestSolTimestamp > earliestCompiledTimestamp - ? yellow(' ⚠⚠⚠ this is later than the last build! Is this intentional?') - : green(' ✅')), - 'Add any new synths found?': addNewSynths - ? green('✅ YES\n\t\t\t\t') + newSynthsToAdd.join(', ') - : yellow('⚠ NO'), - 'Deployer account:': account, - 'Synthetix totalSupply': `${Math.round(formatUnits(currentSynthetixSupply) / 1e6)}m`, - 'ExchangeRates Oracle': oracleExrates, - 'Last Mint Event': `${currentLastMintEvent} (${new Date(currentLastMintEvent * 1000)})`, - 'Current Weeks Of Inflation': currentWeekOfInflation, - 'Aggregated Prices': aggregatedPriceResults, - 'System Suspended': systemSuspended - ? green(' ✅', 'Reason:', systemSuspendedReason) - : yellow('⚠ NO'), - }); - - if (!yes) { - try { - await confirmAction( - yellow( - `⚠⚠⚠ WARNING: This action will deploy the following contracts to ${network}:\n${Object.entries( - config - ) - .filter(([, { deploy }]) => deploy) - .map(([contract]) => contract) - .join(', ')}` + `\nIt will also set proxy targets and add synths to Synthetix.\n` - ) + - gray('-'.repeat(50)) + - '\nDo you want to continue? (y/n) ' - ); - } catch (err) { - console.log(gray('Operation cancelled')); - return; - } - } - - console.log( - gray(`Starting deployment to ${network.toUpperCase()}${useFork ? ' (fork)' : ''}...`) - ); - - const runStep = async opts => - performTransactionalStep({ - gasLimit: methodCallGasLimit, // allow overriding of gasLimit - ...opts, - account, - gasPrice, - etherscanLinkPrefix, - ownerActions, - ownerActionsFile, - dryRun, - nonceManager: manageNonces ? nonceManager : undefined, - }); - - console.log(gray(`\n------ DEPLOY LIBRARIES ------\n`)); - - await deployer.deployContract({ - name: 'SafeDecimalMath', - }); - - await deployer.deployContract({ - name: 'Math', - }); - - console.log(gray(`\n------ DEPLOY CORE PROTOCOL ------\n`)); - - const addressOf = c => (c ? c.options.address : ''); - - const addressResolver = await deployer.deployContract({ - name: 'AddressResolver', - args: [account], - }); - - const readProxyForResolver = await deployer.deployContract({ - name: 'ReadProxyAddressResolver', - source: 'ReadProxy', - args: [account], - }); - - if (addressResolver && readProxyForResolver) { - await runStep({ - contract: 'ReadProxyAddressResolver', - target: readProxyForResolver, - read: 'target', - expected: input => input === addressOf(addressResolver), - write: 'setTarget', - writeArg: addressOf(addressResolver), - }); - } - - await deployer.deployContract({ - name: 'FlexibleStorage', - deps: ['ReadProxyAddressResolver'], - args: [addressOf(readProxyForResolver)], - }); - - const systemSettings = await deployer.deployContract({ - name: 'SystemSettings', - args: [account, addressOf(readProxyForResolver)], - }); - - const systemStatus = await deployer.deployContract({ - name: 'SystemStatus', - args: [account], - }); - - if (network !== 'mainnet' && systemStatus) { - // On testnet, give the deployer the rights to update status - await runStep({ - contract: 'SystemStatus', - target: systemStatus, - read: 'accessControl', - readArg: [toBytes32('System'), account], - expected: ({ canSuspend } = {}) => canSuspend, - write: 'updateAccessControls', - writeArg: [ - ['System', 'Issuance', 'Exchange', 'SynthExchange', 'Synth'].map(toBytes32), - [account, account, account, account, account], - [true, true, true, true, true], - [true, true, true, true, true], - ], - }); - } - - const exchangeRates = await deployer.deployContract({ - name: 'ExchangeRates', - source: useOvm ? 'ExchangeRatesWithoutInvPricing' : 'ExchangeRates', - args: [account, oracleExrates, addressOf(readProxyForResolver), [], []], - }); - - const rewardEscrow = await deployer.deployContract({ - name: 'RewardEscrow', - args: [account, ZERO_ADDRESS, ZERO_ADDRESS], - }); - - const rewardEscrowV2 = await deployer.deployContract({ - name: 'RewardEscrowV2', - source: useOvm ? 'ImportableRewardEscrowV2' : 'RewardEscrowV2', - args: [account, addressOf(readProxyForResolver)], - deps: ['AddressResolver'], - }); - - const synthetixEscrow = await deployer.deployContract({ - name: 'SynthetixEscrow', - args: [account, ZERO_ADDRESS], - }); - - const synthetixState = await deployer.deployContract({ - name: 'SynthetixState', - source: useOvm ? 'SynthetixStateWithLimitedSetup' : 'SynthetixState', - args: [account, account], - }); - - const proxyFeePool = await deployer.deployContract({ - name: 'ProxyFeePool', - source: 'Proxy', - args: [account], - }); - - const delegateApprovalsEternalStorage = await deployer.deployContract({ - name: 'DelegateApprovalsEternalStorage', - source: 'EternalStorage', - args: [account, ZERO_ADDRESS], - }); - - const delegateApprovals = await deployer.deployContract({ - name: 'DelegateApprovals', - args: [account, addressOf(delegateApprovalsEternalStorage)], - }); - - if (delegateApprovals && delegateApprovalsEternalStorage) { - await runStep({ - contract: 'EternalStorage', - target: delegateApprovalsEternalStorage, - read: 'associatedContract', - expected: input => input === addressOf(delegateApprovals), - write: 'setAssociatedContract', - writeArg: addressOf(delegateApprovals), - }); - } - - const liquidations = await deployer.deployContract({ - name: 'Liquidations', - args: [account, addressOf(readProxyForResolver)], - }); - - const eternalStorageLiquidations = await deployer.deployContract({ - name: 'EternalStorageLiquidations', - source: 'EternalStorage', - args: [account, addressOf(liquidations)], - }); - - if (liquidations && eternalStorageLiquidations) { - await runStep({ - contract: 'EternalStorageLiquidations', - target: eternalStorageLiquidations, - read: 'associatedContract', - expected: input => input === addressOf(liquidations), - write: 'setAssociatedContract', - writeArg: addressOf(liquidations), - }); - } - - const feePoolEternalStorage = await deployer.deployContract({ - name: 'FeePoolEternalStorage', - args: [account, ZERO_ADDRESS], - }); - - const feePool = await deployer.deployContract({ - name: 'FeePool', - deps: ['ProxyFeePool', 'AddressResolver'], - args: [addressOf(proxyFeePool), account, addressOf(readProxyForResolver)], - }); - - if (proxyFeePool && feePool) { - await runStep({ - contract: 'ProxyFeePool', - target: proxyFeePool, - read: 'target', - expected: input => input === addressOf(feePool), - write: 'setTarget', - writeArg: addressOf(feePool), - }); - } - - if (feePoolEternalStorage && feePool) { - await runStep({ - contract: 'FeePoolEternalStorage', - target: feePoolEternalStorage, - read: 'associatedContract', - expected: input => input === addressOf(feePool), - write: 'setAssociatedContract', - writeArg: addressOf(feePool), - }); - } - - const feePoolState = await deployer.deployContract({ - name: 'FeePoolState', - deps: ['FeePool'], - args: [account, addressOf(feePool)], - }); - - if (feePool && feePoolState) { - // Rewire feePoolState if there is a feePool upgrade - await runStep({ - contract: 'FeePoolState', - target: feePoolState, - read: 'feePool', - expected: input => input === addressOf(feePool), - write: 'setFeePool', - writeArg: addressOf(feePool), - }); - } - - const rewardsDistribution = await deployer.deployContract({ - name: 'RewardsDistribution', - deps: useOvm ? ['RewardEscrowV2', 'ProxyFeePool'] : ['RewardEscrowV2', 'ProxyFeePool'], - args: [ - account, // owner - ZERO_ADDRESS, // authority (synthetix) - ZERO_ADDRESS, // Synthetix Proxy - addressOf(rewardEscrowV2), - addressOf(proxyFeePool), - ], - }); - - // New Synthetix proxy. - const proxyERC20Synthetix = await deployer.deployContract({ - name: 'ProxyERC20', - args: [account], - }); - - const tokenStateSynthetix = await deployer.deployContract({ - name: 'TokenStateSynthetix', - source: 'TokenState', - args: [account, account], - }); - - const synthetix = await deployer.deployContract({ - name: 'Synthetix', - source: useOvm ? 'MintableSynthetix' : 'Synthetix', - deps: ['ProxyERC20', 'TokenStateSynthetix', 'AddressResolver'], - args: [ - addressOf(proxyERC20Synthetix), - addressOf(tokenStateSynthetix), - account, - currentSynthetixSupply, - addressOf(readProxyForResolver), - ], - }); - - if (synthetix && proxyERC20Synthetix) { - await runStep({ - contract: 'ProxyERC20', - target: proxyERC20Synthetix, - read: 'target', - expected: input => input === addressOf(synthetix), - write: 'setTarget', - writeArg: addressOf(synthetix), - }); - await runStep({ - contract: 'Synthetix', - target: synthetix, - read: 'proxy', - expected: input => input === addressOf(proxyERC20Synthetix), - write: 'setProxy', - writeArg: addressOf(proxyERC20Synthetix), - }); - } - - // Old Synthetix proxy based off Proxy.sol: this has been deprecated. - // To be removed after May 30, 2020: - // https://docs.synthetix.io/integrations/guide/#proxy-deprecation - const proxySynthetix = await deployer.deployContract({ - name: 'ProxySynthetix', - source: 'Proxy', - args: [account], - }); - if (proxySynthetix && synthetix) { - await runStep({ - contract: 'ProxySynthetix', - target: proxySynthetix, - read: 'target', - expected: input => input === addressOf(synthetix), - write: 'setTarget', - writeArg: addressOf(synthetix), - }); - } - - const debtCache = await deployer.deployContract({ - name: 'DebtCache', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - - let exchanger; - if (useOvm) { - exchanger = await deployer.deployContract({ - name: 'Exchanger', - source: 'Exchanger', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - } else { - exchanger = await deployer.deployContract({ - name: 'Exchanger', - source: 'ExchangerWithVirtualSynth', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - - await deployer.deployContract({ - name: 'VirtualSynthMastercopy', - }); - } - - const exchangeState = await deployer.deployContract({ - name: 'ExchangeState', - deps: ['Exchanger'], - args: [account, addressOf(exchanger)], - }); - - if (exchanger && exchangeState) { - // The exchangeState contract has Exchanger as it's associated contract - await runStep({ - contract: 'ExchangeState', - target: exchangeState, - read: 'associatedContract', - expected: input => input === exchanger.options.address, - write: 'setAssociatedContract', - writeArg: exchanger.options.address, - }); - } - - if (exchanger && systemStatus) { - // SIP-65: ensure Exchanger can suspend synths if price spikes occur - await runStep({ - contract: 'SystemStatus', - target: systemStatus, - read: 'accessControl', - readArg: [toBytes32('Synth'), addressOf(exchanger)], - expected: ({ canSuspend } = {}) => canSuspend, - write: 'updateAccessControl', - writeArg: [toBytes32('Synth'), addressOf(exchanger), true, false], - }); - } - - // only reset token state if redeploying - if (tokenStateSynthetix && config['TokenStateSynthetix'].deploy) { - const initialIssuance = await getDeployParameter('INITIAL_ISSUANCE'); - await runStep({ - contract: 'TokenStateSynthetix', - target: tokenStateSynthetix, - read: 'balanceOf', - readArg: account, - expected: input => input === initialIssuance, - write: 'setBalanceOf', - writeArg: [account, initialIssuance], - }); - } - - if (tokenStateSynthetix && synthetix) { - await runStep({ - contract: 'TokenStateSynthetix', - target: tokenStateSynthetix, - read: 'associatedContract', - expected: input => input === addressOf(synthetix), - write: 'setAssociatedContract', - writeArg: addressOf(synthetix), - }); - } - - const issuer = await deployer.deployContract({ - name: 'Issuer', - source: useOvm ? 'IssuerWithoutLiquidations' : 'Issuer', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - - const issuerAddress = addressOf(issuer); - - await deployer.deployContract({ - name: 'TradingRewards', - deps: ['AddressResolver', 'Exchanger'], - args: [account, account, addressOf(readProxyForResolver)], - }); - - if (synthetixState && issuer) { - // The SynthetixState contract has Issuer as it's associated contract (after v2.19 refactor) - await runStep({ - contract: 'SynthetixState', - target: synthetixState, - read: 'associatedContract', - expected: input => input === issuerAddress, - write: 'setAssociatedContract', - writeArg: issuerAddress, - }); - } - - if (useOvm && synthetixState && feePool) { - // The SynthetixStateLimitedSetup) contract has FeePool to appendAccountIssuanceRecord - await runStep({ - contract: 'SynthetixState', - target: synthetixState, - read: 'feePool', - expected: input => input === addressOf(feePool), - write: 'setFeePool', - writeArg: addressOf(feePool), - }); - } - - if (synthetixEscrow) { - await deployer.deployContract({ - name: 'EscrowChecker', - deps: ['SynthetixEscrow'], - args: [addressOf(synthetixEscrow)], - }); - } - - if (rewardEscrow && synthetix) { - await runStep({ - contract: 'RewardEscrow', - target: rewardEscrow, - read: 'synthetix', - expected: input => input === addressOf(synthetix), - write: 'setSynthetix', - writeArg: addressOf(synthetix), - }); - } - - if (rewardEscrow && feePool) { - await runStep({ - contract: 'RewardEscrow', - target: rewardEscrow, - read: 'feePool', - expected: input => input === addressOf(feePool), - write: 'setFeePool', - writeArg: addressOf(feePool), - }); - } - - if (!useOvm) { - const supplySchedule = await deployer.deployContract({ - name: 'SupplySchedule', - args: [account, currentLastMintEvent, currentWeekOfInflation], - }); - if (supplySchedule && synthetix) { - await runStep({ - contract: 'SupplySchedule', - target: supplySchedule, - read: 'synthetixProxy', - expected: input => input === addressOf(proxySynthetix), - write: 'setSynthetixProxy', - writeArg: addressOf(proxySynthetix), - }); - } - } - - if (synthetix && rewardsDistribution) { - await runStep({ - contract: 'RewardsDistribution', - target: rewardsDistribution, - read: 'authority', - expected: input => input === addressOf(synthetix), - write: 'setAuthority', - writeArg: addressOf(synthetix), - }); - - await runStep({ - contract: 'RewardsDistribution', - target: rewardsDistribution, - read: 'synthetixProxy', - expected: input => input === addressOf(proxyERC20Synthetix), - write: 'setSynthetixProxy', - writeArg: addressOf(proxyERC20Synthetix), - }); - } - - // RewardEscrow on RewardsDistribution should be set to new RewardEscrowV2 - if (rewardEscrowV2 && rewardsDistribution) { - await runStep({ - contract: 'RewardsDistribution', - target: rewardsDistribution, - read: 'rewardEscrow', - expected: input => input === addressOf(rewardEscrowV2), - write: 'setRewardEscrow', - writeArg: addressOf(rewardEscrowV2), - }); - } - - // ---------------- - // Setting proxyERC20 Synthetix for synthetixEscrow - // ---------------- - - // Skip setting unless redeploying either of these, - if (config['Synthetix'].deploy || config['SynthetixEscrow'].deploy) { - // Note: currently on mainnet SynthetixEscrow.methods.synthetix() does NOT exist - // it is "havven" and the ABI we have here is not sufficient - if (network === 'mainnet' && !useOvm) { - await runStep({ - contract: 'SynthetixEscrow', - target: synthetixEscrow, - read: 'havven', - expected: input => input === addressOf(proxyERC20Synthetix), - write: 'setHavven', - writeArg: addressOf(proxyERC20Synthetix), - }); - } else { - await runStep({ - contract: 'SynthetixEscrow', - target: synthetixEscrow, - read: 'synthetix', - expected: input => input === addressOf(proxyERC20Synthetix), - write: 'setSynthetix', - writeArg: addressOf(proxyERC20Synthetix), - }); - } - } - - // ---------------- - // Synths - // ---------------- - console.log(gray(`\n------ DEPLOY SYNTHS ------\n`)); - - // The list of synth to be added to the Issuer once dependencies have been set up - const synthsToAdd = []; - - for (const { name: currencyKey, subclass, asset } of synths) { - console.log(gray(`\n --- SYNTH ${currencyKey} ---\n`)); - - const tokenStateForSynth = await deployer.deployContract({ - name: `TokenState${currencyKey}`, - source: 'TokenState', - args: [account, ZERO_ADDRESS], - force: addNewSynths, - }); - - // Legacy proxy will be around until May 30, 2020 - // https://docs.synthetix.io/integrations/guide/#proxy-deprecation - // Until this time, on mainnet we will still deploy ProxyERC20sUSD and ensure that - // SynthsUSD.proxy is ProxyERC20sUSD, SynthsUSD.integrationProxy is ProxysUSD - const synthProxyIsLegacy = currencyKey === 'sUSD' && network === 'mainnet'; - - const proxyForSynth = await deployer.deployContract({ - name: `Proxy${currencyKey}`, - source: synthProxyIsLegacy ? 'Proxy' : 'ProxyERC20', - args: [account], - force: addNewSynths, - }); - - // additionally deploy an ERC20 proxy for the synth if it's legacy (sUSD) - let proxyERC20ForSynth; - if (currencyKey === 'sUSD') { - proxyERC20ForSynth = await deployer.deployContract({ - name: `ProxyERC20${currencyKey}`, - source: `ProxyERC20`, - args: [account], - force: addNewSynths, - }); - } - - const currencyKeyInBytes = toBytes32(currencyKey); - - const synthConfig = config[`Synth${currencyKey}`] || {}; - - // track the original supply if we're deploying a new synth contract for an existing synth - let originalTotalSupply = 0; - if (synthConfig.deploy) { - try { - const oldSynth = deployer.getExistingContract({ contract: `Synth${currencyKey}` }); - originalTotalSupply = await oldSynth.methods.totalSupply().call(); - } catch (err) { - if (!freshDeploy) { - // only throw if not local - allows local environments to handle both new - // and updating configurations - throw err; - } - } - } - - // user confirm totalSupply is correct for oldSynth before deploy new Synth - if (synthConfig.deploy && !yes && originalTotalSupply > 0) { - try { - await confirmAction( - yellow( - `⚠⚠⚠ WARNING: Please confirm - ${network}:\n` + - `Synth${currencyKey} totalSupply is ${originalTotalSupply} \n` - ) + - gray('-'.repeat(50)) + - '\nDo you want to continue? (y/n) ' - ); - } catch (err) { - console.log(gray('Operation cancelled')); - return; - } - } - - const sourceContract = subclass || 'Synth'; - const synth = await deployer.deployContract({ - name: `Synth${currencyKey}`, - source: sourceContract, - deps: [`TokenState${currencyKey}`, `Proxy${currencyKey}`, 'Synthetix', 'FeePool'], - args: [ - proxyERC20ForSynth ? addressOf(proxyERC20ForSynth) : addressOf(proxyForSynth), - addressOf(tokenStateForSynth), - `Synth ${currencyKey}`, - currencyKey, - account, - currencyKeyInBytes, - originalTotalSupply, - addressOf(readProxyForResolver), - ], - force: addNewSynths, - }); - - if (tokenStateForSynth && synth) { - await runStep({ - contract: `TokenState${currencyKey}`, - target: tokenStateForSynth, - read: 'associatedContract', - expected: input => input === addressOf(synth), - write: 'setAssociatedContract', - writeArg: addressOf(synth), - }); - } - - // Setup proxy for synth - if (proxyForSynth && synth) { - await runStep({ - contract: `Proxy${currencyKey}`, - target: proxyForSynth, - read: 'target', - expected: input => input === addressOf(synth), - write: 'setTarget', - writeArg: addressOf(synth), - }); - - // Migration Phrase 2: if there's a ProxyERC20sUSD then the Synth's proxy must use it - await runStep({ - contract: `Synth${currencyKey}`, - target: synth, - read: 'proxy', - expected: input => input === addressOf(proxyERC20ForSynth || proxyForSynth), - write: 'setProxy', - writeArg: addressOf(proxyERC20ForSynth || proxyForSynth), - }); - - if (proxyERC20ForSynth) { - // and make sure this new proxy has the target of the synth - await runStep({ - contract: `ProxyERC20${currencyKey}`, - target: proxyERC20ForSynth, - read: 'target', - expected: input => input === addressOf(synth), - write: 'setTarget', - writeArg: addressOf(synth), - }); - } - } - - // Save the synth to be added once the AddressResolver has been synced. - if (synth && issuer) { - synthsToAdd.push({ - synth, - currencyKeyInBytes, - }); - } - - const { feed } = feeds[asset] || {}; - - // now setup price aggregator if any for the synth - if (isAddress(feed) && exchangeRates) { - await runStep({ - contract: `ExchangeRates`, - target: exchangeRates, - read: 'aggregators', - readArg: currencyKeyInBytes, - expected: input => input === feed, - write: 'addAggregator', - writeArg: [currencyKeyInBytes, feed], - }); - } - } - - console.log(gray(`\n------ DEPLOY ANCILLARY CONTRACTS ------\n`)); - - await deployer.deployContract({ - name: 'Depot', - deps: ['ProxySynthetix', 'SynthsUSD', 'FeePool'], - args: [account, account, addressOf(readProxyForResolver)], - }); - - // let manager, collateralEth, collateralErc20, collateralShort; - - if (useOvm) { - await deployer.deployContract({ - // name is EtherCollateral as it behaves as EtherCollateral in the address resolver - name: 'EtherCollateral', - source: 'EmptyEtherCollateral', - args: [], - }); - await deployer.deployContract({ - name: 'EtherCollateralsUSD', - source: 'EmptyEtherCollateral', - args: [], - }); - await deployer.deployContract({ - name: 'CollateralManager', - source: 'EmptyCollateralManager', - args: [], - }); - await deployer.deployContract({ - name: 'SynthetixBridgeToBase', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - } else { - await deployer.deployContract({ - name: 'EtherCollateral', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - await deployer.deployContract({ - name: 'EtherCollateralsUSD', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - const SynthetixBridgeToOptimism = await deployer.deployContract({ - name: 'SynthetixBridgeToOptimism', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - const SynthetixBridgeEscrow = await deployer.deployContract({ - name: 'SynthetixBridgeEscrow', - deps: ['AddressResolver'], - args: [account], - }); - - const allowance = await proxyERC20Synthetix.methods - .allowance(addressOf(SynthetixBridgeEscrow), addressOf(SynthetixBridgeToOptimism)) - .call(); - if (allowance.toString() === '0') { - await runStep({ - contract: `SynthetixBridgeEscrow`, - target: SynthetixBridgeEscrow, - write: 'approveBridge', - writeArg: [ - addressOf(proxyERC20Synthetix), - addressOf(SynthetixBridgeToOptimism), - parseUnits('100000000').toString(), - ], - }); - } - } - - let WETH_ADDRESS = (await getDeployParameter('WETH_ERC20_ADDRESSES'))[network]; - - if (network === 'local') { - // On local, deploy a mock WETH token. - // OVM already has a deployment of WETH, however since we use - // Hardhat for the local-ovm environment, we must deploy - // our own. - const weth = await deployer.deployContract({ - name: useOvm ? 'MockWETH' : 'WETH', - force: true, - }); - WETH_ADDRESS = weth.options.address; - } - - if (!WETH_ADDRESS) { - throw new Error('WETH address is not known'); - } - - await deployer.deployContract({ - name: 'EtherWrapper', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver), WETH_ADDRESS], - }); - - if (!useOvm) { - await deployer.deployContract({ - name: 'NativeEtherWrapper', - deps: ['AddressResolver'], - args: [account, addressOf(readProxyForResolver)], - }); - } - - // ---------------- - // Binary option market factory and manager setup - // ---------------- - - console.log(gray(`\n------ DEPLOY BINARY OPTIONS ------\n`)); - - await deployer.deployContract({ - name: 'BinaryOptionMarketFactory', - args: [account, addressOf(readProxyForResolver)], - deps: ['AddressResolver'], - }); - - const day = 24 * 60 * 60; - const maxOraclePriceAge = 120 * 60; // Price updates are accepted from up to two hours before maturity to allow for delayed chainlink heartbeats. - const expiryDuration = 26 * 7 * day; // Six months to exercise options before the market is destructible. - const maxTimeToMaturity = 730 * day; // Markets may not be deployed more than two years in the future. - const creatorCapitalRequirement = parseUnits('1000').toString(); // 1000 sUSD is required to create a new market. - const creatorSkewLimit = parseUnits('0.05').toString(); // Market creators must leave 5% or more of their position on either side. - const poolFee = parseUnits('0.008').toString(); // 0.8% of the market's value goes to the pool in the end. - const creatorFee = parseUnits('0.002').toString(); // 0.2% of the market's value goes to the creator. - const refundFee = parseUnits('0.05').toString(); // 5% of a bid stays in the pot if it is refunded. - const binaryOptionMarketManager = await deployer.deployContract({ - name: 'BinaryOptionMarketManager', - args: [ - account, - addressOf(readProxyForResolver), - maxOraclePriceAge, - expiryDuration, - maxTimeToMaturity, - creatorCapitalRequirement, - creatorSkewLimit, - poolFee, - creatorFee, - refundFee, - ], - deps: ['AddressResolver'], - }); - - console.log(gray(`\n------ DEPLOY DAPP UTILITIES ------\n`)); - - await deployer.deployContract({ - name: 'SynthUtil', - deps: ['ReadProxyAddressResolver'], - args: [addressOf(readProxyForResolver)], - }); - - await deployer.deployContract({ - name: 'DappMaintenance', - args: [account], - }); - - await deployer.deployContract({ - name: 'BinaryOptionMarketData', - }); - - console.log(gray(`\n------ CONFIGURE STANDLONE FEEDS ------\n`)); - - // Setup remaining price feeds (that aren't synths) - - for (const { asset, feed } of standaloneFeeds) { - if (isAddress(feed) && exchangeRates) { - await runStep({ - contract: `ExchangeRates`, - target: exchangeRates, - read: 'aggregators', - readArg: toBytes32(asset), - expected: input => input === feed, - write: 'addAggregator', - writeArg: [toBytes32(asset), feed], - }); - } - } - - // ---------------- - // Multi Collateral System - // ---------------- - let collateralManager, collateralEth, collateralErc20, collateralShort; - - const collateralManagerDefaults = await getDeployParameter('COLLATERAL_MANAGER'); - - if (!useOvm) { - console.log(gray(`\n------ DEPLOY MULTI COLLATERAL ------\n`)); - - const managerState = await deployer.deployContract({ - name: 'CollateralManagerState', - args: [account, account], - }); - - collateralManager = await deployer.deployContract({ - name: 'CollateralManager', - args: [ - addressOf(managerState), - account, - addressOf(readProxyForResolver), - collateralManagerDefaults['MAX_DEBT'], - collateralManagerDefaults['BASE_BORROW_RATE'], - collateralManagerDefaults['BASE_SHORT_RATE'], - ], - }); - - if (managerState && collateralManager) { - await runStep({ - contract: 'CollateralManagerState', - target: managerState, - read: 'associatedContract', - expected: input => input === addressOf(collateralManager), - write: 'setAssociatedContract', - writeArg: addressOf(collateralManager), - }); - } - - const collateralStateEth = await deployer.deployContract({ - name: 'CollateralStateEth', - source: 'CollateralState', - args: [account, account], - }); - - collateralEth = await deployer.deployContract({ - name: 'CollateralEth', - args: [ - addressOf(collateralStateEth), - account, - addressOf(collateralManager), - addressOf(readProxyForResolver), - toBytes32('sETH'), - (await getDeployParameter('COLLATERAL_ETH'))['MIN_CRATIO'], - (await getDeployParameter('COLLATERAL_ETH'))['MIN_COLLATERAL'], - ], - }); - - if (collateralStateEth && collateralEth) { - await runStep({ - contract: 'CollateralStateEth', - target: collateralStateEth, - read: 'associatedContract', - expected: input => input === addressOf(collateralEth), - write: 'setAssociatedContract', - writeArg: addressOf(collateralEth), - }); - } - - const collateralStateErc20 = await deployer.deployContract({ - name: 'CollateralStateErc20', - source: 'CollateralState', - args: [account, account], - }); - - let RENBTC_ADDRESS = (await getDeployParameter('RENBTC_ERC20_ADDRESSES'))[network]; - if (!RENBTC_ADDRESS) { - if (network !== 'local') { - throw new Error('renBTC address is not known'); - } - - // On local, deploy a mock renBTC token to use as the underlying in CollateralErc20 - const renBTC = await deployer.deployContract({ - name: 'MockToken', - args: ['renBTC', 'renBTC', 8], - }); - - RENBTC_ADDRESS = renBTC.options.address; - } - - collateralErc20 = await deployer.deployContract({ - name: 'CollateralErc20', - source: 'CollateralErc20', - args: [ - addressOf(collateralStateErc20), - account, - addressOf(collateralManager), - addressOf(readProxyForResolver), - toBytes32('sBTC'), - (await getDeployParameter('COLLATERAL_RENBTC'))['MIN_CRATIO'], - (await getDeployParameter('COLLATERAL_RENBTC'))['MIN_COLLATERAL'], - RENBTC_ADDRESS, - 8, - ], - }); - - if (collateralStateErc20 && collateralErc20) { - await runStep({ - contract: 'CollateralStateErc20', - target: collateralStateErc20, - read: 'associatedContract', - expected: input => input === addressOf(collateralErc20), - write: 'setAssociatedContract', - writeArg: addressOf(collateralErc20), - }); - } - - const collateralStateShort = await deployer.deployContract({ - name: 'CollateralStateShort', - source: 'CollateralState', - args: [account, account], - }); - - collateralShort = await deployer.deployContract({ - name: 'CollateralShort', - args: [ - addressOf(collateralStateShort), - account, - addressOf(collateralManager), - addressOf(readProxyForResolver), - toBytes32('sUSD'), - (await getDeployParameter('COLLATERAL_SHORT'))['MIN_CRATIO'], - (await getDeployParameter('COLLATERAL_SHORT'))['MIN_COLLATERAL'], - ], - }); - - if (collateralStateShort && collateralShort) { - await runStep({ - contract: 'CollateralStateShort', - target: collateralStateShort, - read: 'associatedContract', - expected: input => input === collateralShort.options.address, - write: 'setAssociatedContract', - writeArg: collateralShort.options.address, - }); - } - } - - console.log(gray(`\n------ CONFIGURE ADDRESS RESOLVER ------\n`)); - - let addressesAreImported = false; - - if (addressResolver) { - const addressArgs = [[], []]; - - const allContracts = Object.entries(deployer.deployedContracts); - await Promise.all( - allContracts.map(([name, contract]) => { - return limitPromise(async () => { - const isImported = await addressResolver.methods - .areAddressesImported([toBytes32(name)], [contract.options.address]) - .call(); - - if (!isImported) { - console.log(green(`${name} needs to be imported to the AddressResolver`)); - - addressArgs[0].push(toBytes32(name)); - addressArgs[1].push(contract.options.address); - } - }); - }) - ); - - const { pending } = await runStep({ - gasLimit: 6e6, // higher gas required - contract: `AddressResolver`, - target: addressResolver, - read: 'areAddressesImported', - readArg: addressArgs, - expected: input => input, - write: 'importAddresses', - writeArg: addressArgs, - }); - - addressesAreImported = !pending; - } - - // Whewn addresses - // This relies on the fact that runStep returns undefined if nothing needed to be done, a tx hash if the - // transaction could be mined, and true in other cases, including appending to the owner actions file. - // Note that this will also end the script in the case of manual transaction mining. - if (!addressesAreImported) { - console.log(gray(`\n------ DEPLOY PARTIALLY COMPLETED ------\n`)); - - console.log( - yellow( - '⚠⚠⚠ WARNING: Addresses have not been imported into the resolver, owner actions must be performed before re-running the script.' - ) - ); - - if (deployer.newContractsDeployed.length > 0) { - reportDeployedContracts({ deployer }); - } - - process.exit(1); - } - - console.log(gray('Addresses are correctly set up, continuing...')); - - // Legacy contracts. - if (network === 'mainnet') { - // v2.35.2 contracts. - const CollateralEth = '0x3FF5c0A14121Ca39211C95f6cEB221b86A90729E'; - const CollateralErc20REN = '0x3B3812BB9f6151bEb6fa10783F1ae848a77a0d46'; - const CollateralShort = '0x188C2274B04Ea392B21487b5De299e382Ff84246'; - - const legacyContracts = Object.entries({ - CollateralEth, - CollateralErc20REN, - CollateralShort, - }).map(([name, address]) => { - const contract = new deployer.provider.web3.eth.Contract( - [...compiled['MixinResolver'].abi, ...compiled['Owned'].abi], - address - ); - return [`legacy:${name}`, contract]; - }); - - await Promise.all( - legacyContracts.map(async ([name, contract]) => { - return runStep({ - gasLimit: 7e6, - contract: name, - target: contract, - read: 'isResolverCached', - expected: input => input, - publiclyCallable: true, // does not require owner - write: 'rebuildCache', - }); - }) - ); - } - - const filterTargetsWith = ({ prop }) => - Object.entries(deployer.deployedContracts).filter(([, target]) => - target.options.jsonInterface.find(({ name }) => name === prop) - ); - - const contractsWithRebuildableCache = filterTargetsWith({ prop: 'rebuildCache' }); - - // collect all resolver addresses required - const resolverAddressesRequired = ( - await Promise.all( - contractsWithRebuildableCache.map(([, contract]) => { - return limitPromise(() => contract.methods.resolverAddressesRequired().call()); - }) - ) - ).reduce((allAddresses, contractAddresses) => { - return allAddresses.concat( - contractAddresses.filter(contractAddress => !allAddresses.includes(contractAddress)) - ); - }, []); - - // check which resolver addresses are imported - const resolvedAddresses = await Promise.all( - resolverAddressesRequired.map(id => { - return limitPromise(() => addressResolver.methods.getAddress(id).call()); - }) - ); - const isResolverAddressImported = {}; - for (let i = 0; i < resolverAddressesRequired.length; i++) { - isResolverAddressImported[resolverAddressesRequired[i]] = resolvedAddresses[i] !== ZERO_ADDRESS; - } - - // print out resolver addresses - console.log(gray('Imported resolver addresses:')); - for (const id of Object.keys(isResolverAddressImported)) { - const isImported = isResolverAddressImported[id]; - const chalkFn = isImported ? gray : red; - console.log(chalkFn(` > ${fromBytes32(id)}: ${isImported}`)); - } - - // now ensure all caches are rebuilt for those in need - const contractsToRebuildCache = []; - for (const [name, target] of contractsWithRebuildableCache) { - const isCached = await target.methods.isResolverCached().call(); - if (!isCached) { - const requiredAddresses = await target.methods.resolverAddressesRequired().call(); - - const unknownAddress = requiredAddresses.find(id => !isResolverAddressImported[id]); - if (unknownAddress) { - console.log( - redBright( - `WARINING: Not invoking ${name}.rebuildCache() because ${fromBytes32( - unknownAddress - )} is unknown. This contract requires: ${requiredAddresses.map(id => fromBytes32(id))}` - ) - ); - } else { - contractsToRebuildCache.push(target.options.address); - } - } - } - - const addressesChunkSize = useOvm ? 7 : 20; - for (let i = 0; i < contractsToRebuildCache.length; i += addressesChunkSize) { - const chunk = contractsToRebuildCache.slice(i, i + addressesChunkSize); - await runStep({ - gasLimit: useOvm ? OVM_MAX_GAS_LIMIT : 7e6, - contract: `AddressResolver`, - target: addressResolver, - publiclyCallable: true, // does not require owner - write: 'rebuildCaches', - writeArg: [chunk], - }); - } - - console.log(gray('Double check all contracts with rebuildCache() are rebuilt...')); - for (const [contract, target] of contractsWithRebuildableCache) { - if (contractsToRebuildCache.includes(target.options.address)) { - await runStep({ - gasLimit: 500e3, // higher gas required - contract, - target, - read: 'isResolverCached', - expected: input => input, - publiclyCallable: true, // does not require owner - write: 'rebuildCache', - }); - } - } - - // Now do binary option market cache rebuilding - if (binaryOptionMarketManager) { - console.log(gray('Checking all binary option markets have rebuilt caches')); - let binaryOptionMarkets = []; - // now grab all possible binary option markets to rebuild caches as well - const binaryOptionsFetchPageSize = 100; - for (const marketType of ['Active', 'Matured']) { - const numBinaryOptionMarkets = Number( - await binaryOptionMarketManager.methods[`num${marketType}Markets`]().call() - ); - console.log( - gray('Found'), - yellow(numBinaryOptionMarkets), - gray(marketType, 'binary option markets') - ); - - if (numBinaryOptionMarkets > binaryOptionsFetchPageSize) { - console.log( - redBright( - '⚠⚠⚠ Warning: cannot fetch all', - marketType, - 'binary option markets as there are', - numBinaryOptionMarkets, - 'which is more than page size of', - binaryOptionsFetchPageSize - ) - ); - } else { - // fetch the list of markets - const marketAddresses = await binaryOptionMarketManager.methods[ - `${marketType.toLowerCase()}Markets` - ](0, binaryOptionsFetchPageSize).call(); - - // wrap them in a contract via the deployer - const markets = marketAddresses.map( - binaryOptionMarket => - new deployer.provider.web3.eth.Contract( - compiled['BinaryOptionMarket'].abi, - binaryOptionMarket - ) - ); - - binaryOptionMarkets = binaryOptionMarkets.concat(markets); - } - } - - // now figure out which binary option markets need their caches rebuilt - const binaryOptionMarketsToRebuildCacheOn = []; - for (const market of binaryOptionMarkets) { - try { - const isCached = await market.methods.isResolverCached().call(); - if (!isCached) { - binaryOptionMarketsToRebuildCacheOn.push(addressOf(market)); - } - console.log( - gray('Binary option market'), - yellow(addressOf(market)), - gray('is newer and cache status'), - yellow(isCached) - ); - } catch (err) { - // the challenge being that some used an older MixinResolver API - const oldBinaryOptionMarketABI = [ - { - constant: true, - inputs: [ - { - internalType: 'contract AddressResolver', - name: '_resolver', - type: 'address', - }, - ], - name: 'isResolverCached', - outputs: [ - { - internalType: 'bool', - name: '', - type: 'bool', - }, - ], - payable: false, - stateMutability: 'view', - type: 'function', - signature: '0x631e1444', - }, - ]; - - const oldBinaryOptionMarket = new deployer.provider.web3.eth.Contract( - oldBinaryOptionMarketABI, - addressOf(market) - ); - - const isCached = await oldBinaryOptionMarket.methods - .isResolverCached(addressOf(readProxyForResolver)) - .call(); - if (!isCached) { - binaryOptionMarketsToRebuildCacheOn.push(addressOf(market)); - } - - console.log( - gray('Binary option market'), - yellow(addressOf(market)), - gray('is older and cache status'), - yellow(isCached) - ); - } - } - - console.log( - gray('In total'), - yellow(binaryOptionMarketsToRebuildCacheOn.length), - gray('binary option markets need their caches rebuilt') - ); - - const addressesChunkSize = useOvm ? 7 : 20; - for (let i = 0; i < binaryOptionMarketsToRebuildCacheOn.length; i += addressesChunkSize) { - const chunk = binaryOptionMarketsToRebuildCacheOn.slice(i, i + addressesChunkSize); - await runStep({ - gasLimit: useOvm ? OVM_MAX_GAS_LIMIT : 7e6, - contract: `BinaryOptionMarketManager`, - target: binaryOptionMarketManager, - publiclyCallable: true, // does not require owner - write: 'rebuildMarketCaches', - writeArg: [chunk], - }); - } - } - - // Now perform a sync of legacy contracts that have not been replaced in Shaula (v2.35.x) - // EtherCollateral, EtherCollateralsUSD - console.log(gray('Checking all legacy contracts with setResolverAndSyncCache() are rebuilt...')); - const contractsWithLegacyResolverCaching = filterTargetsWith({ - prop: 'setResolverAndSyncCache', - }); - for (const [contract, target] of contractsWithLegacyResolverCaching) { - await runStep({ - gasLimit: 500e3, // higher gas required - contract, - target, - read: 'isResolverCached', - readArg: addressOf(readProxyForResolver), - expected: input => input, - write: 'setResolverAndSyncCache', - writeArg: addressOf(readProxyForResolver), - }); - } - - // Finally set resolver on contracts even older than legacy (Depot) - console.log(gray('Checking all legacy contracts with setResolver() are rebuilt...')); - const contractsWithLegacyResolverNoCache = filterTargetsWith({ - prop: 'setResolver', - }); - for (const [contract, target] of contractsWithLegacyResolverNoCache) { - await runStep({ - gasLimit: 500e3, // higher gas required - contract, - target, - read: 'resolver', - expected: input => addressOf(readProxyForResolver), - write: 'setResolver', - writeArg: addressOf(readProxyForResolver), - }); - } - - console.log(gray('All caches are rebuilt. Continuing.')); - - // now after resolvers have been set - - console.log(gray(`\n------ ADD SYNTHS TO ISSUER ------\n`)); - - // Set up the connection to the Issuer for each Synth (requires FlexibleStorage to have been configured) - - // First filter out all those synths which are already properly imported - console.log(gray('Filtering synths to add to the issuer.')); - const filteredSynths = []; - for (const synth of synthsToAdd) { - const issuerSynthAddress = await issuer.methods.synths(synth.currencyKeyInBytes).call(); - const currentSynthAddress = addressOf(synth.synth); - if (issuerSynthAddress === currentSynthAddress) { - console.log(gray(`${currentSynthAddress} requires no action`)); - } else { - console.log(gray(`${currentSynthAddress} will be added to the issuer.`)); - filteredSynths.push(synth); - } - } - - const synthChunkSize = 15; - for (let i = 0; i < filteredSynths.length; i += synthChunkSize) { - const chunk = filteredSynths.slice(i, i + synthChunkSize); - await runStep({ - contract: 'Issuer', - target: issuer, - read: 'getSynths', - readArg: [chunk.map(synth => synth.currencyKeyInBytes)], - expected: input => - input.length === chunk.length && - input.every((cur, idx) => cur === addressOf(chunk[idx].synth)), - write: 'addSynths', - writeArg: [chunk.map(synth => addressOf(synth.synth))], - gasLimit: 1e5 * synthChunkSize, - }); - } - - console.log(gray(`\n------ CONFIGURE INVERSE SYNTHS ------\n`)); - - for (const { name: currencyKey, inverted } of synths) { - if (inverted) { - const { entryPoint, upperLimit, lowerLimit } = inverted; - - // helper function - const setInversePricing = ({ freezeAtUpperLimit, freezeAtLowerLimit }) => - runStep({ - contract: 'ExchangeRates', - target: exchangeRates, - write: 'setInversePricing', - writeArg: [ - toBytes32(currencyKey), - parseUnits(entryPoint.toString()).toString(), - parseUnits(upperLimit.toString()).toString(), - parseUnits(lowerLimit.toString()).toString(), - freezeAtUpperLimit, - freezeAtLowerLimit, - ], - }); - - // when the oldExrates exists - meaning there is a valid ExchangeRates in the existing deployment.json - // for this environment (true for all environments except the initial deploy in 'local' during those tests) - if (oldExrates) { - // get inverse synth's params from the old exrates, if any exist - const oldInversePricing = await oldExrates.methods - .inversePricing(toBytes32(currencyKey)) - .call(); - - const { - entryPoint: oldEntryPoint, - upperLimit: oldUpperLimit, - lowerLimit: oldLowerLimit, - frozenAtUpperLimit: currentRateIsFrozenUpper, - frozenAtLowerLimit: currentRateIsFrozenLower, - } = oldInversePricing; - - const currentRateIsFrozen = currentRateIsFrozenUpper || currentRateIsFrozenLower; - // and the last rate if any exists - const currentRateForCurrency = await oldExrates.methods - .rateForCurrency(toBytes32(currencyKey)) - .call(); - - // and total supply, if any - const synth = deployer.deployedContracts[`Synth${currencyKey}`]; - const totalSynthSupply = await synth.methods.totalSupply().call(); - console.log(gray(`totalSupply of ${currencyKey}: ${Number(totalSynthSupply)}`)); - - const inversePricingOnCurrentExRates = await exchangeRates.methods - .inversePricing(toBytes32(currencyKey)) - .call(); - - // ensure that if it's a newer exchange rates deployed, then skip reinserting the inverse pricing if - // already done - if ( - oldExrates.options.address !== exchangeRates.options.address && - JSON.stringify(inversePricingOnCurrentExRates) === JSON.stringify(oldInversePricing) && - +formatUnits(inversePricingOnCurrentExRates.entryPoint) === entryPoint && - +formatUnits(inversePricingOnCurrentExRates.upperLimit) === upperLimit && - +formatUnits(inversePricingOnCurrentExRates.lowerLimit) === lowerLimit - ) { - console.log( - gray( - `Current ExchangeRates.inversePricing(${currencyKey}) is the same as the previous. Nothing to do.` - ) - ); - } - // When there's an inverted synth with matching parameters - else if ( - entryPoint === +formatUnits(oldEntryPoint) && - upperLimit === +formatUnits(oldUpperLimit) && - lowerLimit === +formatUnits(oldLowerLimit) - ) { - if (oldExrates.options.address !== addressOf(exchangeRates)) { - const freezeAtUpperLimit = +formatUnits(currentRateForCurrency) === upperLimit; - const freezeAtLowerLimit = +formatUnits(currentRateForCurrency) === lowerLimit; - console.log( - gray( - `Detected an existing inverted synth for ${currencyKey} with identical parameters and a newer ExchangeRates. ` + - `Persisting its frozen status (${currentRateIsFrozen}) and if frozen, then freeze rate at upper (${freezeAtUpperLimit}) or lower (${freezeAtLowerLimit}).` - ) - ); - - // then ensure it gets set to the same frozen status and frozen rate - // as the old exchange rates - await setInversePricing({ - freezeAtUpperLimit, - freezeAtLowerLimit, - }); - } else { - console.log( - gray( - `Detected an existing inverted synth for ${currencyKey} with identical parameters and no new ExchangeRates. Skipping check of frozen status.` - ) - ); - } - } else if (Number(currentRateForCurrency) === 0) { - console.log(gray(`Detected a new inverted synth for ${currencyKey}. Proceeding to add.`)); - // Then a new inverted synth is being added (as there's no previous rate for it) - await setInversePricing({ freezeAtUpperLimit: false, freezeAtLowerLimit: false }); - } else if (Number(totalSynthSupply) === 0) { - console.log( - gray( - `Inverted synth at ${currencyKey} has 0 total supply and its inverted parameters have changed. ` + - `Proceeding to reconfigure its parameters as instructed, unfreezing it if currently frozen.` - ) - ); - // Then a new inverted synth is being added (as there's no existing supply) - await setInversePricing({ freezeAtUpperLimit: false, freezeAtLowerLimit: false }); - } else if (network !== 'mainnet' && forceUpdateInverseSynthsOnTestnet) { - // as we are on testnet and the flag is enabled, allow a mutative pricing change - console.log( - redBright( - `⚠⚠⚠ WARNING: The parameters for the inverted synth ${currencyKey} ` + - `have changed and it has non-zero totalSupply. This is allowed only on testnets` - ) - ); - await setInversePricing({ freezeAtUpperLimit: false, freezeAtLowerLimit: false }); - } else { - // Then an existing synth's inverted parameters have changed. - // For safety sake, let's inform the user and skip this step - console.log( - redBright( - `⚠⚠⚠ WARNING: The parameters for the inverted synth ${currencyKey} ` + - `have changed and it has non-zero totalSupply. This use-case is not supported by the deploy script. ` + - `This should be done as a purge() and setInversePricing() separately` - ) - ); - } - } else { - // When no exrates, then totally fresh deploy (local deployment) - await setInversePricing({ freezeAtUpperLimit: false, freezeAtLowerLimit: false }); - } - } - } - - // then ensure the defaults of SystemSetting - // are set (requires FlexibleStorage to have been correctly configured) - if (systemSettings) { - console.log(gray(`\n------ CONFIGURE SYSTEM SETTINGS ------\n`)); - - // Now ensure all the fee rates are set for various synths (this must be done after the AddressResolver - // has populated all references). - // Note: this populates rates for new synths regardless of the addNewSynths flag - const synthRates = await Promise.all( - synths.map(({ name }) => systemSettings.methods.exchangeFeeRate(toBytes32(name)).call()) - ); - - const exchangeFeeRates = await getDeployParameter('EXCHANGE_FEE_RATES'); - - // override individual currencyKey / synths exchange rates - const synthExchangeRateOverride = { - sETH: parseUnits('0.0025').toString(), - iETH: parseUnits('0.004').toString(), - sBTC: parseUnits('0.003').toString(), - iBTC: parseUnits('0.003').toString(), - iBNB: parseUnits('0.021').toString(), - sXTZ: parseUnits('0.0085').toString(), - iXTZ: parseUnits('0.0085').toString(), - sEOS: parseUnits('0.0085').toString(), - iEOS: parseUnits('0.009').toString(), - sETC: parseUnits('0.0085').toString(), - sLINK: parseUnits('0.0085').toString(), - sDASH: parseUnits('0.009').toString(), - iDASH: parseUnits('0.009').toString(), - sXRP: parseUnits('0.009').toString(), - }; - - const synthsRatesToUpdate = synths - .map((synth, i) => - Object.assign( - { - currentRate: parseUnits(synthRates[i] || '0').toString(), - targetRate: - synth.name in synthExchangeRateOverride - ? synthExchangeRateOverride[synth.name] - : exchangeFeeRates[synth.category], - }, - synth - ) - ) - .filter(({ currentRate }) => currentRate === '0'); - - console.log(gray(`Found ${synthsRatesToUpdate.length} synths needs exchange rate pricing`)); - - if (synthsRatesToUpdate.length) { - console.log( - gray( - 'Setting the following:', - synthsRatesToUpdate - .map( - ({ name, targetRate, currentRate }) => - `\t${name} from ${currentRate * 100}% to ${formatUnits(targetRate) * 100}%` - ) - .join('\n') - ) - ); - - await runStep({ - gasLimit: Math.max(methodCallGasLimit, 150e3 * synthsRatesToUpdate.length), // higher gas required, 150k per synth is sufficient (in OVM) - contract: 'SystemSettings', - target: systemSettings, - write: 'setExchangeFeeRateForSynths', - writeArg: [ - synthsRatesToUpdate.map(({ name }) => toBytes32(name)), - synthsRatesToUpdate.map(({ targetRate }) => targetRate), - ], - }); - } - - // setup initial values if they are unset - - const waitingPeriodSecs = await getDeployParameter('WAITING_PERIOD_SECS'); - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'waitingPeriodSecs', - expected: input => (waitingPeriodSecs === '0' ? true : input !== '0'), - write: 'setWaitingPeriodSecs', - writeArg: waitingPeriodSecs, - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'priceDeviationThresholdFactor', - expected: input => input !== '0', // only change if zero - write: 'setPriceDeviationThresholdFactor', - writeArg: await getDeployParameter('PRICE_DEVIATION_THRESHOLD_FACTOR'), - }); - - const tradingRewardsEnabled = await getDeployParameter('TRADING_REWARDS_ENABLED'); - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'tradingRewardsEnabled', - expected: input => input === tradingRewardsEnabled, // only change if non-default - write: 'setTradingRewardsEnabled', - writeArg: tradingRewardsEnabled, - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'issuanceRatio', - expected: input => input !== '0', // only change if zero - write: 'setIssuanceRatio', - writeArg: await getDeployParameter('ISSUANCE_RATIO'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'feePeriodDuration', - expected: input => input !== '0', // only change if zero - write: 'setFeePeriodDuration', - writeArg: await getDeployParameter('FEE_PERIOD_DURATION'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'targetThreshold', - expected: input => input !== '0', // only change if zero - write: 'setTargetThreshold', - writeArg: await getDeployParameter('TARGET_THRESHOLD'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'liquidationDelay', - expected: input => input !== '0', // only change if zero - write: 'setLiquidationDelay', - writeArg: await getDeployParameter('LIQUIDATION_DELAY'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'liquidationRatio', - expected: input => input !== '0', // only change if zero - write: 'setLiquidationRatio', - writeArg: await getDeployParameter('LIQUIDATION_RATIO'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'liquidationPenalty', - expected: input => input !== '0', // only change if zero - write: 'setLiquidationPenalty', - writeArg: await getDeployParameter('LIQUIDATION_PENALTY'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'rateStalePeriod', - expected: input => input !== '0', // only change if zero - write: 'setRateStalePeriod', - writeArg: await getDeployParameter('RATE_STALE_PERIOD'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'minimumStakeTime', - expected: input => input !== '0', // only change if zero - write: 'setMinimumStakeTime', - writeArg: await getDeployParameter('MINIMUM_STAKE_TIME'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'debtSnapshotStaleTime', - expected: input => input !== '0', // only change if zero - write: 'setDebtSnapshotStaleTime', - writeArg: await getDeployParameter('DEBT_SNAPSHOT_STALE_TIME'), - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'crossDomainMessageGasLimit', - readArg: 0, - expected: input => input !== '0', // only change if zero - write: 'setCrossDomainMessageGasLimit', - writeArg: [0, await getDeployParameter('CROSS_DOMAIN_DEPOSIT_GAS_LIMIT')], - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'crossDomainMessageGasLimit', - readArg: 1, - expected: input => input !== '0', // only change if zero - write: 'setCrossDomainMessageGasLimit', - writeArg: [1, await getDeployParameter('CROSS_DOMAIN_ESCROW_GAS_LIMIT')], - }); - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'crossDomainMessageGasLimit', - readArg: 2, - expected: input => input !== '0', // only change if zero - write: 'setCrossDomainMessageGasLimit', - writeArg: [2, await getDeployParameter('CROSS_DOMAIN_REWARD_GAS_LIMIT')], - }); - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'crossDomainMessageGasLimit', - readArg: 3, - expected: input => input !== '0', // only change if zero - write: 'setCrossDomainMessageGasLimit', - writeArg: [3, await getDeployParameter('CROSS_DOMAIN_WITHDRAWAL_GAS_LIMIT')], - }); - - const aggregatorWarningFlags = (await getDeployParameter('AGGREGATOR_WARNING_FLAGS'))[network]; - // If deploying to OVM avoid ivoking setAggregatorWarningFlags for now. - if (aggregatorWarningFlags && !useOvm) { - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'aggregatorWarningFlags', - expected: input => input !== ZERO_ADDRESS, // only change if zero - write: 'setAggregatorWarningFlags', - writeArg: aggregatorWarningFlags, - }); - } - - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'etherWrapperMaxETH', - expected: input => input !== '0', // only change if zero - write: 'setEtherWrapperMaxETH', - writeArg: await getDeployParameter('ETHER_WRAPPER_MAX_ETH'), - }); - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'etherWrapperMintFeeRate', - expected: input => input !== '0', // only change if zero - write: 'setEtherWrapperMintFeeRate', - writeArg: await getDeployParameter('ETHER_WRAPPER_MINT_FEE_RATE'), - }); - await runStep({ - contract: 'SystemSettings', - target: systemSettings, - read: 'etherWrapperBurnFeeRate', - expected: input => input !== '0', // only change if zero - write: 'setEtherWrapperBurnFeeRate', - writeArg: await getDeployParameter('ETHER_WRAPPER_BURN_FEE_RATE'), - }); - } - - if (!useOvm) { - console.log(gray(`\n------ INITIALISING MULTI COLLATERAL ------\n`)); - const collateralsArg = [collateralEth, collateralErc20, collateralShort].map(addressOf); - await runStep({ - contract: 'CollateralManager', - target: collateralManager, - read: 'hasAllCollaterals', - readArg: [collateralsArg], - expected: input => input, - write: 'addCollaterals', - writeArg: [collateralsArg], - }); - - await runStep({ - contract: 'CollateralEth', - target: collateralEth, - read: 'manager', - expected: input => input === addressOf(collateralManager), - write: 'setManager', - writeArg: addressOf(collateralManager), - }); - - const collateralEthSynths = (await getDeployParameter('COLLATERAL_ETH'))['SYNTHS']; // COLLATERAL_ETH synths - ['sUSD', 'sETH'] - await runStep({ - contract: 'CollateralEth', - gasLimit: 1e6, - target: collateralEth, - read: 'areSynthsAndCurrenciesSet', - readArg: [ - collateralEthSynths.map(key => toBytes32(`Synth${key}`)), - collateralEthSynths.map(toBytes32), - ], - expected: input => input, - write: 'addSynths', - writeArg: [ - collateralEthSynths.map(key => toBytes32(`Synth${key}`)), - collateralEthSynths.map(toBytes32), - ], - }); - - await runStep({ - contract: 'CollateralErc20', - target: collateralErc20, - read: 'manager', - expected: input => input === addressOf(collateralManager), - write: 'setManager', - writeArg: addressOf(collateralManager), - }); - - const collateralErc20Synths = (await getDeployParameter('COLLATERAL_RENBTC'))['SYNTHS']; // COLLATERAL_RENBTC synths - ['sUSD', 'sBTC'] - await runStep({ - contract: 'CollateralErc20', - gasLimit: 1e6, - target: collateralErc20, - read: 'areSynthsAndCurrenciesSet', - readArg: [ - collateralErc20Synths.map(key => toBytes32(`Synth${key}`)), - collateralErc20Synths.map(toBytes32), - ], - expected: input => input, - write: 'addSynths', - writeArg: [ - collateralErc20Synths.map(key => toBytes32(`Synth${key}`)), - collateralErc20Synths.map(toBytes32), - ], - }); - - await runStep({ - contract: 'CollateralShort', - target: collateralShort, - read: 'manager', - expected: input => input === addressOf(collateralManager), - write: 'setManager', - writeArg: addressOf(collateralManager), - }); - - const collateralShortSynths = (await getDeployParameter('COLLATERAL_SHORT'))['SYNTHS']; // COLLATERAL_SHORT synths - ['sBTC', 'sETH'] - await runStep({ - contract: 'CollateralShort', - gasLimit: 1e6, - target: collateralShort, - read: 'areSynthsAndCurrenciesSet', - readArg: [ - collateralShortSynths.map(key => toBytes32(`Synth${key}`)), - collateralShortSynths.map(toBytes32), - ], - expected: input => input, - write: 'addSynths', - writeArg: [ - collateralShortSynths.map(key => toBytes32(`Synth${key}`)), - collateralShortSynths.map(toBytes32), - ], - }); - - await runStep({ - contract: 'CollateralManager', - target: collateralManager, - read: 'maxDebt', - expected: input => input !== '0', // only change if zero - write: 'setMaxDebt', - writeArg: [collateralManagerDefaults['MAX_DEBT']], - }); - - await runStep({ - contract: 'CollateralManager', - target: collateralManager, - read: 'baseBorrowRate', - expected: input => input !== '0', // only change if zero - write: 'setBaseBorrowRate', - writeArg: [collateralManagerDefaults['BASE_BORROW_RATE']], - }); - - await runStep({ - contract: 'CollateralManager', - target: collateralManager, - read: 'baseShortRate', - expected: input => input !== '0', // only change if zero - write: 'setBaseShortRate', - writeArg: [collateralManagerDefaults['BASE_SHORT_RATE']], - }); - - // add to the manager. - const collateralManagerSynths = collateralManagerDefaults['SYNTHS']; - await runStep({ - gasLimit: 1e6, - contract: 'CollateralManager', - target: collateralManager, - read: 'areSynthsAndCurrenciesSet', - readArg: [ - collateralManagerSynths.map(key => toBytes32(`Synth${key}`)), - collateralManagerSynths.map(toBytes32), - ], - expected: input => input, - write: 'addSynths', - writeArg: [ - collateralManagerSynths.map(key => toBytes32(`Synth${key}`)), - collateralManagerSynths.map(toBytes32), - ], - }); - - const collateralManagerShorts = collateralManagerDefaults['SHORTS']; - await runStep({ - gasLimit: 1e6, - contract: 'CollateralManager', - target: collateralManager, - read: 'areShortableSynthsSet', - readArg: [ - collateralManagerShorts.map(({ long }) => toBytes32(`Synth${long}`)), - collateralManagerShorts.map(({ long }) => toBytes32(long)), - ], - expected: input => input, - write: 'addShortableSynths', - writeArg: [ - collateralManagerShorts.map(({ long, short }) => - [`Synth${long}`, `Synth${short}`].map(toBytes32) - ), - collateralManagerShorts.map(({ long }) => toBytes32(long)), - ], - }); - - const collateralShortInteractionDelay = (await getDeployParameter('COLLATERAL_SHORT'))[ - 'INTERACTION_DELAY' - ]; - - await runStep({ - contract: 'CollateralShort', - target: collateralShort, - read: 'interactionDelay', - expected: input => input !== '0', // only change if zero - write: 'setInteractionDelay', - writeArg: collateralShortInteractionDelay, - }); - - await runStep({ - contract: 'CollateralEth', - target: collateralEth, - read: 'issueFeeRate', - expected: input => input !== '0', // only change if zero - write: 'setIssueFeeRate', - writeArg: (await getDeployParameter('COLLATERAL_ETH'))['ISSUE_FEE_RATE'], - }); - - await runStep({ - contract: 'CollateralErc20', - target: collateralErc20, - read: 'issueFeeRate', - expected: input => input !== '0', // only change if zero - write: 'setIssueFeeRate', - writeArg: (await getDeployParameter('COLLATERAL_RENBTC'))['ISSUE_FEE_RATE'], - }); - - await runStep({ - contract: 'CollateralShort', - target: collateralShort, - read: 'issueFeeRate', - expected: input => input !== '0', // only change if zero - write: 'setIssueFeeRate', - writeArg: (await getDeployParameter('COLLATERAL_SHORT'))['ISSUE_FEE_RATE'], - }); - } - - console.log(gray(`\n------ CHECKING DEBT CACHE ------\n`)); - - const refreshSnapshotIfPossible = async (wasInvalid, isInvalid, force = false) => { - const validityChanged = wasInvalid !== isInvalid; - - if (force || validityChanged) { - console.log(yellow(`Refreshing debt snapshot...`)); - await runStep({ - gasLimit: useOvm ? 4.0e6 : 5.0e6, // About 3.34 million gas is required to refresh the snapshot with ~40 synths on L1 - contract: 'DebtCache', - target: debtCache, - write: 'takeDebtSnapshot', - writeArg: [], - publiclyCallable: true, // does not require owner - }); - } else if (!validityChanged) { - console.log( - red('⚠⚠⚠ WARNING: Deployer attempted to refresh the debt cache, but it cannot be.') - ); - } - }; - - const checkSnapshot = async () => { - const [cacheInfo, currentDebt] = await Promise.all([ - debtCache.methods.cacheInfo().call(), - debtCache.methods.currentDebt().call(), - ]); - - // Check if the snapshot is stale and can be fixed. - if (cacheInfo.isStale && !currentDebt.anyRateIsInvalid) { - console.log(yellow('Debt snapshot is stale, and can be refreshed.')); - await refreshSnapshotIfPossible( - cacheInfo.isInvalid, - currentDebt.anyRateIsInvalid, - cacheInfo.isStale - ); - return true; - } - - // Otherwise, if the rates are currently valid, - // we might still need to take a snapshot due to invalidity or deviation. - if (!currentDebt.anyRateIsInvalid) { - if (cacheInfo.isInvalid) { - console.log(yellow('Debt snapshot is invalid, and can be refreshed.')); - await refreshSnapshotIfPossible( - cacheInfo.isInvalid, - currentDebt.anyRateIsInvalid, - cacheInfo.isStale - ); - return true; - } else { - const cachedDebtEther = formatUnits(cacheInfo.debt); - const currentDebtEther = formatUnits(currentDebt.debt); - const deviation = - (Number(currentDebtEther) - Number(cachedDebtEther)) / Number(cachedDebtEther); - const maxDeviation = DEFAULTS.debtSnapshotMaxDeviation; - - if (maxDeviation <= Math.abs(deviation)) { - console.log( - yellow( - `Debt cache deviation is ${deviation * 100}% >= ${maxDeviation * - 100}%; refreshing it...` - ) - ); - await refreshSnapshotIfPossible(cacheInfo.isInvalid, currentDebt.anyRateIsInvalid, true); - return true; - } - } - } - - // Finally, if the debt cache is currently valid, but needs to be invalidated, we will also perform a snapshot. - if (!cacheInfo.isInvalid && currentDebt.anyRateIsInvalid) { - console.log(yellow('Debt snapshot needs to be invalidated.')); - await refreshSnapshotIfPossible(cacheInfo.isInvalid, currentDebt.anyRateIsInvalid, false); - return true; - } - return false; - }; - - const performedSnapshot = await checkSnapshot(); - - if (performedSnapshot) { - console.log(gray('Snapshot complete.')); - } else { - console.log(gray('No snapshot required.')); - } - - console.log(gray(`\n------ DEPLOY COMPLETE ------\n`)); - - reportDeployedContracts({ deployer }); -}; - -module.exports = { - deploy, - DEFAULTS, - cmd: program => - program - .command('deploy') - .description('Deploy compiled solidity files') - .option( - '-a, --add-new-synths', - `Whether or not any new synths in the ${SYNTHS_FILENAME} file should be deployed if there is no entry in the config file` - ) - .option( - '-b, --build-path [value]', - 'Path to a folder hosting compiled files from the "build" step in this script', - DEFAULTS.buildPath - ) - .option( - '-c, --contract-deployment-gas-limit ', - 'Contract deployment gas limit', - parseFloat, - DEFAULTS.contractDeploymentGasLimit - ) - .option( - '-d, --deployment-path ', - `Path to a folder that has your input configuration file ${CONFIG_FILENAME}, the synth list ${SYNTHS_FILENAME} and where your ${DEPLOYMENT_FILENAME} files will go` - ) - .option( - '-e, --concurrency ', - 'Number of parallel calls that can be made to a provider', - 10 - ) - .option( - '-f, --fee-auth ', - 'The address of the fee authority for this network (default is to use existing)' - ) - .option('-g, --gas-price ', 'Gas price in GWEI', DEFAULTS.gasPrice) - .option( - '-h, --fresh-deploy', - 'Perform a "fresh" deploy, i.e. the first deployment on a network.' - ) - .option( - '-i, --ignore-safety-checks', - 'Ignores some validations regarding paths, compiler versions, etc.', - false - ) - .option( - '--ignore-custom-parameters', - 'Ignores deployment parameters specified in params.json', - false - ) - .option( - '-k, --use-fork', - 'Perform the deployment on a forked chain running on localhost (see fork command).', - false - ) - .option( - '-l, --oracle-gas-limit ', - 'The address of the gas limit oracle for this network (default is use existing)' - ) - .option( - '-m, --method-call-gas-limit ', - 'Method call gas limit', - parseFloat, - DEFAULTS.methodCallGasLimit - ) - .option( - '-n, --network ', - 'The network to run off.', - x => x.toLowerCase(), - DEFAULTS.network - ) - .option( - '-o, --oracle-exrates ', - 'The address of the oracle for this network (default is use existing)' - ) - .option( - '-q, --manage-nonces', - 'The command makes sure that no repeated nonces are sent (which may be the case when reorgs are common, i.e. in Goerli. Not to be confused with --manage-nonsense.)', - false - ) - .option( - '-p, --provider-url ', - 'Ethereum network provider URL. If default, will use PROVIDER_URL found in the .env file.' - ) - .option( - '-r, --dry-run', - 'If enabled, will not run any transactions but merely report on them.' - ) - .option( - '-v, --private-key [value]', - 'The private key to deploy with (only works in local mode, otherwise set in .env).' - ) - .option( - '-u, --force-update-inverse-synths-on-testnet', - 'Allow inverse synth pricing to be updated on testnet regardless of total supply' - ) - .option( - '-x, --specify-contracts ', - 'Ignore config.json and specify contracts to be deployed (Comma separated list)' - ) - .option('-y, --yes', 'Dont prompt, just reply yes.') - .option('-z, --use-ovm', 'Target deployment for the OVM (Optimism).') - .action(async (...args) => { - try { - await deploy(...args); - } catch (err) { - // show pretty errors for CLI users - console.error(red(err)); - console.log(err.stack); - process.exitCode = 1; - } - }), -}; diff --git a/publish/src/commands/deploy/add-synths-to-protocol.js b/publish/src/commands/deploy/add-synths-to-protocol.js new file mode 100644 index 0000000000..2ff673b576 --- /dev/null +++ b/publish/src/commands/deploy/add-synths-to-protocol.js @@ -0,0 +1,42 @@ +'use strict'; + +const { gray } = require('chalk'); + +module.exports = async ({ addressOf, deployer, runStep, synthsToAdd }) => { + console.log(gray(`\n------ ADD SYNTHS TO ISSUER ------\n`)); + + const { Issuer } = deployer.deployedContracts; + + // Set up the connection to the Issuer for each Synth (requires FlexibleStorage to have been configured) + + // First filter out all those synths which are already properly imported + console.log(gray('Filtering synths to add to the issuer.')); + const filteredSynths = []; + for (const synth of synthsToAdd) { + const issuerSynthAddress = await Issuer.methods.synths(synth.currencyKeyInBytes).call(); + const currentSynthAddress = addressOf(synth.synth); + if (issuerSynthAddress === currentSynthAddress) { + console.log(gray(`${currentSynthAddress} requires no action`)); + } else { + console.log(gray(`${currentSynthAddress} will be added to the issuer.`)); + filteredSynths.push(synth); + } + } + + const synthChunkSize = 15; + for (let i = 0; i < filteredSynths.length; i += synthChunkSize) { + const chunk = filteredSynths.slice(i, i + synthChunkSize); + await runStep({ + contract: 'Issuer', + target: Issuer, + read: 'getSynths', + readArg: [chunk.map(synth => synth.currencyKeyInBytes)], + expected: input => + input.length === chunk.length && + input.every((cur, idx) => cur === addressOf(chunk[idx].synth)), + write: 'addSynths', + writeArg: [chunk.map(synth => addressOf(synth.synth))], + gasLimit: 1e5 * synthChunkSize, + }); + } +}; diff --git a/publish/src/check-aggregator-prices.js b/publish/src/commands/deploy/check-aggregator-prices.js similarity index 96% rename from publish/src/check-aggregator-prices.js rename to publish/src/commands/deploy/check-aggregator-prices.js index 8ce2a5e106..2322e3a6d7 100644 --- a/publish/src/check-aggregator-prices.js +++ b/publish/src/commands/deploy/check-aggregator-prices.js @@ -4,8 +4,8 @@ const Web3 = require('web3'); const axios = require('axios'); const { gray, yellow, red, cyan } = require('chalk'); -const { loadConnections } = require('./util'); -const { toBytes32 } = require('../../.'); +const { loadConnections } = require('../../util'); +const { toBytes32 } = require('../../../..'); module.exports = async ({ network, useOvm, providerUrl, synths, oldExrates, standaloneFeeds }) => { const output = []; diff --git a/publish/src/commands/deploy/configure-inverse-synths.js b/publish/src/commands/deploy/configure-inverse-synths.js new file mode 100644 index 0000000000..41ad783be4 --- /dev/null +++ b/publish/src/commands/deploy/configure-inverse-synths.js @@ -0,0 +1,156 @@ +'use strict'; + +const { gray, redBright } = require('chalk'); +const { + utils: { parseUnits, formatUnits }, +} = require('ethers'); +const { toBytes32 } = require('../../../..'); + +module.exports = async ({ + addressOf, + deployer, + forceUpdateInverseSynthsOnTestnet, + network, + oldExrates, + runStep, + synths, +}) => { + console.log(gray(`\n------ CONFIGURE INVERSE SYNTHS ------\n`)); + + const { ExchangeRates } = deployer.deployedContracts; + + for (const { name: currencyKey, inverted } of synths) { + if (inverted) { + const { entryPoint, upperLimit, lowerLimit } = inverted; + + // helper function + const setInversePricing = ({ freezeAtUpperLimit, freezeAtLowerLimit }) => + runStep({ + contract: 'ExchangeRates', + target: ExchangeRates, + write: 'setInversePricing', + writeArg: [ + toBytes32(currencyKey), + parseUnits(entryPoint.toString()).toString(), + parseUnits(upperLimit.toString()).toString(), + parseUnits(lowerLimit.toString()).toString(), + freezeAtUpperLimit, + freezeAtLowerLimit, + ], + }); + + // when the oldExrates exists - meaning there is a valid ExchangeRates in the existing deployment.json + // for this environment (true for all environments except the initial deploy in 'local' during those tests) + if (oldExrates) { + // get inverse synth's params from the old exrates, if any exist + const oldInversePricing = await oldExrates.methods + .inversePricing(toBytes32(currencyKey)) + .call(); + + const { + entryPoint: oldEntryPoint, + upperLimit: oldUpperLimit, + lowerLimit: oldLowerLimit, + frozenAtUpperLimit: currentRateIsFrozenUpper, + frozenAtLowerLimit: currentRateIsFrozenLower, + } = oldInversePricing; + + const currentRateIsFrozen = currentRateIsFrozenUpper || currentRateIsFrozenLower; + // and the last rate if any exists + const currentRateForCurrency = await oldExrates.methods + .rateForCurrency(toBytes32(currencyKey)) + .call(); + + // and total supply, if any + const synth = deployer.deployedContracts[`Synth${currencyKey}`]; + const totalSynthSupply = await synth.methods.totalSupply().call(); + console.log(gray(`totalSupply of ${currencyKey}: ${Number(totalSynthSupply)}`)); + + const inversePricingOnCurrentExRates = await ExchangeRates.methods + .inversePricing(toBytes32(currencyKey)) + .call(); + + // ensure that if it's a newer exchange rates deployed, then skip reinserting the inverse pricing if + // already done + if ( + oldExrates.options.address !== ExchangeRates.options.address && + JSON.stringify(inversePricingOnCurrentExRates) === JSON.stringify(oldInversePricing) && + +formatUnits(inversePricingOnCurrentExRates.entryPoint) === entryPoint && + +formatUnits(inversePricingOnCurrentExRates.upperLimit) === upperLimit && + +formatUnits(inversePricingOnCurrentExRates.lowerLimit) === lowerLimit + ) { + console.log( + gray( + `Current ExchangeRates.inversePricing(${currencyKey}) is the same as the previous. Nothing to do.` + ) + ); + } + // When there's an inverted synth with matching parameters + else if ( + entryPoint === +formatUnits(oldEntryPoint) && + upperLimit === +formatUnits(oldUpperLimit) && + lowerLimit === +formatUnits(oldLowerLimit) + ) { + if (oldExrates.options.address !== addressOf(ExchangeRates)) { + const freezeAtUpperLimit = +formatUnits(currentRateForCurrency) === upperLimit; + const freezeAtLowerLimit = +formatUnits(currentRateForCurrency) === lowerLimit; + console.log( + gray( + `Detected an existing inverted synth for ${currencyKey} with identical parameters and a newer ExchangeRates. ` + + `Persisting its frozen status (${currentRateIsFrozen}) and if frozen, then freeze rate at upper (${freezeAtUpperLimit}) or lower (${freezeAtLowerLimit}).` + ) + ); + + // then ensure it gets set to the same frozen status and frozen rate + // as the old exchange rates + await setInversePricing({ + freezeAtUpperLimit, + freezeAtLowerLimit, + }); + } else { + console.log( + gray( + `Detected an existing inverted synth for ${currencyKey} with identical parameters and no new ExchangeRates. Skipping check of frozen status.` + ) + ); + } + } else if (Number(currentRateForCurrency) === 0) { + console.log(gray(`Detected a new inverted synth for ${currencyKey}. Proceeding to add.`)); + // Then a new inverted synth is being added (as there's no previous rate for it) + await setInversePricing({ freezeAtUpperLimit: false, freezeAtLowerLimit: false }); + } else if (Number(totalSynthSupply) === 0) { + console.log( + gray( + `Inverted synth at ${currencyKey} has 0 total supply and its inverted parameters have changed. ` + + `Proceeding to reconfigure its parameters as instructed, unfreezing it if currently frozen.` + ) + ); + // Then a new inverted synth is being added (as there's no existing supply) + await setInversePricing({ freezeAtUpperLimit: false, freezeAtLowerLimit: false }); + } else if (network !== 'mainnet' && forceUpdateInverseSynthsOnTestnet) { + // as we are on testnet and the flag is enabled, allow a mutative pricing change + console.log( + redBright( + `⚠⚠⚠ WARNING: The parameters for the inverted synth ${currencyKey} ` + + `have changed and it has non-zero totalSupply. This is allowed only on testnets` + ) + ); + await setInversePricing({ freezeAtUpperLimit: false, freezeAtLowerLimit: false }); + } else { + // Then an existing synth's inverted parameters have changed. + // For safety sake, let's inform the user and skip this step + console.log( + redBright( + `⚠⚠⚠ WARNING: The parameters for the inverted synth ${currencyKey} ` + + `have changed and it has non-zero totalSupply. This use-case is not supported by the deploy script. ` + + `This should be done as a purge() and setInversePricing() separately` + ) + ); + } + } else { + // When no exrates, then totally fresh deploy (local deployment) + await setInversePricing({ freezeAtUpperLimit: false, freezeAtLowerLimit: false }); + } + } + } +}; diff --git a/publish/src/commands/deploy/configure-legacy-settings.js b/publish/src/commands/deploy/configure-legacy-settings.js new file mode 100644 index 0000000000..0679fc6b4f --- /dev/null +++ b/publish/src/commands/deploy/configure-legacy-settings.js @@ -0,0 +1,315 @@ +'use strict'; + +const { gray } = require('chalk'); +const { toBytes32 } = require('../../../..'); + +module.exports = async ({ + account, + addressOf, + config, + deployer, + getDeployParameter, + network, + runStep, + useOvm, +}) => { + console.log(gray(`\n------ CONFIGURE LEGACY CONTRACTS VIA SETTERS ------\n`)); + + const { + DelegateApprovals, + DelegateApprovalsEternalStorage, + EternalStorageLiquidations, + Exchanger, + ExchangeState, + FeePool, + FeePoolEternalStorage, + FeePoolState, + Issuer, + Liquidations, + ProxyERC20, + ProxyFeePool, + ProxySynthetix, + RewardEscrow, + RewardEscrowV2, + RewardsDistribution, + SupplySchedule, + Synthetix, + SynthetixEscrow, + SynthetixState, + SystemStatus, + TokenStateSynthetix, + } = deployer.deployedContracts; + + // now configure everything + if (network !== 'mainnet' && SystemStatus) { + // On testnet, give the deployer the rights to update status + await runStep({ + contract: 'SystemStatus', + target: SystemStatus, + read: 'accessControl', + readArg: [toBytes32('System'), account], + expected: ({ canSuspend } = {}) => canSuspend, + write: 'updateAccessControls', + writeArg: [ + ['System', 'Issuance', 'Exchange', 'SynthExchange', 'Synth'].map(toBytes32), + [account, account, account, account, account], + [true, true, true, true, true], + [true, true, true, true, true], + ], + }); + } + if (DelegateApprovals && DelegateApprovalsEternalStorage) { + await runStep({ + contract: 'EternalStorage', + target: DelegateApprovalsEternalStorage, + read: 'associatedContract', + expected: input => input === addressOf(DelegateApprovals), + write: 'setAssociatedContract', + writeArg: addressOf(DelegateApprovals), + }); + } + + if (Liquidations && EternalStorageLiquidations) { + await runStep({ + contract: 'EternalStorageLiquidations', + target: EternalStorageLiquidations, + read: 'associatedContract', + expected: input => input === addressOf(Liquidations), + write: 'setAssociatedContract', + writeArg: addressOf(Liquidations), + }); + } + + if (ProxyFeePool && FeePool) { + await runStep({ + contract: 'ProxyFeePool', + target: ProxyFeePool, + read: 'target', + expected: input => input === addressOf(FeePool), + write: 'setTarget', + writeArg: addressOf(FeePool), + }); + } + + if (FeePoolEternalStorage && FeePool) { + await runStep({ + contract: 'FeePoolEternalStorage', + target: FeePoolEternalStorage, + read: 'associatedContract', + expected: input => input === addressOf(FeePool), + write: 'setAssociatedContract', + writeArg: addressOf(FeePool), + }); + } + + if (FeePool && FeePoolState) { + // Rewire FeePoolState if there is a FeePool upgrade + await runStep({ + contract: 'FeePoolState', + target: FeePoolState, + read: 'feePool', + expected: input => input === addressOf(FeePool), + write: 'setFeePool', + writeArg: addressOf(FeePool), + }); + } + + if (Synthetix && ProxyERC20) { + await runStep({ + contract: 'ProxyERC20', + target: ProxyERC20, + read: 'target', + expected: input => input === addressOf(Synthetix), + write: 'setTarget', + writeArg: addressOf(Synthetix), + }); + await runStep({ + contract: 'Synthetix', + target: Synthetix, + read: 'proxy', + expected: input => input === addressOf(ProxyERC20), + write: 'setProxy', + writeArg: addressOf(ProxyERC20), + }); + } + + if (ProxySynthetix && Synthetix) { + await runStep({ + contract: 'ProxySynthetix', + target: ProxySynthetix, + read: 'target', + expected: input => input === addressOf(Synthetix), + write: 'setTarget', + writeArg: addressOf(Synthetix), + }); + } + + if (Exchanger && ExchangeState) { + // The ExchangeState contract has Exchanger as it's associated contract + await runStep({ + contract: 'ExchangeState', + target: ExchangeState, + read: 'associatedContract', + expected: input => input === Exchanger.options.address, + write: 'setAssociatedContract', + writeArg: Exchanger.options.address, + }); + } + + if (Exchanger && SystemStatus) { + // SIP-65: ensure Exchanger can suspend synths if price spikes occur + await runStep({ + contract: 'SystemStatus', + target: SystemStatus, + read: 'accessControl', + readArg: [toBytes32('Synth'), addressOf(Exchanger)], + expected: ({ canSuspend } = {}) => canSuspend, + write: 'updateAccessControl', + writeArg: [toBytes32('Synth'), addressOf(Exchanger), true, false], + }); + } + + // only reset token state if redeploying + if (TokenStateSynthetix && config['TokenStateSynthetix'].deploy) { + const initialIssuance = await getDeployParameter('INITIAL_ISSUANCE'); + await runStep({ + contract: 'TokenStateSynthetix', + target: TokenStateSynthetix, + read: 'balanceOf', + readArg: account, + expected: input => input === initialIssuance, + write: 'setBalanceOf', + writeArg: [account, initialIssuance], + }); + } + + if (TokenStateSynthetix && Synthetix) { + await runStep({ + contract: 'TokenStateSynthetix', + target: TokenStateSynthetix, + read: 'associatedContract', + expected: input => input === addressOf(Synthetix), + write: 'setAssociatedContract', + writeArg: addressOf(Synthetix), + }); + } + + if (SynthetixState && Issuer) { + const IssuerAddress = addressOf(Issuer); + // The SynthetixState contract has Issuer as it's associated contract (after v2.19 refactor) + await runStep({ + contract: 'SynthetixState', + target: SynthetixState, + read: 'associatedContract', + expected: input => input === IssuerAddress, + write: 'setAssociatedContract', + writeArg: IssuerAddress, + }); + } + + if (useOvm && SynthetixState && FeePool) { + // The SynthetixStateLimitedSetup) contract has FeePool to appendAccountIssuanceRecord + await runStep({ + contract: 'SynthetixState', + target: SynthetixState, + read: 'feePool', + expected: input => input === addressOf(FeePool), + write: 'setFeePool', + writeArg: addressOf(FeePool), + }); + } + + if (RewardEscrow && Synthetix) { + await runStep({ + contract: 'RewardEscrow', + target: RewardEscrow, + read: 'synthetix', + expected: input => input === addressOf(Synthetix), + write: 'setSynthetix', + writeArg: addressOf(Synthetix), + }); + } + + if (RewardEscrow && FeePool) { + await runStep({ + contract: 'RewardEscrow', + target: RewardEscrow, + read: 'feePool', + expected: input => input === addressOf(FeePool), + write: 'setFeePool', + writeArg: addressOf(FeePool), + }); + } + + if (SupplySchedule && Synthetix) { + await runStep({ + contract: 'SupplySchedule', + target: SupplySchedule, + read: 'synthetixProxy', + expected: input => input === addressOf(ProxySynthetix), + write: 'setSynthetixProxy', + writeArg: addressOf(ProxySynthetix), + }); + } + + if (Synthetix && RewardsDistribution) { + await runStep({ + contract: 'RewardsDistribution', + target: RewardsDistribution, + read: 'authority', + expected: input => input === addressOf(Synthetix), + write: 'setAuthority', + writeArg: addressOf(Synthetix), + }); + + await runStep({ + contract: 'RewardsDistribution', + target: RewardsDistribution, + read: 'synthetixProxy', + expected: input => input === addressOf(ProxyERC20), + write: 'setSynthetixProxy', + writeArg: addressOf(ProxyERC20), + }); + } + + // RewardEscrow on RewardsDistribution should be set to new RewardEscrowV2 + if (RewardEscrowV2 && RewardsDistribution) { + await runStep({ + contract: 'RewardsDistribution', + target: RewardsDistribution, + read: 'rewardEscrow', + expected: input => input === addressOf(RewardEscrowV2), + write: 'setRewardEscrow', + writeArg: addressOf(RewardEscrowV2), + }); + } + + // ---------------- + // Setting ProxyERC20 Synthetix for SynthetixEscrow + // ---------------- + + // Skip setting unless redeploying either of these, + if (config['Synthetix'].deploy || config['SynthetixEscrow'].deploy) { + // Note: currently on mainnet SynthetixEscrow.methods.Synthetix() does NOT exist + // it is "havven" and the ABI we have here is not sufficient + if (network === 'mainnet' && !useOvm) { + await runStep({ + contract: 'SynthetixEscrow', + target: SynthetixEscrow, + read: 'havven', + expected: input => input === addressOf(ProxyERC20), + write: 'setHavven', + writeArg: addressOf(ProxyERC20), + }); + } else { + await runStep({ + contract: 'SynthetixEscrow', + target: SynthetixEscrow, + read: 'synthetix', + expected: input => input === addressOf(ProxyERC20), + write: 'setSynthetix', + writeArg: addressOf(ProxyERC20), + }); + } + } +}; diff --git a/publish/src/commands/deploy/configure-loans.js b/publish/src/commands/deploy/configure-loans.js new file mode 100644 index 0000000000..884552f890 --- /dev/null +++ b/publish/src/commands/deploy/configure-loans.js @@ -0,0 +1,275 @@ +'use strict'; + +const { gray } = require('chalk'); +const { toBytes32 } = require('../../../..'); + +module.exports = async ({ + addressOf, + collateralManagerDefaults, + deployer, + getDeployParameter, + runStep, + useEmptyCollateralManager, +}) => { + console.log(gray(`\n------ CONFIGURING MULTI COLLATERAL ------\n`)); + + const { + CollateralErc20, + CollateralEth, + CollateralShort, + CollateralManager, + CollateralManagerState, + CollateralStateErc20, + CollateralStateEth, + CollateralStateShort, + } = deployer.deployedContracts; + + if (CollateralStateShort && CollateralShort) { + await runStep({ + contract: 'CollateralStateShort', + target: CollateralStateShort, + read: 'associatedContract', + expected: input => input === CollateralShort.options.address, + write: 'setAssociatedContract', + writeArg: CollateralShort.options.address, + }); + } + + if (CollateralStateErc20 && CollateralErc20) { + await runStep({ + contract: 'CollateralStateErc20', + target: CollateralStateErc20, + read: 'associatedContract', + expected: input => input === addressOf(CollateralErc20), + write: 'setAssociatedContract', + writeArg: addressOf(CollateralErc20), + }); + } + + if (CollateralStateEth && CollateralEth) { + await runStep({ + contract: 'CollateralStateEth', + target: CollateralStateEth, + read: 'associatedContract', + expected: input => input === addressOf(CollateralEth), + write: 'setAssociatedContract', + writeArg: addressOf(CollateralEth), + }); + } + if (CollateralManagerState && CollateralManager) { + await runStep({ + contract: 'CollateralManagerState', + target: CollateralManagerState, + read: 'associatedContract', + expected: input => input === addressOf(CollateralManager), + write: 'setAssociatedContract', + writeArg: addressOf(CollateralManager), + }); + } + + console.log(gray(`\n------ INITIALISING MULTI COLLATERAL ------\n`)); + + if (CollateralEth && CollateralErc20 && CollateralShort) { + const CollateralsArg = [CollateralEth, CollateralErc20, CollateralShort].map(addressOf); + await runStep({ + contract: 'CollateralManager', + target: CollateralManager, + read: 'hasAllCollaterals', + readArg: [CollateralsArg], + expected: input => input, + write: 'addCollaterals', + writeArg: [CollateralsArg], + }); + } + if (CollateralEth) { + await runStep({ + contract: 'CollateralEth', + target: CollateralEth, + read: 'manager', + expected: input => input === addressOf(CollateralManager), + write: 'setManager', + writeArg: addressOf(CollateralManager), + }); + const CollateralEthSynths = (await getDeployParameter('COLLATERAL_ETH'))['SYNTHS']; // COLLATERAL_ETH synths - ['sUSD', 'sETH'] + await runStep({ + contract: 'CollateralEth', + gasLimit: 1e6, + target: CollateralEth, + read: 'areSynthsAndCurrenciesSet', + readArg: [ + CollateralEthSynths.map(key => toBytes32(`Synth${key}`)), + CollateralEthSynths.map(toBytes32), + ], + expected: input => input, + write: 'addSynths', + writeArg: [ + CollateralEthSynths.map(key => toBytes32(`Synth${key}`)), + CollateralEthSynths.map(toBytes32), + ], + }); + + await runStep({ + contract: 'CollateralEth', + target: CollateralEth, + read: 'issueFeeRate', + expected: input => input !== '0', // only change if zero + write: 'setIssueFeeRate', + writeArg: (await getDeployParameter('COLLATERAL_ETH'))['ISSUE_FEE_RATE'], + }); + } + + if (CollateralErc20) { + await runStep({ + contract: 'CollateralErc20', + target: CollateralErc20, + read: 'manager', + expected: input => input === addressOf(CollateralManager), + write: 'setManager', + writeArg: addressOf(CollateralManager), + }); + const CollateralErc20Synths = (await getDeployParameter('COLLATERAL_RENBTC'))['SYNTHS']; // COLLATERAL_RENBTC synths - ['sUSD', 'sBTC'] + await runStep({ + contract: 'CollateralErc20', + gasLimit: 1e6, + target: CollateralErc20, + read: 'areSynthsAndCurrenciesSet', + readArg: [ + CollateralErc20Synths.map(key => toBytes32(`Synth${key}`)), + CollateralErc20Synths.map(toBytes32), + ], + expected: input => input, + write: 'addSynths', + writeArg: [ + CollateralErc20Synths.map(key => toBytes32(`Synth${key}`)), + CollateralErc20Synths.map(toBytes32), + ], + }); + + await runStep({ + contract: 'CollateralErc20', + target: CollateralErc20, + read: 'issueFeeRate', + expected: input => input !== '0', // only change if zero + write: 'setIssueFeeRate', + writeArg: (await getDeployParameter('COLLATERAL_RENBTC'))['ISSUE_FEE_RATE'], + }); + } + + if (CollateralShort) { + await runStep({ + contract: 'CollateralShort', + target: CollateralShort, + read: 'manager', + expected: input => input === addressOf(CollateralManager), + write: 'setManager', + writeArg: addressOf(CollateralManager), + }); + + const CollateralShortSynths = (await getDeployParameter('COLLATERAL_SHORT'))['SYNTHS']; // COLLATERAL_SHORT synths - ['sBTC', 'sETH'] + await runStep({ + contract: 'CollateralShort', + gasLimit: 1e6, + target: CollateralShort, + read: 'areSynthsAndCurrenciesSet', + readArg: [ + CollateralShortSynths.map(key => toBytes32(`Synth${key}`)), + CollateralShortSynths.map(toBytes32), + ], + expected: input => input, + write: 'addSynths', + writeArg: [ + CollateralShortSynths.map(key => toBytes32(`Synth${key}`)), + CollateralShortSynths.map(toBytes32), + ], + }); + + const CollateralShortInteractionDelay = (await getDeployParameter('COLLATERAL_SHORT'))[ + 'INTERACTION_DELAY' + ]; + + await runStep({ + contract: 'CollateralShort', + target: CollateralShort, + read: 'interactionDelay', + expected: input => input !== '0', // only change if zero + write: 'setInteractionDelay', + writeArg: CollateralShortInteractionDelay, + }); + await runStep({ + contract: 'CollateralShort', + target: CollateralShort, + read: 'issueFeeRate', + expected: input => input !== '0', // only change if zero + write: 'setIssueFeeRate', + writeArg: (await getDeployParameter('COLLATERAL_SHORT'))['ISSUE_FEE_RATE'], + }); + } + + if (!useEmptyCollateralManager) { + await runStep({ + contract: 'CollateralManager', + target: CollateralManager, + read: 'maxDebt', + expected: input => input !== '0', // only change if zero + write: 'setMaxDebt', + writeArg: [collateralManagerDefaults['MAX_DEBT']], + }); + + await runStep({ + contract: 'CollateralManager', + target: CollateralManager, + read: 'baseBorrowRate', + expected: input => input !== '0', // only change if zero + write: 'setBaseBorrowRate', + writeArg: [collateralManagerDefaults['BASE_BORROW_RATE']], + }); + + await runStep({ + contract: 'CollateralManager', + target: CollateralManager, + read: 'baseShortRate', + expected: input => input !== '0', // only change if zero + write: 'setBaseShortRate', + writeArg: [collateralManagerDefaults['BASE_SHORT_RATE']], + }); + + // add to the manager. + const CollateralManagerSynths = collateralManagerDefaults['SYNTHS']; + await runStep({ + gasLimit: 1e6, + contract: 'CollateralManager', + target: CollateralManager, + read: 'areSynthsAndCurrenciesSet', + readArg: [ + CollateralManagerSynths.map(key => toBytes32(`Synth${key}`)), + CollateralManagerSynths.map(toBytes32), + ], + expected: input => input, + write: 'addSynths', + writeArg: [ + CollateralManagerSynths.map(key => toBytes32(`Synth${key}`)), + CollateralManagerSynths.map(toBytes32), + ], + }); + + const CollateralManagerShorts = collateralManagerDefaults['SHORTS']; + await runStep({ + gasLimit: 1e6, + contract: 'CollateralManager', + target: CollateralManager, + read: 'areShortableSynthsSet', + readArg: [ + CollateralManagerShorts.map(({ long }) => toBytes32(`Synth${long}`)), + CollateralManagerShorts.map(({ long }) => toBytes32(long)), + ], + expected: input => input, + write: 'addShortableSynths', + writeArg: [ + CollateralManagerShorts.map(({ long, short }) => + [`Synth${long}`, `Synth${short}`].map(toBytes32) + ), + CollateralManagerShorts.map(({ long }) => toBytes32(long)), + ], + }); + } +}; diff --git a/publish/src/commands/deploy/configure-standalone-price-feeds.js b/publish/src/commands/deploy/configure-standalone-price-feeds.js new file mode 100644 index 0000000000..3dc307c335 --- /dev/null +++ b/publish/src/commands/deploy/configure-standalone-price-feeds.js @@ -0,0 +1,28 @@ +'use strict'; + +const { gray } = require('chalk'); +const { + utils: { isAddress }, +} = require('ethers'); +const { toBytes32 } = require('../../../..'); + +module.exports = async ({ deployer, runStep, standaloneFeeds }) => { + console.log(gray(`\n------ CONFIGURE STANDLONE FEEDS ------\n`)); + + // Setup remaining price feeds (that aren't synths) + const { ExchangeRates } = deployer.deployedContracts; + + for (const { asset, feed } of standaloneFeeds) { + if (isAddress(feed) && ExchangeRates) { + await runStep({ + contract: `ExchangeRates`, + target: ExchangeRates, + read: 'aggregators', + readArg: toBytes32(asset), + expected: input => input === feed, + write: 'addAggregator', + writeArg: [toBytes32(asset), feed], + }); + } + } +}; diff --git a/publish/src/commands/deploy/configure-synths.js b/publish/src/commands/deploy/configure-synths.js new file mode 100644 index 0000000000..1ab2151978 --- /dev/null +++ b/publish/src/commands/deploy/configure-synths.js @@ -0,0 +1,86 @@ +'use strict'; + +const { gray } = require('chalk'); +const { + utils: { isAddress }, +} = require('ethers'); +const { toBytes32 } = require('../../../..'); + +module.exports = async ({ addressOf, synths, feeds, deployer, runStep }) => { + // now configure synths + console.log(gray(`\n------ CONFIGURE SYNTHS ------\n`)); + + const { ExchangeRates } = deployer.deployedContracts; + + for (const { name: currencyKey, asset } of synths) { + console.log(gray(`\n --- SYNTH ${currencyKey} ---\n`)); + + const currencyKeyInBytes = toBytes32(currencyKey); + + const synth = deployer.deployedContracts[`Synth${currencyKey}`]; + const tokenStateForSynth = deployer.deployedContracts[`TokenState${currencyKey}`]; + const proxyForSynth = deployer.deployedContracts[`Proxy${currencyKey}`]; + const proxyERC20ForSynth = + currencyKey === 'sUSD' ? deployer.deployedContracts[`ProxyERC20sUSD`] : undefined; + + if (tokenStateForSynth && synth) { + await runStep({ + contract: `TokenState${currencyKey}`, + target: tokenStateForSynth, + read: 'associatedContract', + expected: input => input === addressOf(synth), + write: 'setAssociatedContract', + writeArg: addressOf(synth), + }); + } + + // Setup proxy for synth + if (proxyForSynth && synth) { + await runStep({ + contract: `Proxy${currencyKey}`, + target: proxyForSynth, + read: 'target', + expected: input => input === addressOf(synth), + write: 'setTarget', + writeArg: addressOf(synth), + }); + + // Migration Phrase 2: if there's a ProxyERC20sUSD then the Synth's proxy must use it + await runStep({ + contract: `Synth${currencyKey}`, + target: synth, + read: 'proxy', + expected: input => input === addressOf(proxyERC20ForSynth || proxyForSynth), + write: 'setProxy', + writeArg: addressOf(proxyERC20ForSynth || proxyForSynth), + }); + + if (proxyERC20ForSynth) { + // and make sure this new proxy has the target of the synth + await runStep({ + contract: `ProxyERC20sUSD`, + target: proxyERC20ForSynth, + read: 'target', + expected: input => input === addressOf(synth), + write: 'setTarget', + writeArg: addressOf(synth), + }); + } + } + + const { feed } = feeds[asset] || {}; + + // now setup price aggregator if any for the synth + if (isAddress(feed) && ExchangeRates) { + await runStep({ + contract: `ExchangeRates`, + target: ExchangeRates, + read: 'aggregators', + readArg: currencyKeyInBytes, + expected: input => input === feed, + write: 'addAggregator', + writeArg: [currencyKeyInBytes, feed], + }); + } + } +}; diff --git a/publish/src/commands/deploy/configure-system-settings.js b/publish/src/commands/deploy/configure-system-settings.js new file mode 100644 index 0000000000..ad0c9a86ad --- /dev/null +++ b/publish/src/commands/deploy/configure-system-settings.js @@ -0,0 +1,286 @@ +'use strict'; + +const { gray } = require('chalk'); +const { + utils: { parseUnits, formatUnits }, +} = require('ethers'); +const { + toBytes32, + constants: { ZERO_ADDRESS }, +} = require('../../../..'); + +module.exports = async ({ + deployer, + methodCallGasLimit, + useOvm, + getDeployParameter, + network, + runStep, + synths, +}) => { + const { SystemSettings } = deployer.deployedContracts; + + // then ensure the defaults of SystemSetting + // are set (requires FlexibleStorage to have been correctly configured) + if (SystemSettings) { + console.log(gray(`\n------ CONFIGURE SYSTEM SETTINGS ------\n`)); + + // Now ensure all the fee rates are set for various synths (this must be done after the AddressResolver + // has populated all references). + // Note: this populates rates for new synths regardless of the addNewSynths flag + const synthRates = await Promise.all( + synths.map(({ name }) => SystemSettings.methods.exchangeFeeRate(toBytes32(name)).call()) + ); + + const exchangeFeeRates = await getDeployParameter('EXCHANGE_FEE_RATES'); + + // override individual currencyKey / synths exchange rates + const synthExchangeRateOverride = { + sETH: parseUnits('0.0025').toString(), + iETH: parseUnits('0.004').toString(), + sBTC: parseUnits('0.003').toString(), + iBTC: parseUnits('0.003').toString(), + iBNB: parseUnits('0.021').toString(), + sXTZ: parseUnits('0.0085').toString(), + iXTZ: parseUnits('0.0085').toString(), + sEOS: parseUnits('0.0085').toString(), + iEOS: parseUnits('0.009').toString(), + sETC: parseUnits('0.0085').toString(), + sLINK: parseUnits('0.0085').toString(), + sDASH: parseUnits('0.009').toString(), + iDASH: parseUnits('0.009').toString(), + sXRP: parseUnits('0.009').toString(), + }; + + const synthsRatesToUpdate = synths + .map((synth, i) => + Object.assign( + { + currentRate: parseUnits(synthRates[i] || '0').toString(), + targetRate: + synth.name in synthExchangeRateOverride + ? synthExchangeRateOverride[synth.name] + : exchangeFeeRates[synth.category], + }, + synth + ) + ) + .filter(({ currentRate }) => currentRate === '0'); + + console.log(gray(`Found ${synthsRatesToUpdate.length} synths needs exchange rate pricing`)); + + if (synthsRatesToUpdate.length) { + console.log( + gray( + 'Setting the following:', + synthsRatesToUpdate + .map( + ({ name, targetRate, currentRate }) => + `\t${name} from ${currentRate * 100}% to ${formatUnits(targetRate) * 100}%` + ) + .join('\n') + ) + ); + + await runStep({ + gasLimit: Math.max(methodCallGasLimit, 150e3 * synthsRatesToUpdate.length), // higher gas required, 150k per synth is sufficient (in OVM) + contract: 'SystemSettings', + target: SystemSettings, + write: 'setExchangeFeeRateForSynths', + writeArg: [ + synthsRatesToUpdate.map(({ name }) => toBytes32(name)), + synthsRatesToUpdate.map(({ targetRate }) => targetRate), + ], + }); + } + + // setup initial values if they are unset + + const waitingPeriodSecs = await getDeployParameter('WAITING_PERIOD_SECS'); + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'waitingPeriodSecs', + expected: input => (waitingPeriodSecs === '0' ? true : input !== '0'), + write: 'setWaitingPeriodSecs', + writeArg: waitingPeriodSecs, + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'priceDeviationThresholdFactor', + expected: input => input !== '0', // only change if zero + write: 'setPriceDeviationThresholdFactor', + writeArg: await getDeployParameter('PRICE_DEVIATION_THRESHOLD_FACTOR'), + }); + + const tradingRewardsEnabled = await getDeployParameter('TRADING_REWARDS_ENABLED'); + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'tradingRewardsEnabled', + expected: input => input === tradingRewardsEnabled, // only change if non-default + write: 'setTradingRewardsEnabled', + writeArg: tradingRewardsEnabled, + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'issuanceRatio', + expected: input => input !== '0', // only change if zero + write: 'setIssuanceRatio', + writeArg: await getDeployParameter('ISSUANCE_RATIO'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'feePeriodDuration', + expected: input => input !== '0', // only change if zero + write: 'setFeePeriodDuration', + writeArg: await getDeployParameter('FEE_PERIOD_DURATION'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'targetThreshold', + expected: input => input !== '0', // only change if zero + write: 'setTargetThreshold', + writeArg: await getDeployParameter('TARGET_THRESHOLD'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'liquidationDelay', + expected: input => input !== '0', // only change if zero + write: 'setLiquidationDelay', + writeArg: await getDeployParameter('LIQUIDATION_DELAY'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'liquidationRatio', + expected: input => input !== '0', // only change if zero + write: 'setLiquidationRatio', + writeArg: await getDeployParameter('LIQUIDATION_RATIO'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'liquidationPenalty', + expected: input => input !== '0', // only change if zero + write: 'setLiquidationPenalty', + writeArg: await getDeployParameter('LIQUIDATION_PENALTY'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'rateStalePeriod', + expected: input => input !== '0', // only change if zero + write: 'setRateStalePeriod', + writeArg: await getDeployParameter('RATE_STALE_PERIOD'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'minimumStakeTime', + expected: input => input !== '0', // only change if zero + write: 'setMinimumStakeTime', + writeArg: await getDeployParameter('MINIMUM_STAKE_TIME'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'debtSnapshotStaleTime', + expected: input => input !== '0', // only change if zero + write: 'setDebtSnapshotStaleTime', + writeArg: await getDeployParameter('DEBT_SNAPSHOT_STALE_TIME'), + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'crossDomainMessageGasLimit', + readArg: 0, + expected: input => input !== '0', // only change if zero + write: 'setCrossDomainMessageGasLimit', + writeArg: [0, await getDeployParameter('CROSS_DOMAIN_DEPOSIT_GAS_LIMIT')], + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'crossDomainMessageGasLimit', + readArg: 1, + expected: input => input !== '0', // only change if zero + write: 'setCrossDomainMessageGasLimit', + writeArg: [1, await getDeployParameter('CROSS_DOMAIN_ESCROW_GAS_LIMIT')], + }); + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'crossDomainMessageGasLimit', + readArg: 2, + expected: input => input !== '0', // only change if zero + write: 'setCrossDomainMessageGasLimit', + writeArg: [2, await getDeployParameter('CROSS_DOMAIN_REWARD_GAS_LIMIT')], + }); + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'crossDomainMessageGasLimit', + readArg: 3, + expected: input => input !== '0', // only change if zero + write: 'setCrossDomainMessageGasLimit', + writeArg: [3, await getDeployParameter('CROSS_DOMAIN_WITHDRAWAL_GAS_LIMIT')], + }); + + const aggregatorWarningFlags = (await getDeployParameter('AGGREGATOR_WARNING_FLAGS'))[network]; + // If deploying to OVM avoid ivoking setAggregatorWarningFlags for now. + if (aggregatorWarningFlags && !useOvm) { + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'aggregatorWarningFlags', + expected: input => input !== ZERO_ADDRESS, // only change if zero + write: 'setAggregatorWarningFlags', + writeArg: aggregatorWarningFlags, + }); + } + + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'etherWrapperMaxETH', + expected: input => input !== '0', // only change if zero + write: 'setEtherWrapperMaxETH', + writeArg: await getDeployParameter('ETHER_WRAPPER_MAX_ETH'), + }); + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'etherWrapperMintFeeRate', + expected: input => input !== '0', // only change if zero + write: 'setEtherWrapperMintFeeRate', + writeArg: await getDeployParameter('ETHER_WRAPPER_MINT_FEE_RATE'), + }); + await runStep({ + contract: 'SystemSettings', + target: SystemSettings, + read: 'etherWrapperBurnFeeRate', + expected: input => input !== '0', // only change if zero + write: 'setEtherWrapperBurnFeeRate', + writeArg: await getDeployParameter('ETHER_WRAPPER_BURN_FEE_RATE'), + }); + } +}; diff --git a/publish/src/commands/deploy/deploy-binary-options.js b/publish/src/commands/deploy/deploy-binary-options.js new file mode 100644 index 0000000000..34efc6ee7b --- /dev/null +++ b/publish/src/commands/deploy/deploy-binary-options.js @@ -0,0 +1,49 @@ +'use strict'; + +const { gray } = require('chalk'); + +const { + utils: { parseUnits }, +} = require('ethers'); + +module.exports = async ({ account, addressOf, deployer }) => { + // ---------------- + // Binary option market factory and manager setup + // ---------------- + + console.log(gray(`\n------ DEPLOY BINARY OPTIONS ------\n`)); + + const { ReadProxyAddressResolver } = deployer.deployedContracts; + + await deployer.deployContract({ + name: 'BinaryOptionMarketFactory', + args: [account, addressOf(ReadProxyAddressResolver)], + deps: ['AddressResolver'], + }); + + const day = 24 * 60 * 60; + const maxOraclePriceAge = 120 * 60; // Price updates are accepted from up to two hours before maturity to allow for delayed chainlink heartbeats. + const expiryDuration = 26 * 7 * day; // Six months to exercise options before the market is destructible. + const maxTimeToMaturity = 730 * day; // Markets may not be deployed more than two years in the future. + const creatorCapitalRequirement = parseUnits('1000').toString(); // 1000 sUSD is required to create a new market. + const creatorSkewLimit = parseUnits('0.05').toString(); // Market creators must leave 5% or more of their position on either side. + const poolFee = parseUnits('0.008').toString(); // 0.8% of the market's value goes to the pool in the end. + const creatorFee = parseUnits('0.002').toString(); // 0.2% of the market's value goes to the creator. + const refundFee = parseUnits('0.05').toString(); // 5% of a bid stays in the pot if it is refunded. + await deployer.deployContract({ + name: 'BinaryOptionMarketManager', + args: [ + account, + addressOf(ReadProxyAddressResolver), + maxOraclePriceAge, + expiryDuration, + maxTimeToMaturity, + creatorCapitalRequirement, + creatorSkewLimit, + poolFee, + creatorFee, + refundFee, + ], + deps: ['AddressResolver'], + }); +}; diff --git a/publish/src/commands/deploy/deploy-core.js b/publish/src/commands/deploy/deploy-core.js new file mode 100644 index 0000000000..a27d0abe64 --- /dev/null +++ b/publish/src/commands/deploy/deploy-core.js @@ -0,0 +1,244 @@ +'use strict'; + +const { gray } = require('chalk'); + +const { + constants: { ZERO_ADDRESS }, +} = require('../../../..'); + +module.exports = async ({ + account, + addressOf, + currentLastMintEvent, + currentSynthetixSupply, + currentWeekOfInflation, + deployer, + oracleAddress, + useOvm, +}) => { + console.log(gray(`\n------ DEPLOY LIBRARIES ------\n`)); + + await deployer.deployContract({ + name: 'SafeDecimalMath', + }); + + await deployer.deployContract({ + name: 'Math', + }); + + console.log(gray(`\n------ DEPLOY CORE PROTOCOL ------\n`)); + + await deployer.deployContract({ + name: 'AddressResolver', + args: [account], + }); + + const readProxyForResolver = await deployer.deployContract({ + name: 'ReadProxyAddressResolver', + source: 'ReadProxy', + args: [account], + }); + + await deployer.deployContract({ + name: 'FlexibleStorage', + deps: ['ReadProxyAddressResolver'], + args: [addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'SystemSettings', + args: [account, addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'SystemStatus', + args: [account], + }); + + await deployer.deployContract({ + name: 'ExchangeRates', + source: useOvm ? 'ExchangeRatesWithoutInvPricing' : 'ExchangeRates', + args: [account, oracleAddress, addressOf(readProxyForResolver), [], []], + }); + + await deployer.deployContract({ + name: 'RewardEscrow', + args: [account, ZERO_ADDRESS, ZERO_ADDRESS], + }); + + const rewardEscrowV2 = await deployer.deployContract({ + name: 'RewardEscrowV2', + source: useOvm ? 'ImportableRewardEscrowV2' : 'RewardEscrowV2', + args: [account, addressOf(readProxyForResolver)], + deps: ['AddressResolver'], + }); + + const synthetixEscrow = await deployer.deployContract({ + name: 'SynthetixEscrow', + args: [account, ZERO_ADDRESS], + }); + + await deployer.deployContract({ + name: 'SynthetixState', + source: useOvm ? 'SynthetixStateWithLimitedSetup' : 'SynthetixState', + args: [account, account], + }); + + const proxyFeePool = await deployer.deployContract({ + name: 'ProxyFeePool', + source: 'Proxy', + args: [account], + }); + + const delegateApprovalsEternalStorage = await deployer.deployContract({ + name: 'DelegateApprovalsEternalStorage', + source: 'EternalStorage', + args: [account, ZERO_ADDRESS], + }); + + await deployer.deployContract({ + name: 'DelegateApprovals', + args: [account, addressOf(delegateApprovalsEternalStorage)], + }); + + const liquidations = await deployer.deployContract({ + name: 'Liquidations', + args: [account, addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'EternalStorageLiquidations', + source: 'EternalStorage', + args: [account, addressOf(liquidations)], + }); + + await deployer.deployContract({ + name: 'FeePoolEternalStorage', + args: [account, ZERO_ADDRESS], + }); + + const feePool = await deployer.deployContract({ + name: 'FeePool', + deps: ['ProxyFeePool', 'AddressResolver'], + args: [addressOf(proxyFeePool), account, addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'FeePoolState', + deps: ['FeePool'], + args: [account, addressOf(feePool)], + }); + + await deployer.deployContract({ + name: 'RewardsDistribution', + deps: useOvm ? ['RewardEscrowV2', 'ProxyFeePool'] : ['RewardEscrowV2', 'ProxyFeePool'], + args: [ + account, // owner + ZERO_ADDRESS, // authority (synthetix) + ZERO_ADDRESS, // Synthetix Proxy + addressOf(rewardEscrowV2), + addressOf(proxyFeePool), + ], + }); + + // New Synthetix proxy. + const proxyERC20Synthetix = await deployer.deployContract({ + name: 'ProxyERC20', + args: [account], + }); + + const tokenStateSynthetix = await deployer.deployContract({ + name: 'TokenStateSynthetix', + source: 'TokenState', + args: [account, account], + }); + + await deployer.deployContract({ + name: 'Synthetix', + source: useOvm ? 'MintableSynthetix' : 'Synthetix', + deps: ['ProxyERC20', 'TokenStateSynthetix', 'AddressResolver'], + args: [ + addressOf(proxyERC20Synthetix), + addressOf(tokenStateSynthetix), + account, + currentSynthetixSupply, + addressOf(readProxyForResolver), + ], + }); + + // Old Synthetix proxy based off Proxy.sol: this has been deprecated. + // To be removed after May 30, 2020: + // https://docs.synthetix.io/integrations/guide/#proxy-deprecation + await deployer.deployContract({ + name: 'ProxySynthetix', + source: 'Proxy', + args: [account], + }); + + await deployer.deployContract({ + name: 'DebtCache', + deps: ['AddressResolver'], + args: [account, addressOf(readProxyForResolver)], + }); + + const exchanger = await deployer.deployContract({ + name: 'Exchanger', + source: useOvm ? 'Exchanger' : 'ExchangerWithVirtualSynth', + deps: ['AddressResolver'], + args: [account, addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'VirtualSynthMastercopy', + }); + + await deployer.deployContract({ + name: 'ExchangeState', + deps: ['Exchanger'], + args: [account, addressOf(exchanger)], + }); + + await deployer.deployContract({ + name: 'Issuer', + source: useOvm ? 'IssuerWithoutLiquidations' : 'Issuer', + deps: ['AddressResolver'], + args: [account, addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'TradingRewards', + deps: ['AddressResolver', 'Exchanger'], + args: [account, account, addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'SupplySchedule', + args: [account, currentLastMintEvent, currentWeekOfInflation], + }); + + if (synthetixEscrow) { + await deployer.deployContract({ + name: 'EscrowChecker', + deps: ['SynthetixEscrow'], + args: [addressOf(synthetixEscrow)], + }); + } + + await deployer.deployContract({ + name: 'SynthetixBridgeToBase', + deps: ['AddressResolver'], + args: [account, addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'SynthetixBridgeToOptimism', + deps: ['AddressResolver'], + args: [account, addressOf(readProxyForResolver)], + }); + + await deployer.deployContract({ + name: 'SynthetixBridgeEscrow', + deps: ['AddressResolver'], + args: [account], + }); +}; diff --git a/publish/src/commands/deploy/deploy-dapp-utils.js b/publish/src/commands/deploy/deploy-dapp-utils.js new file mode 100644 index 0000000000..67d2b944db --- /dev/null +++ b/publish/src/commands/deploy/deploy-dapp-utils.js @@ -0,0 +1,24 @@ +'use strict'; + +const { gray } = require('chalk'); + +module.exports = async ({ account, addressOf, deployer }) => { + console.log(gray(`\n------ DEPLOY DAPP UTILITIES ------\n`)); + + const { ReadProxyAddressResolver } = deployer.deployedContracts; + + await deployer.deployContract({ + name: 'SynthUtil', + deps: ['ReadProxyAddressResolver'], + args: [addressOf(ReadProxyAddressResolver)], + }); + + await deployer.deployContract({ + name: 'DappMaintenance', + args: [account], + }); + + await deployer.deployContract({ + name: 'BinaryOptionMarketData', + }); +}; diff --git a/publish/src/commands/deploy/deploy-loans.js b/publish/src/commands/deploy/deploy-loans.js new file mode 100644 index 0000000000..8625b116b0 --- /dev/null +++ b/publish/src/commands/deploy/deploy-loans.js @@ -0,0 +1,168 @@ +'use strict'; + +const { gray } = require('chalk'); +const { toBytes32 } = require('../../../..'); + +module.exports = async ({ account, addressOf, deployer, getDeployParameter, network, useOvm }) => { + console.log(gray(`\n------ DEPLOY ANCILLARY CONTRACTS ------\n`)); + + const { ReadProxyAddressResolver } = deployer.deployedContracts; + + await deployer.deployContract({ + name: 'Depot', + deps: ['ProxySynthetix', 'SynthsUSD', 'FeePool'], + args: [account, account, addressOf(ReadProxyAddressResolver)], + }); + + await deployer.deployContract({ + // name is EtherCollateral as it behaves as EtherCollateral in the address resolver + name: 'EtherCollateral', + source: useOvm ? 'EmptyEtherCollateral' : 'EtherCollateral', + args: useOvm ? [] : [account, addressOf(ReadProxyAddressResolver)], + }); + await deployer.deployContract({ + name: 'EtherCollateralsUSD', + source: useOvm ? 'EmptyEtherCollateral' : 'EtherCollateralsUSD', + args: useOvm ? [] : [account, addressOf(ReadProxyAddressResolver)], + }); + + let WETH_ADDRESS = (await getDeployParameter('WETH_ERC20_ADDRESSES'))[network]; + + if (network === 'local') { + // On local, deploy a mock WETH token. + // OVM already has a deployment of WETH, however since we use + // Hardhat for the local-ovm environment, we must deploy + // our own. + const weth = await deployer.deployContract({ + name: useOvm ? 'MockWETH' : 'WETH', + force: true, + }); + WETH_ADDRESS = weth.options.address; + } + + if (!WETH_ADDRESS) { + throw new Error('WETH address is not known'); + } + + await deployer.deployContract({ + name: 'EtherWrapper', + deps: ['AddressResolver'], + args: [account, addressOf(ReadProxyAddressResolver), WETH_ADDRESS], + }); + + await deployer.deployContract({ + name: 'NativeEtherWrapper', + deps: ['AddressResolver'], + args: [account, addressOf(ReadProxyAddressResolver)], + }); + + // ---------------- + // Multi Collateral System + // ---------------- + + const collateralManagerDefaults = await getDeployParameter('COLLATERAL_MANAGER'); + + console.log(gray(`\n------ DEPLOY MULTI COLLATERAL ------\n`)); + + const collateralManagerState = await deployer.deployContract({ + name: 'CollateralManagerState', + args: [account, account], + }); + + const useEmptyCollateralManager = useOvm; + const collateralManager = await deployer.deployContract({ + name: 'CollateralManager', + source: useEmptyCollateralManager ? 'EmptyCollateralManager' : 'CollateralManager', + args: useEmptyCollateralManager + ? [] + : [ + addressOf(collateralManagerState), + account, + addressOf(ReadProxyAddressResolver), + collateralManagerDefaults['MAX_DEBT'], + collateralManagerDefaults['BASE_BORROW_RATE'], + collateralManagerDefaults['BASE_SHORT_RATE'], + ], + }); + + const collateralStateEth = await deployer.deployContract({ + name: 'CollateralStateEth', + source: 'CollateralState', + args: [account, account], + }); + + await deployer.deployContract({ + name: 'CollateralEth', + args: [ + addressOf(collateralStateEth), + account, + addressOf(collateralManager), + addressOf(ReadProxyAddressResolver), + toBytes32('sETH'), + (await getDeployParameter('COLLATERAL_ETH'))['MIN_CRATIO'], + (await getDeployParameter('COLLATERAL_ETH'))['MIN_COLLATERAL'], + ], + }); + + const collateralStateErc20 = await deployer.deployContract({ + name: 'CollateralStateErc20', + source: 'CollateralState', + args: [account, account], + }); + + let RENBTC_ADDRESS = (await getDeployParameter('RENBTC_ERC20_ADDRESSES'))[network]; + if (!RENBTC_ADDRESS) { + if (network !== 'local') { + throw new Error('renBTC address is not known'); + } + + // On local, deploy a mock renBTC token to use as the underlying in CollateralErc20 + const renBTC = await deployer.deployContract({ + name: 'MockToken', + args: ['renBTC', 'renBTC', 8], + }); + + // this could be undefined in an env where MockToken is not listed in the config flags + RENBTC_ADDRESS = renBTC ? renBTC.options.address : undefined; + } + + await deployer.deployContract({ + name: 'CollateralErc20', + source: 'CollateralErc20', + args: [ + addressOf(collateralStateErc20), + account, + addressOf(collateralManager), + addressOf(ReadProxyAddressResolver), + toBytes32('sBTC'), + (await getDeployParameter('COLLATERAL_RENBTC'))['MIN_CRATIO'], + (await getDeployParameter('COLLATERAL_RENBTC'))['MIN_COLLATERAL'], + RENBTC_ADDRESS, // if undefined then this will error as expected. + 8, + ], + }); + + const collateralStateShort = await deployer.deployContract({ + name: 'CollateralStateShort', + source: 'CollateralState', + args: [account, account], + }); + + await deployer.deployContract({ + name: 'CollateralShort', + args: [ + addressOf(collateralStateShort), + account, + addressOf(collateralManager), + addressOf(ReadProxyAddressResolver), + toBytes32('sUSD'), + (await getDeployParameter('COLLATERAL_SHORT'))['MIN_CRATIO'], + (await getDeployParameter('COLLATERAL_SHORT'))['MIN_COLLATERAL'], + ], + }); + + return { + collateralManagerDefaults, + useEmptyCollateralManager, + }; +}; diff --git a/publish/src/commands/deploy/deploy-synths.js b/publish/src/commands/deploy/deploy-synths.js new file mode 100644 index 0000000000..ad8cf5a6e8 --- /dev/null +++ b/publish/src/commands/deploy/deploy-synths.js @@ -0,0 +1,133 @@ +'use strict'; + +const { gray, yellow } = require('chalk'); + +const { confirmAction } = require('../../util'); + +const { + toBytes32, + constants: { ZERO_ADDRESS }, +} = require('../../../..'); + +module.exports = async ({ + account, + addressOf, + addNewSynths, + config, + deployer, + freshDeploy, + network, + synths, + yes, +}) => { + // ---------------- + // Synths + // ---------------- + console.log(gray(`\n------ DEPLOY SYNTHS ------\n`)); + + const { Issuer, ReadProxyAddressResolver } = deployer.deployedContracts; + + // The list of synth to be added to the Issuer once dependencies have been set up + const synthsToAdd = []; + + for (const { name: currencyKey, subclass } of synths) { + console.log(gray(`\n --- SYNTH ${currencyKey} ---\n`)); + + const tokenStateForSynth = await deployer.deployContract({ + name: `TokenState${currencyKey}`, + source: 'TokenState', + args: [account, ZERO_ADDRESS], + force: addNewSynths, + }); + + // Legacy proxy will be around until May 30, 2020 + // https://docs.synthetix.io/integrations/guide/#proxy-deprecation + // Until this time, on mainnet we will still deploy ProxyERC20sUSD and ensure that + // SynthsUSD.proxy is ProxyERC20sUSD, SynthsUSD.integrationProxy is ProxysUSD + const synthProxyIsLegacy = currencyKey === 'sUSD' && network === 'mainnet'; + + const proxyForSynth = await deployer.deployContract({ + name: `Proxy${currencyKey}`, + source: synthProxyIsLegacy ? 'Proxy' : 'ProxyERC20', + args: [account], + force: addNewSynths, + }); + + // additionally deploy an ERC20 proxy for the synth if it's legacy (sUSD) + let proxyERC20ForSynth; + if (currencyKey === 'sUSD') { + proxyERC20ForSynth = await deployer.deployContract({ + name: `ProxyERC20${currencyKey}`, + source: `ProxyERC20`, + args: [account], + force: addNewSynths, + }); + } + + const currencyKeyInBytes = toBytes32(currencyKey); + + const synthConfig = config[`Synth${currencyKey}`] || {}; + + // track the original supply if we're deploying a new synth contract for an existing synth + let originalTotalSupply = 0; + if (synthConfig.deploy) { + try { + const oldSynth = deployer.getExistingContract({ contract: `Synth${currencyKey}` }); + originalTotalSupply = await oldSynth.methods.totalSupply().call(); + } catch (err) { + if (!freshDeploy) { + // only throw if not local - allows local environments to handle both new + // and updating configurations + throw err; + } + } + } + + // user confirm totalSupply is correct for oldSynth before deploy new Synth + if (synthConfig.deploy && !yes && originalTotalSupply > 0) { + try { + await confirmAction( + yellow( + `⚠⚠⚠ WARNING: Please confirm - ${network}:\n` + + `Synth${currencyKey} totalSupply is ${originalTotalSupply} \n` + ) + + gray('-'.repeat(50)) + + '\nDo you want to continue? (y/n) ' + ); + } catch (err) { + console.log(gray('Operation cancelled')); + return; + } + } + + const sourceContract = subclass || 'Synth'; + const synth = await deployer.deployContract({ + name: `Synth${currencyKey}`, + source: sourceContract, + deps: [`TokenState${currencyKey}`, `Proxy${currencyKey}`, 'Synthetix', 'FeePool'], + args: [ + proxyERC20ForSynth ? addressOf(proxyERC20ForSynth) : addressOf(proxyForSynth), + addressOf(tokenStateForSynth), + `Synth ${currencyKey}`, + currencyKey, + account, + currencyKeyInBytes, + originalTotalSupply, + addressOf(ReadProxyAddressResolver), + ], + force: addNewSynths, + }); + + // Save the synth to be added once the AddressResolver has been synced. + if (synth && Issuer) { + synthsToAdd.push({ + synth, + currencyKeyInBytes, + }); + } + } + + return { + synthsToAdd, + }; +}; diff --git a/publish/src/commands/deploy/get-deploy-parameter-factory.js b/publish/src/commands/deploy/get-deploy-parameter-factory.js new file mode 100644 index 0000000000..92f832fd79 --- /dev/null +++ b/publish/src/commands/deploy/get-deploy-parameter-factory.js @@ -0,0 +1,48 @@ +'use strict'; + +const { yellow } = require('chalk'); + +const { defaults } = require('../../../..'); + +const { confirmAction } = require('../../util'); + +module.exports = ({ params, yes, ignoreCustomParameters }) => async name => { + const defaultParam = defaults[name]; + if (ignoreCustomParameters) { + return defaultParam; + } + + let effectiveValue = defaultParam; + + const param = (params || []).find(p => p.name === name); + + if (param) { + if (!yes) { + try { + await confirmAction( + yellow( + `⚠⚠⚠ WARNING: Found an entry for ${param.name} in params.json. Specified value is ${param.value} and default is ${defaultParam}.` + + '\nDo you want to use the specified value (default otherwise)? (y/n) ' + ) + ); + + effectiveValue = param.value; + } catch (err) { + console.error(err); + } + } else { + // yes = true + effectiveValue = param.value; + } + } + + if (effectiveValue !== defaultParam) { + console.log( + yellow( + `PARAMETER OVERRIDE: Overriding default ${name} with ${effectiveValue}, specified in params.json.` + ) + ); + } + + return effectiveValue; +}; diff --git a/publish/src/commands/deploy/import-addresses.js b/publish/src/commands/deploy/import-addresses.js new file mode 100644 index 0000000000..fad85b8842 --- /dev/null +++ b/publish/src/commands/deploy/import-addresses.js @@ -0,0 +1,84 @@ +'use strict'; + +const { gray, green, yellow } = require('chalk'); +const { toBytes32 } = require('../../../..'); + +const { reportDeployedContracts } = require('../../util'); + +module.exports = async ({ addressOf, deployer, limitPromise, runStep }) => { + console.log(gray(`\n------ CONFIGURE ADDRESS RESOLVER ------\n`)); + + const { AddressResolver, ReadProxyAddressResolver } = deployer.deployedContracts; + + // Note: RPAR.setTarget(AR) MUST go before the addresses are imported into the resolver. + // most of the time it will be a no-op but when there's a new AddressResolver, it's critical + if (AddressResolver && ReadProxyAddressResolver) { + await runStep({ + contract: 'ReadProxyAddressResolver', + target: ReadProxyAddressResolver, + read: 'target', + expected: input => input === addressOf(AddressResolver), + write: 'setTarget', + writeArg: addressOf(AddressResolver), + }); + } + + let addressesAreImported = false; + + if (AddressResolver) { + const addressArgs = [[], []]; + + const allContracts = Object.entries(deployer.deployedContracts); + await Promise.all( + allContracts.map(([name, contract]) => { + return limitPromise(async () => { + const isImported = await AddressResolver.methods + .areAddressesImported([toBytes32(name)], [contract.options.address]) + .call(); + + if (!isImported) { + console.log(green(`${name} needs to be imported to the AddressResolver`)); + + addressArgs[0].push(toBytes32(name)); + addressArgs[1].push(contract.options.address); + } + }); + }) + ); + + const { pending } = await runStep({ + gasLimit: 6e6, // higher gas required + contract: `AddressResolver`, + target: AddressResolver, + read: 'areAddressesImported', + readArg: addressArgs, + expected: input => input, + write: 'importAddresses', + writeArg: addressArgs, + }); + + addressesAreImported = !pending; + } + + // When addresses not yet imported, the deployment must be suspended until it can be completed by the owner. + // This relies on the fact that runStep returns undefined if nothing needed to be done, a tx hash if the + // transaction could be mined, and true in other cases, including appending to the owner actions file. + // Note that this will also end the script in the case of manual transaction mining. + if (!addressesAreImported) { + console.log(gray(`\n------ DEPLOY PARTIALLY COMPLETED ------\n`)); + + console.log( + yellow( + '⚠⚠⚠ WARNING: Addresses have not been imported into the resolver, owner actions must be performed before re-running the script.' + ) + ); + + if (deployer.newContractsDeployed.length > 0) { + reportDeployedContracts({ deployer }); + } + + process.exit(); + } else { + console.log(gray('Addresses are correctly set up.')); + } +}; diff --git a/publish/src/commands/deploy/index.js b/publish/src/commands/deploy/index.js new file mode 100644 index 0000000000..406627c8c2 --- /dev/null +++ b/publish/src/commands/deploy/index.js @@ -0,0 +1,506 @@ +'use strict'; + +const path = require('path'); +const { gray, red } = require('chalk'); +const { constants } = require('ethers'); +const pLimit = require('p-limit'); +const Deployer = require('../../Deployer'); +const NonceManager = require('../../NonceManager'); +const { loadCompiledFiles } = require('../../solidity'); + +const { + ensureDeploymentPath, + ensureNetwork, + getDeploymentPathForNetwork, + loadAndCheckRequiredSources, + loadConnections, + performTransactionalStep, + reportDeployedContracts, +} = require('../../util'); + +const { + constants: { BUILD_FOLDER, CONFIG_FILENAME, SYNTHS_FILENAME, DEPLOYMENT_FILENAME }, +} = require('../../../..'); + +const performSafetyChecks = require('./perform-safety-checks'); +const getDeployParameterFactory = require('./get-deploy-parameter-factory'); +const systemAndParameterCheck = require('./system-and-parameter-check'); +const deployCore = require('./deploy-core'); +const deploySynths = require('./deploy-synths'); +const deployLoans = require('./deploy-loans'); +const deployDappUtils = require('./deploy-dapp-utils.js'); +const deployBinaryOptions = require('./deploy-binary-options'); +const importAddresses = require('./import-addresses'); +const rebuildResolverCaches = require('./rebuild-resolver-caches'); +const configureLegacySettings = require('./configure-legacy-settings'); +const configureStandalonePriceFeeds = require('./configure-standalone-price-feeds'); +const configureSynths = require('./configure-synths'); +const addSynthsToProtocol = require('./add-synths-to-protocol'); +const configureInverseSynths = require('./configure-inverse-synths'); +const configureSystemSettings = require('./configure-system-settings'); +const configureLoans = require('./configure-loans'); +const takeDebtSnapshotWhenRequired = require('./take-debt-snapshot-when-required'); + +const DEFAULTS = { + gasPrice: '1', + methodCallGasLimit: 250e3, // 250k + contractDeploymentGasLimit: 6.9e6, // TODO split out into separate limits for different contracts, Proxys, Synths, Synthetix + debtSnapshotMaxDeviation: 0.01, // a 1 percent deviation will trigger a snapshot + network: 'kovan', + buildPath: path.join(__dirname, '..', '..', '..', '..', BUILD_FOLDER), +}; + +const deploy = async ({ + addNewSynths, + buildPath = DEFAULTS.buildPath, + concurrency, + contractDeploymentGasLimit = DEFAULTS.contractDeploymentGasLimit, + deploymentPath, + dryRun = false, + forceUpdateInverseSynthsOnTestnet = false, + freshDeploy, + gasPrice = DEFAULTS.gasPrice, + ignoreCustomParameters, + ignoreSafetyChecks, + manageNonces, + methodCallGasLimit = DEFAULTS.methodCallGasLimit, + network = DEFAULTS.network, + oracleExrates, + privateKey, + providerUrl, + skipFeedChecks = false, + specifyContracts, + useFork, + useOvm, + yes, +} = {}) => { + ensureNetwork(network); + deploymentPath = deploymentPath || getDeploymentPathForNetwork({ network, useOvm }); + ensureDeploymentPath(deploymentPath); + + // OVM uses a gas price of 0 (unless --gas explicitely defined). + if (useOvm && gasPrice === DEFAULTS.gasPrice) { + gasPrice = constants.Zero; + } + + const limitPromise = pLimit(concurrency); + + const { + config, + params, + configFile, + synths, + deployment, + deploymentFile, + ownerActions, + ownerActionsFile, + feeds, + } = loadAndCheckRequiredSources({ + deploymentPath, + network, + freshDeploy, + }); + + const getDeployParameter = getDeployParameterFactory({ params, yes, ignoreCustomParameters }); + + const addressOf = c => (c ? c.options.address : ''); + + // Mark contracts for deployment specified via an argument + if (specifyContracts) { + // Ignore config.json + Object.keys(config).map(name => { + config[name].deploy = false; + }); + // Add specified contracts + specifyContracts.split(',').map(name => { + if (!config[name]) { + config[name] = { + deploy: true, + }; + } else { + config[name].deploy = true; + } + }); + } + + performSafetyChecks({ + config, + contractDeploymentGasLimit, + deployment, + deploymentPath, + freshDeploy, + ignoreSafetyChecks, + manageNonces, + methodCallGasLimit, + network, + useOvm, + }); + + const standaloneFeeds = Object.values(feeds).filter(({ standalone }) => standalone); + + console.log( + gray('Checking all contracts not flagged for deployment have addresses in this network...') + ); + const missingDeployments = Object.keys(config).filter(name => { + return !config[name].deploy && (!deployment.targets[name] || !deployment.targets[name].address); + }); + + if (missingDeployments.length) { + throw Error( + `Cannot use existing contracts for deployment as addresses not found for the following contracts on ${network}:\n` + + missingDeployments.join('\n') + + '\n' + + gray(`Used: ${deploymentFile} as source`) + ); + } + + console.log(gray('Loading the compiled contracts locally...')); + const { earliestCompiledTimestamp, compiled } = loadCompiledFiles({ buildPath }); + + const { + providerUrl: envProviderUrl, + privateKey: envPrivateKey, + etherscanLinkPrefix, + } = loadConnections({ + network, + useFork, + }); + + if (!providerUrl) { + if (!envProviderUrl) { + throw new Error('Missing .env key of PROVIDER_URL. Please add and retry.'); + } + + providerUrl = envProviderUrl; + } + + // if not specified, or in a local network, override the private key passed as a CLI option, with the one specified in .env + if (network !== 'local' && !privateKey) { + privateKey = envPrivateKey; + } + + const nonceManager = new NonceManager({}); + + const deployer = new Deployer({ + compiled, + contractDeploymentGasLimit, + config, + configFile, + deployment, + deploymentFile, + gasPrice, + methodCallGasLimit, + network, + privateKey, + providerUrl, + dryRun, + useOvm, + useFork, + ignoreSafetyChecks, + nonceManager: manageNonces ? nonceManager : undefined, + }); + + const { account } = deployer; + + nonceManager.web3 = deployer.provider.web3; + nonceManager.account = account; + + const { + currentSynthetixSupply, + currentLastMintEvent, + currentWeekOfInflation, + oldExrates, + oracleAddress, + } = await systemAndParameterCheck({ + account, + addNewSynths, + concurrency, + config, + contractDeploymentGasLimit, + deployer, + deploymentPath, + dryRun, + earliestCompiledTimestamp, + freshDeploy, + gasPrice, + getDeployParameter, + methodCallGasLimit, + network, + oracleExrates, + providerUrl, + skipFeedChecks, + standaloneFeeds, + synths, + useFork, + useOvm, + yes, + }); + + console.log( + gray(`Starting deployment to ${network.toUpperCase()}${useFork ? ' (fork)' : ''}...`) + ); + + const runStep = async opts => + performTransactionalStep({ + gasLimit: methodCallGasLimit, // allow overriding of gasLimit + ...opts, + account, + gasPrice, + etherscanLinkPrefix, + ownerActions, + ownerActionsFile, + dryRun, + nonceManager: manageNonces ? nonceManager : undefined, + }); + + await deployCore({ + account, + addressOf, + currentLastMintEvent, + currentSynthetixSupply, + currentWeekOfInflation, + deployer, + oracleAddress, + useOvm, + }); + + const { synthsToAdd } = await deploySynths({ + account, + addressOf, + addNewSynths, + config, + deployer, + freshDeploy, + network, + synths, + yes, + }); + + const { useEmptyCollateralManager, collateralManagerDefaults } = await deployLoans({ + account, + addressOf, + deployer, + getDeployParameter, + network, + useOvm, + }); + + await deployBinaryOptions({ + account, + addressOf, + deployer, + }); + + await deployDappUtils({ + account, + addressOf, + deployer, + }); + + await importAddresses({ + addressOf, + deployer, + limitPromise, + runStep, + }); + + await rebuildResolverCaches({ + addressOf, + compiled, + deployer, + limitPromise, + network, + runStep, + useOvm, + }); + + await configureLegacySettings({ + account, + addressOf, + config, + deployer, + getDeployParameter, + network, + runStep, + useOvm, + }); + + await configureStandalonePriceFeeds({ + deployer, + runStep, + standaloneFeeds, + }); + + await configureSynths({ + addressOf, + synths, + feeds, + deployer, + runStep, + }); + + await addSynthsToProtocol({ + addressOf, + deployer, + runStep, + synthsToAdd, + }); + + await configureInverseSynths({ + addressOf, + deployer, + forceUpdateInverseSynthsOnTestnet, + network, + oldExrates, + runStep, + synths, + }); + + await configureSystemSettings({ + deployer, + methodCallGasLimit, + useOvm, + getDeployParameter, + network, + runStep, + synths, + }); + + await configureLoans({ + addressOf, + collateralManagerDefaults, + deployer, + getDeployParameter, + runStep, + useEmptyCollateralManager, + }); + + await takeDebtSnapshotWhenRequired({ + debtSnapshotMaxDeviation: DEFAULTS.debtSnapshotMaxDeviation, + deployer, + runStep, + useOvm, + }); + + console.log(gray(`\n------ DEPLOY COMPLETE ------\n`)); + + reportDeployedContracts({ deployer }); +}; + +module.exports = { + deploy, + DEFAULTS, + cmd: program => + program + .command('deploy') + .description('Deploy compiled solidity files') + .option( + '-a, --add-new-synths', + `Whether or not any new synths in the ${SYNTHS_FILENAME} file should be deployed if there is no entry in the config file` + ) + .option( + '-b, --build-path [value]', + 'Path to a folder hosting compiled files from the "build" step in this script', + DEFAULTS.buildPath + ) + .option( + '-c, --contract-deployment-gas-limit ', + 'Contract deployment gas limit', + parseFloat, + DEFAULTS.contractDeploymentGasLimit + ) + .option( + '-d, --deployment-path ', + `Path to a folder that has your input configuration file ${CONFIG_FILENAME}, the synth list ${SYNTHS_FILENAME} and where your ${DEPLOYMENT_FILENAME} files will go` + ) + .option( + '-e, --concurrency ', + 'Number of parallel calls that can be made to a provider', + 10 + ) + .option( + '-f, --fee-auth ', + 'The address of the fee authority for this network (default is to use existing)' + ) + .option('-g, --gas-price ', 'Gas price in GWEI', DEFAULTS.gasPrice) + .option( + '-h, --fresh-deploy', + 'Perform a "fresh" deploy, i.e. the first deployment on a network.' + ) + .option( + '-i, --ignore-safety-checks', + 'Ignores some validations regarding paths, compiler versions, etc.', + false + ) + .option( + '--ignore-custom-parameters', + 'Ignores deployment parameters specified in params.json', + false + ) + .option( + '-k, --use-fork', + 'Perform the deployment on a forked chain running on localhost (see fork command).', + false + ) + .option( + '-l, --oracle-gas-limit ', + 'The address of the gas limit oracle for this network (default is use existing)' + ) + .option( + '-m, --method-call-gas-limit ', + 'Method call gas limit', + parseFloat, + DEFAULTS.methodCallGasLimit + ) + .option( + '-n, --network ', + 'The network to run off.', + x => x.toLowerCase(), + DEFAULTS.network + ) + .option( + '-o, --oracle-exrates ', + 'The address of the oracle for this network (default is use existing)' + ) + .option( + '-q, --manage-nonces', + 'The command makes sure that no repeated nonces are sent (which may be the case when reorgs are common, i.e. in Goerli. Not to be confused with --manage-nonsense.)', + false + ) + .option( + '-p, --provider-url ', + 'Ethereum network provider URL. If default, will use PROVIDER_URL found in the .env file.' + ) + .option( + '--skip-feed-checks', + 'If enabled, will skip the feed checking on start (speeds up deployment)' + ) + .option( + '-r, --dry-run', + 'If enabled, will not run any transactions but merely report on them.' + ) + .option( + '-v, --private-key [value]', + 'The private key to deploy with (only works in local mode, otherwise set in .env).' + ) + .option( + '-u, --force-update-inverse-synths-on-testnet', + 'Allow inverse synth pricing to be updated on testnet regardless of total supply' + ) + .option( + '-x, --specify-contracts ', + 'Ignore config.json and specify contracts to be deployed (Comma separated list)' + ) + .option('-y, --yes', 'Dont prompt, just reply yes.') + .option('-z, --use-ovm', 'Target deployment for the OVM (Optimism).') + .action(async (...args) => { + try { + await deploy(...args); + } catch (err) { + // show pretty errors for CLI users + console.error(red(err)); + console.log(err.stack); + process.exitCode = 1; + } + }), +}; diff --git a/publish/src/commands/deploy/perform-safety-checks.js b/publish/src/commands/deploy/perform-safety-checks.js new file mode 100644 index 0000000000..ddeb2cf99c --- /dev/null +++ b/publish/src/commands/deploy/perform-safety-checks.js @@ -0,0 +1,86 @@ +'use strict'; + +const { + constants: { OVM_MAX_GAS_LIMIT }, + nonUpgradeable, +} = require('../../../..'); + +module.exports = ({ + config, + contractDeploymentGasLimit, + deployment, + deploymentPath, + freshDeploy, + ignoreSafetyChecks, + manageNonces, + methodCallGasLimit, + network, + useOvm, +}) => { + if (!ignoreSafetyChecks) { + // Using Goerli without manageNonces? + if (network.toLowerCase() === 'goerli' && !useOvm && !manageNonces) { + throw new Error(`Deploying on Goerli needs to be performed with --manage-nonces.`); + } + + // Cannot re-deploy legacy contracts + if (!freshDeploy) { + // Get list of contracts to be deployed + const contractsToDeploy = []; + Object.keys(config).map(contractName => { + if (config[contractName].deploy) { + contractsToDeploy.push(contractName); + } + }); + + // Check that no non-deployable is marked for deployment. + // Note: if nonDeployable = 'TokenState', this will match 'TokenStatesUSD' + nonUpgradeable.map(nonUpgradeableContract => { + contractsToDeploy.map(contractName => { + if (contractName.match(new RegExp(`^${nonUpgradeableContract}`, 'g'))) { + throw new Error( + `You are attempting to deploy a contract marked as non-upgradeable: ${contractName}. This action could result in loss of state. Please verify and use --ignore-safety-checks if you really know what you're doing.` + ); + } + }); + }); + } + + // Every transaction in Optimism needs to be below 9m gas, to ensure + // there are no deployment out of gas errors during fraud proofs. + if (useOvm) { + const maxOptimismGasLimit = OVM_MAX_GAS_LIMIT; + if ( + contractDeploymentGasLimit > maxOptimismGasLimit || + methodCallGasLimit > maxOptimismGasLimit + ) { + throw new Error( + `Maximum transaction gas limit for OVM is ${maxOptimismGasLimit} gas, and specified contractDeploymentGasLimit and/or methodCallGasLimit are over such limit. Please make sure that these values are below the maximum gas limit to guarantee that fraud proofs can be done in L1.` + ); + } + } + + // Deploying on OVM and not using an OVM deployment path? + const lastPathItem = deploymentPath.split('/').pop(); + const isOvmPath = lastPathItem.includes('ovm'); + const deploymentPathMismatch = (useOvm && !isOvmPath) || (!useOvm && isOvmPath); + if (deploymentPathMismatch) { + if (useOvm) { + throw new Error( + `You are deploying to a non-ovm path ${deploymentPath}, while --use-ovm is true.` + ); + } else { + throw new Error( + `You are deploying to an ovm path ${deploymentPath}, while --use-ovm is false.` + ); + } + } + + // Fresh deploy and deployment.json not empty? + if (freshDeploy && Object.keys(deployment.targets).length > 0 && network !== 'local') { + throw new Error( + `Cannot make a fresh deploy on ${deploymentPath} because a deployment has already been made on this path. If you intend to deploy a new instance, use a different path or delete the deployment files for this one.` + ); + } + } +}; diff --git a/publish/src/commands/deploy/rebuild-resolver-caches.js b/publish/src/commands/deploy/rebuild-resolver-caches.js new file mode 100644 index 0000000000..374391bd33 --- /dev/null +++ b/publish/src/commands/deploy/rebuild-resolver-caches.js @@ -0,0 +1,312 @@ +'use strict'; + +const { gray, red, yellow, redBright } = require('chalk'); +const { + fromBytes32, + constants: { OVM_MAX_GAS_LIMIT, ZERO_ADDRESS }, +} = require('../../../..'); + +module.exports = async ({ + addressOf, + compiled, + deployer, + limitPromise, + network, + runStep, + useOvm, +}) => { + const { + AddressResolver, + BinaryOptionMarketManager, + ReadProxyAddressResolver, + } = deployer.deployedContracts; + + // Legacy contracts. + if (network === 'mainnet') { + // v2.35.2 contracts. + const CollateralEth = '0x3FF5c0A14121Ca39211C95f6cEB221b86A90729E'; + const CollateralErc20REN = '0x3B3812BB9f6151bEb6fa10783F1ae848a77a0d46'; + const CollateralShort = '0x188C2274B04Ea392B21487b5De299e382Ff84246'; + + const legacyContracts = Object.entries({ + CollateralEth, + CollateralErc20REN, + CollateralShort, + }).map(([name, address]) => { + const contract = new deployer.provider.web3.eth.Contract( + [...compiled['MixinResolver'].abi, ...compiled['Owned'].abi], + address + ); + return [`legacy:${name}`, contract]; + }); + + await Promise.all( + legacyContracts.map(async ([name, contract]) => { + return runStep({ + gasLimit: 7e6, + contract: name, + target: contract, + read: 'isResolverCached', + expected: input => input, + publiclyCallable: true, // does not require owner + write: 'rebuildCache', + }); + }) + ); + } + + const filterTargetsWith = ({ prop }) => + Object.entries(deployer.deployedContracts).filter(([, target]) => + target.options.jsonInterface.find(({ name }) => name === prop) + ); + + const contractsWithRebuildableCache = filterTargetsWith({ prop: 'rebuildCache' }); + + // collect all resolver addresses required + const resolverAddressesRequired = ( + await Promise.all( + contractsWithRebuildableCache.map(([, contract]) => { + return limitPromise(() => contract.methods.resolverAddressesRequired().call()); + }) + ) + ).reduce((allAddresses, contractAddresses) => { + return allAddresses.concat( + contractAddresses.filter(contractAddress => !allAddresses.includes(contractAddress)) + ); + }, []); + + // check which resolver addresses are imported + const resolvedAddresses = await Promise.all( + resolverAddressesRequired.map(id => { + return limitPromise(() => AddressResolver.methods.getAddress(id).call()); + }) + ); + const isResolverAddressImported = {}; + for (let i = 0; i < resolverAddressesRequired.length; i++) { + isResolverAddressImported[resolverAddressesRequired[i]] = resolvedAddresses[i] !== ZERO_ADDRESS; + } + + // print out resolver addresses + console.log(gray('Imported resolver addresses:')); + for (const id of Object.keys(isResolverAddressImported)) { + const isImported = isResolverAddressImported[id]; + const chalkFn = isImported ? gray : red; + console.log(chalkFn(` > ${fromBytes32(id)}: ${isImported}`)); + } + + // now ensure all caches are rebuilt for those in need + const contractsToRebuildCache = []; + for (const [name, target] of contractsWithRebuildableCache) { + const isCached = await target.methods.isResolverCached().call(); + if (!isCached) { + const requiredAddresses = await target.methods.resolverAddressesRequired().call(); + + const unknownAddress = requiredAddresses.find(id => !isResolverAddressImported[id]); + if (unknownAddress) { + console.log( + redBright( + `WARINING: Not invoking ${name}.rebuildCache() because ${fromBytes32( + unknownAddress + )} is unknown. This contract requires: ${requiredAddresses.map(id => fromBytes32(id))}` + ) + ); + } else { + contractsToRebuildCache.push(target.options.address); + } + } + } + + const addressesChunkSize = useOvm ? 7 : 20; + for (let i = 0; i < contractsToRebuildCache.length; i += addressesChunkSize) { + const chunk = contractsToRebuildCache.slice(i, i + addressesChunkSize); + await runStep({ + gasLimit: useOvm ? OVM_MAX_GAS_LIMIT : 7e6, + contract: `AddressResolver`, + target: AddressResolver, + publiclyCallable: true, // does not require owner + write: 'rebuildCaches', + writeArg: [chunk], + }); + } + + console.log(gray('Double check all contracts with rebuildCache() are rebuilt...')); + for (const [contract, target] of contractsWithRebuildableCache) { + if (contractsToRebuildCache.includes(target.options.address)) { + await runStep({ + gasLimit: 500e3, // higher gas required + contract, + target, + read: 'isResolverCached', + expected: input => input, + publiclyCallable: true, // does not require owner + write: 'rebuildCache', + }); + } + } + + // Now do binary option market cache rebuilding + if (BinaryOptionMarketManager) { + console.log(gray('Checking all binary option markets have rebuilt caches')); + let binaryOptionMarkets = []; + // now grab all possible binary option markets to rebuild caches as well + const binaryOptionsFetchPageSize = 100; + for (const marketType of ['Active', 'Matured']) { + const numBinaryOptionMarkets = Number( + await BinaryOptionMarketManager.methods[`num${marketType}Markets`]().call() + ); + console.log( + gray('Found'), + yellow(numBinaryOptionMarkets), + gray(marketType, 'binary option markets') + ); + + if (numBinaryOptionMarkets > binaryOptionsFetchPageSize) { + console.log( + redBright( + '⚠⚠⚠ Warning: cannot fetch all', + marketType, + 'binary option markets as there are', + numBinaryOptionMarkets, + 'which is more than page size of', + binaryOptionsFetchPageSize + ) + ); + } else { + // fetch the list of markets + const marketAddresses = await BinaryOptionMarketManager.methods[ + `${marketType.toLowerCase()}Markets` + ](0, binaryOptionsFetchPageSize).call(); + + // wrap them in a contract via the deployer + const markets = marketAddresses.map( + binaryOptionMarket => + new deployer.provider.web3.eth.Contract( + compiled['BinaryOptionMarket'].abi, + binaryOptionMarket + ) + ); + + binaryOptionMarkets = binaryOptionMarkets.concat(markets); + } + } + + // now figure out which binary option markets need their caches rebuilt + const binaryOptionMarketsToRebuildCacheOn = []; + for (const market of binaryOptionMarkets) { + try { + const isCached = await market.methods.isResolverCached().call(); + if (!isCached) { + binaryOptionMarketsToRebuildCacheOn.push(addressOf(market)); + } + console.log( + gray('Binary option market'), + yellow(addressOf(market)), + gray('is newer and cache status'), + yellow(isCached) + ); + } catch (err) { + // the challenge being that some used an older MixinResolver API + const oldBinaryOptionMarketABI = [ + { + constant: true, + inputs: [ + { + internalType: 'contract AddressResolver', + name: '_resolver', + type: 'address', + }, + ], + name: 'isResolverCached', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + signature: '0x631e1444', + }, + ]; + + const oldBinaryOptionMarket = new deployer.provider.web3.eth.Contract( + oldBinaryOptionMarketABI, + addressOf(market) + ); + + const isCached = await oldBinaryOptionMarket.methods + .isResolverCached(addressOf(ReadProxyAddressResolver)) + .call(); + if (!isCached) { + binaryOptionMarketsToRebuildCacheOn.push(addressOf(market)); + } + + console.log( + gray('Binary option market'), + yellow(addressOf(market)), + gray('is older and cache status'), + yellow(isCached) + ); + } + } + + console.log( + gray('In total'), + yellow(binaryOptionMarketsToRebuildCacheOn.length), + gray('binary option markets need their caches rebuilt') + ); + + const addressesChunkSize = useOvm ? 7 : 20; + for (let i = 0; i < binaryOptionMarketsToRebuildCacheOn.length; i += addressesChunkSize) { + const chunk = binaryOptionMarketsToRebuildCacheOn.slice(i, i + addressesChunkSize); + await runStep({ + gasLimit: useOvm ? OVM_MAX_GAS_LIMIT : 7e6, + contract: `BinaryOptionMarketManager`, + target: BinaryOptionMarketManager, + publiclyCallable: true, // does not require owner + write: 'rebuildMarketCaches', + writeArg: [chunk], + }); + } + } + + // Now perform a sync of legacy contracts that have not been replaced in Shaula (v2.35.x) + // EtherCollateral, EtherCollateralsUSD + console.log(gray('Checking all legacy contracts with setResolverAndSyncCache() are rebuilt...')); + const contractsWithLegacyResolverCaching = filterTargetsWith({ + prop: 'setResolverAndSyncCache', + }); + for (const [contract, target] of contractsWithLegacyResolverCaching) { + await runStep({ + gasLimit: 500e3, // higher gas required + contract, + target, + read: 'isResolverCached', + readArg: addressOf(ReadProxyAddressResolver), + expected: input => input, + write: 'setResolverAndSyncCache', + writeArg: addressOf(ReadProxyAddressResolver), + }); + } + + // Finally set resolver on contracts even older than legacy (Depot) + console.log(gray('Checking all legacy contracts with setResolver() are rebuilt...')); + const contractsWithLegacyResolverNoCache = filterTargetsWith({ + prop: 'setResolver', + }); + for (const [contract, target] of contractsWithLegacyResolverNoCache) { + await runStep({ + gasLimit: 500e3, // higher gas required + contract, + target, + read: 'resolver', + expected: input => addressOf(ReadProxyAddressResolver), + write: 'setResolver', + writeArg: addressOf(ReadProxyAddressResolver), + }); + } + + console.log(gray('All caches are rebuilt. ')); +}; diff --git a/publish/src/commands/deploy/system-and-parameter-check.js b/publish/src/commands/deploy/system-and-parameter-check.js new file mode 100644 index 0000000000..55427e1e23 --- /dev/null +++ b/publish/src/commands/deploy/system-and-parameter-check.js @@ -0,0 +1,252 @@ +'use strict'; + +const path = require('path'); + +const { gray, green, yellow, red } = require('chalk'); + +const { + utils: { parseUnits, formatUnits, isAddress }, + constants, +} = require('ethers'); + +const checkAggregatorPrices = require('./check-aggregator-prices'); + +const { confirmAction, parameterNotice } = require('../../util'); + +const { getLatestSolTimestamp } = require('../../solidity'); + +const { + constants: { CONTRACTS_FOLDER, inflationStartTimestampInSecs }, +} = require('../../../..'); + +module.exports = async ({ + account, + addNewSynths, + concurrency, + config, + contractDeploymentGasLimit, + deployer, + deploymentPath, + dryRun, + earliestCompiledTimestamp, + freshDeploy, + gasPrice, + getDeployParameter, + methodCallGasLimit, + network, + oracleExrates, + providerUrl, + skipFeedChecks, + standaloneFeeds, + synths, + useFork, + useOvm, + yes, +}) => { + let oracleAddress; + let currentSynthetixSupply; + let oldExrates; + let currentLastMintEvent; + let currentWeekOfInflation; + let systemSuspended = false; + let systemSuspendedReason; + + try { + const oldSynthetix = deployer.getExistingContract({ contract: 'Synthetix' }); + currentSynthetixSupply = await oldSynthetix.methods.totalSupply().call(); + + // inflationSupplyToDate = total supply - 100m + const inflationSupplyToDate = parseUnits(currentSynthetixSupply, 'wei').sub( + parseUnits((100e6).toString(), 'wei') + ); + + // current weekly inflation 75m / 52 + const weeklyInflation = parseUnits((75e6 / 52).toString()).toString(); + currentWeekOfInflation = inflationSupplyToDate.div(weeklyInflation); + + // Check result is > 0 else set to 0 for currentWeek + currentWeekOfInflation = currentWeekOfInflation.gt(constants.Zero) + ? currentWeekOfInflation.toNumber() + : 0; + + // Calculate lastMintEvent as Inflation start date + number of weeks issued * secs in weeks + const mintingBuffer = 86400; + const secondsInWeek = 604800; + const inflationStartDate = inflationStartTimestampInSecs; + currentLastMintEvent = + inflationStartDate + currentWeekOfInflation * secondsInWeek + mintingBuffer; + } catch (err) { + if (freshDeploy) { + currentSynthetixSupply = await getDeployParameter('INITIAL_ISSUANCE'); + currentWeekOfInflation = 0; + currentLastMintEvent = 0; + } else { + console.error( + red( + 'Cannot connect to existing Synthetix contract. Please double check the deploymentPath is correct for the network allocated' + ) + ); + throw Error('Cannot deploy. Halted.'); + } + } + + try { + oldExrates = deployer.getExistingContract({ contract: 'ExchangeRates' }); + if (!oracleExrates) { + oracleAddress = await oldExrates.methods.oracle().call(); + } + } catch (err) { + if (freshDeploy) { + oracleAddress = oracleExrates || account; + oldExrates = undefined; // unset to signify that a fresh one will be deployed + } else { + console.error( + red( + 'Cannot connect to existing ExchangeRates contract. Please double check the deploymentPath is correct for the network allocated' + ) + ); + throw Error('Cannot deploy. Halted.'); + } + } + + try { + const oldSystemStatus = deployer.getExistingContract({ contract: 'SystemStatus' }); + + const systemSuspensionStatus = await oldSystemStatus.methods.systemSuspension().call(); + + systemSuspended = systemSuspensionStatus.suspended; + systemSuspendedReason = systemSuspensionStatus.reason; + } catch (err) { + if (!freshDeploy) { + console.error( + red( + 'Cannot connect to existing SystemStatus contract. Please double check the deploymentPath is correct for the network allocated' + ) + ); + throw Error('Cannot deploy. Halted.'); + } + } + + for (const address of [account, oracleAddress]) { + if (!isAddress(address)) { + console.error(red('Invalid address detected (please check your inputs):', address)); + process.exitCode = 1; + process.exit(); + } + } + + const newSynthsToAdd = synths + .filter(({ name }) => !config[`Synth${name}`]) + .map(({ name }) => name); + + let aggregatedPriceResults = 'N/A'; + + if (oldExrates && network !== 'local' && !skipFeedChecks) { + const padding = '\n\t\t\t\t'; + const aggResults = await checkAggregatorPrices({ + network, + useOvm, + providerUrl, + synths, + oldExrates, + standaloneFeeds, + }); + aggregatedPriceResults = padding + aggResults.join(padding); + } + + const deployerBalance = parseInt( + formatUnits(await deployer.provider.web3.eth.getBalance(account), 'ether'), + 10 + ); + if (useFork) { + // Make sure the pwned account has ETH when using a fork + const accounts = await deployer.provider.web3.eth.getAccounts(); + + await deployer.provider.web3.eth.sendTransaction({ + from: accounts[0], + to: account, + value: parseUnits('10', 'ether').toString(), + }); + } else if (deployerBalance < 5) { + console.log( + yellow(`⚠ WARNING: Deployer account balance could be too low: ${deployerBalance} ETH`) + ); + } + + let ovmDeploymentPathWarning = false; + // OVM targets must end with '-ovm'. + if (useOvm) { + const lastPathElement = path.basename(deploymentPath); + ovmDeploymentPathWarning = !lastPathElement.includes('ovm'); + } + + // now get the latest time a Solidity file was edited + const latestSolTimestamp = getLatestSolTimestamp(CONTRACTS_FOLDER); + + parameterNotice({ + 'Dry Run': dryRun ? green('true') : yellow('⚠ NO'), + 'Using a fork': useFork ? green('true') : yellow('⚠ NO'), + Concurrency: `${concurrency} max parallel calls`, + Network: network, + 'OVM?': useOvm + ? ovmDeploymentPathWarning + ? red('⚠ No -ovm folder suffix!') + : green('true') + : 'false', + 'Gas price to use': `${gasPrice} GWEI`, + 'Method call gas limit': `${methodCallGasLimit} gas`, + 'Contract deployment gas limit': `${contractDeploymentGasLimit} gas`, + 'Deployment Path': new RegExp(network, 'gi').test(deploymentPath) + ? deploymentPath + : yellow('⚠⚠⚠ cant find network name in path. Please double check this! ') + deploymentPath, + Provider: providerUrl, + 'Local build last modified': `${new Date(earliestCompiledTimestamp)} ${yellow( + ((new Date().getTime() - earliestCompiledTimestamp) / 60000).toFixed(2) + ' mins ago' + )}`, + 'Last Solidity update': + new Date(latestSolTimestamp) + + (latestSolTimestamp > earliestCompiledTimestamp + ? yellow(' ⚠⚠⚠ this is later than the last build! Is this intentional?') + : green(' ✅')), + 'Add any new synths found?': addNewSynths + ? green('✅ YES\n\t\t\t\t') + newSynthsToAdd.join(', ') + : yellow('⚠ NO'), + 'Deployer account:': account, + 'Synthetix totalSupply': `${Math.round(formatUnits(currentSynthetixSupply) / 1e6)}m`, + 'ExchangeRates Oracle': oracleExrates, + 'Last Mint Event': `${currentLastMintEvent} (${new Date(currentLastMintEvent * 1000)})`, + 'Current Weeks Of Inflation': currentWeekOfInflation, + 'Aggregated Prices': aggregatedPriceResults, + 'System Suspended': systemSuspended + ? green(' ✅', 'Reason:', systemSuspendedReason) + : yellow('⚠ NO'), + }); + + if (!yes) { + try { + await confirmAction( + yellow( + `⚠⚠⚠ WARNING: This action will deploy the following contracts to ${network}:\n${Object.entries( + config + ) + .filter(([, { deploy }]) => deploy) + .map(([contract]) => contract) + .join(', ')}` + `\nIt will also set proxy targets and add synths to Synthetix.\n` + ) + + gray('-'.repeat(50)) + + '\nDo you want to continue? (y/n) ' + ); + } catch (err) { + console.log(gray('Operation cancelled')); + throw Error('Halted.'); + } + } + + return { + currentSynthetixSupply, + currentLastMintEvent, + currentWeekOfInflation, + oldExrates, + oracleAddress, + }; +}; diff --git a/publish/src/commands/deploy/take-debt-snapshot-when-required.js b/publish/src/commands/deploy/take-debt-snapshot-when-required.js new file mode 100644 index 0000000000..00cb098ad5 --- /dev/null +++ b/publish/src/commands/deploy/take-debt-snapshot-when-required.js @@ -0,0 +1,97 @@ +'use strict'; + +const { gray, yellow, red } = require('chalk'); +const { + utils: { formatUnits }, +} = require('ethers'); + +module.exports = async ({ debtSnapshotMaxDeviation, deployer, runStep, useOvm }) => { + const { DebtCache } = deployer.deployedContracts; + + console.log(gray(`\n------ CHECKING DEBT CACHE ------\n`)); + + const refreshSnapshotIfPossible = async (wasInvalid, isInvalid, force = false) => { + const validityChanged = wasInvalid !== isInvalid; + + if (force || validityChanged) { + console.log(yellow(`Refreshing debt snapshot...`)); + await runStep({ + gasLimit: useOvm ? 4.0e6 : 5.0e6, // About 3.34 million gas is required to refresh the snapshot with ~40 synths on L1 + contract: 'DebtCache', + target: DebtCache, + write: 'takeDebtSnapshot', + writeArg: [], + publiclyCallable: true, // does not require owner + }); + } else if (!validityChanged) { + console.log( + red('⚠⚠⚠ WARNING: Deployer attempted to refresh the debt cache, but it cannot be.') + ); + } + }; + + const checkSnapshot = async () => { + const [cacheInfo, currentDebt] = await Promise.all([ + DebtCache.methods.cacheInfo().call(), + DebtCache.methods.currentDebt().call(), + ]); + + // Check if the snapshot is stale and can be fixed. + if (cacheInfo.isStale && !currentDebt.anyRateIsInvalid) { + console.log(yellow('Debt snapshot is stale, and can be refreshed.')); + await refreshSnapshotIfPossible( + cacheInfo.isInvalid, + currentDebt.anyRateIsInvalid, + cacheInfo.isStale + ); + return true; + } + + // Otherwise, if the rates are currently valid, + // we might still need to take a snapshot due to invalidity or deviation. + if (!currentDebt.anyRateIsInvalid) { + if (cacheInfo.isInvalid) { + console.log(yellow('Debt snapshot is invalid, and can be refreshed.')); + await refreshSnapshotIfPossible( + cacheInfo.isInvalid, + currentDebt.anyRateIsInvalid, + cacheInfo.isStale + ); + return true; + } else { + const cachedDebtEther = formatUnits(cacheInfo.debt); + const currentDebtEther = formatUnits(currentDebt.debt); + const deviation = + (Number(currentDebtEther) - Number(cachedDebtEther)) / Number(cachedDebtEther); + const maxDeviation = debtSnapshotMaxDeviation; + + if (maxDeviation <= Math.abs(deviation)) { + console.log( + yellow( + `Debt cache deviation is ${deviation * 100}% >= ${maxDeviation * + 100}%; refreshing it...` + ) + ); + await refreshSnapshotIfPossible(cacheInfo.isInvalid, currentDebt.anyRateIsInvalid, true); + return true; + } + } + } + + // Finally, if the debt cache is currently valid, but needs to be invalidated, we will also perform a snapshot. + if (!cacheInfo.isInvalid && currentDebt.anyRateIsInvalid) { + console.log(yellow('Debt snapshot needs to be invalidated.')); + await refreshSnapshotIfPossible(cacheInfo.isInvalid, currentDebt.anyRateIsInvalid, false); + return true; + } + return false; + }; + + const performedSnapshot = await checkSnapshot(); + + if (performedSnapshot) { + console.log(gray('Snapshot complete.')); + } else { + console.log(gray('No snapshot required.')); + } +}; diff --git a/publish/src/util.js b/publish/src/util.js index d267475e22..6c9b9d0ab4 100644 --- a/publish/src/util.js +++ b/publish/src/util.js @@ -59,7 +59,7 @@ const ensureDeploymentPath = deploymentPath => { }; // Load up all contracts in the flagged source, get their deployed addresses (if any) and compiled sources -const loadAndCheckRequiredSources = ({ deploymentPath, network }) => { +const loadAndCheckRequiredSources = ({ deploymentPath, network, freshDeploy }) => { console.log(gray(`Loading the list of synths for ${network.toUpperCase()}...`)); const synthsFile = path.join(deploymentPath, SYNTHS_FILENAME); const synths = getSynths({ network, deploymentPath }); @@ -97,6 +97,11 @@ const loadAndCheckRequiredSources = ({ deploymentPath, network }) => { } const deployment = JSON.parse(fs.readFileSync(deploymentFile)); + if (freshDeploy) { + deployment.targets = {}; + deployment.sources = {}; + } + const ownerActionsFile = path.join(deploymentPath, OWNER_ACTIONS_FILENAME); if (!fs.existsSync(ownerActionsFile)) { fs.writeFileSync(ownerActionsFile, stringify({}));