UNPKG

synthetix

Version:

The smart contracts which make up the Synthetix system. (synthetix.io)

582 lines (526 loc) 17 kB
'use strict'; const w3utils = require('web3-utils'); const abiDecoder = require('abi-decoder'); // load the data in explicitly (not programmatically) so webpack knows what to bundle const data = { kovan: require('./publish/deployed/kovan'), rinkeby: require('./publish/deployed/rinkeby'), ropsten: require('./publish/deployed/ropsten'), mainnet: require('./publish/deployed/mainnet'), goerli: require('./publish/deployed/goerli'), 'goerli-ovm': require('./publish/deployed/goerli-ovm'), 'kovan-ovm': require('./publish/deployed/kovan-ovm'), 'mainnet-ovm': require('./publish/deployed/mainnet-ovm'), }; const assets = require('./publish/assets.json'); const ovmIgnored = require('./publish/ovm-ignore.json'); const nonUpgradeable = require('./publish/non-upgradeable.json'); const releases = require('./publish/releases.json'); const networks = ['local', 'kovan', 'rinkeby', 'ropsten', 'mainnet', 'goerli']; const networkToChainId = { mainnet: 1, ropsten: 3, rinkeby: 4, goerli: 5, kovan: 42, }; const constants = { BUILD_FOLDER: 'build', CONTRACTS_FOLDER: 'contracts', COMPILED_FOLDER: 'compiled', FLATTENED_FOLDER: 'flattened', AST_FOLDER: 'ast', CONFIG_FILENAME: 'config.json', PARAMS_FILENAME: 'params.json', SYNTHS_FILENAME: 'synths.json', STAKING_REWARDS_FILENAME: 'rewards.json', OWNER_ACTIONS_FILENAME: 'owner-actions.json', DEPLOYMENT_FILENAME: 'deployment.json', VERSIONS_FILENAME: 'versions.json', FEEDS_FILENAME: 'feeds.json', AST_FILENAME: 'asts.json', ZERO_ADDRESS: '0x' + '0'.repeat(40), OVM_MAX_GAS_LIMIT: '8999999', inflationStartTimestampInSecs: 1551830400, // 2019-03-06T00:00:00Z }; const knownAccounts = { mainnet: [ { name: 'binance', // Binance 8 Wallet address: '0xF977814e90dA44bFA03b6295A0616a897441aceC', }, { name: 'renBTCWallet', address: '0x53463cd0b074E5FDafc55DcE7B1C82ADF1a43B2E', }, { name: 'loansAccount', address: '0x62f7A1F94aba23eD2dD108F8D23Aa3e7d452565B', }, ], rinkeby: [], kovan: [], }; // The solidity defaults are managed here in the same format they will be stored, hence all // numbers are converted to strings and those with 18 decimals are also converted to wei amounts const defaults = { WAITING_PERIOD_SECS: (60 * 5).toString(), // 5 mins PRICE_DEVIATION_THRESHOLD_FACTOR: w3utils.toWei('3'), TRADING_REWARDS_ENABLED: false, ISSUANCE_RATIO: w3utils .toBN(1) .mul(w3utils.toBN(1e18)) .div(w3utils.toBN(6)) .toString(), // 1/6 = 0.16666666667 FEE_PERIOD_DURATION: (3600 * 24 * 7).toString(), // 1 week TARGET_THRESHOLD: '1', // 1% target threshold (it will be converted to a decimal when set) LIQUIDATION_DELAY: (3600 * 24 * 3).toString(), // 3 days LIQUIDATION_RATIO: w3utils.toWei('0.5'), // 200% cratio LIQUIDATION_PENALTY: w3utils.toWei('0.1'), // 10% penalty RATE_STALE_PERIOD: (3600 * 25).toString(), // 25 hours EXCHANGE_FEE_RATES: { forex: w3utils.toWei('0.003'), commodity: w3utils.toWei('0.003'), equities: w3utils.toWei('0.003'), crypto: w3utils.toWei('0.01'), index: w3utils.toWei('0.01'), }, MINIMUM_STAKE_TIME: (3600 * 24).toString(), // 1 days DEBT_SNAPSHOT_STALE_TIME: (43800).toString(), // 12 hour heartbeat + 10 minutes mining time AGGREGATOR_WARNING_FLAGS: { mainnet: '0x4A5b9B4aD08616D11F3A402FF7cBEAcB732a76C6', kovan: '0x6292aa9a6650ae14fbf974e5029f36f95a1848fd', }, RENBTC_ERC20_ADDRESSES: { mainnet: '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', kovan: '0x9B2fE385cEDea62D839E4dE89B0A23EF4eacC717', rinkeby: '0xEDC0C23864B041607D624E2d9a67916B6cf40F7a', }, INITIAL_ISSUANCE: w3utils.toWei(`${100e6}`), CROSS_DOMAIN_DEPOSIT_GAS_LIMIT: `${3e6}`, CROSS_DOMAIN_ESCROW_GAS_LIMIT: `${8e6}`, CROSS_DOMAIN_REWARD_GAS_LIMIT: `${3e6}`, CROSS_DOMAIN_WITHDRAWAL_GAS_LIMIT: `${3e6}`, COLLATERAL_MANAGER: { SYNTHS: ['sUSD', 'sBTC', 'sETH'], SHORTS: [ { long: 'sBTC', short: 'iBTC' }, { long: 'sETH', short: 'iETH' }, ], MAX_DEBT: w3utils.toWei('20000000'), // 20 million sUSD BASE_BORROW_RATE: Math.round((0.005 * 1e18) / 31556926).toString(), // 31556926 is CollateralManager seconds per year BASE_SHORT_RATE: Math.round((0.005 * 1e18) / 31556926).toString(), }, COLLATERAL_ETH: { SYNTHS: ['sUSD', 'sETH'], MIN_CRATIO: w3utils.toWei('1.3'), MIN_COLLATERAL: w3utils.toWei('2'), ISSUE_FEE_RATE: w3utils.toWei('0.001'), }, COLLATERAL_RENBTC: { SYNTHS: ['sUSD', 'sBTC'], MIN_CRATIO: w3utils.toWei('1.3'), MIN_COLLATERAL: w3utils.toWei('0.05'), ISSUE_FEE_RATE: w3utils.toWei('0.001'), }, COLLATERAL_SHORT: { SYNTHS: ['sBTC', 'sETH'], MIN_CRATIO: w3utils.toWei('1.2'), MIN_COLLATERAL: w3utils.toWei('1000'), ISSUE_FEE_RATE: w3utils.toWei('0.005'), INTERACTION_DELAY: '3600', // 1 hour in secs }, }; /** * Converts a string into a hex representation of bytes32, with right padding */ const toBytes32 = key => w3utils.rightPad(w3utils.asciiToHex(key), 64); const getFolderNameForNetwork = ({ network, useOvm = false }) => { if (network.includes('ovm')) { return network; } return useOvm ? `${network}-ovm` : network; }; const getPathToNetwork = ({ network = 'mainnet', file = '', useOvm = false, path } = {}) => path.join(__dirname, 'publish', 'deployed', getFolderNameForNetwork({ network, useOvm }), file); // Pass in fs and path to avoid webpack wrapping those const loadDeploymentFile = ({ network, path, fs, deploymentPath, useOvm = false }) => { if (!deploymentPath && network !== 'local' && (!path || !fs)) { return data[getFolderNameForNetwork({ network, useOvm })].deployment; } const pathToDeployment = deploymentPath ? path.join(deploymentPath, constants.DEPLOYMENT_FILENAME) : getPathToNetwork({ network, useOvm, path, file: constants.DEPLOYMENT_FILENAME }); if (!fs.existsSync(pathToDeployment)) { throw Error(`Cannot find deployment for network: ${network}.`); } return JSON.parse(fs.readFileSync(pathToDeployment)); }; /** * Retrieve the list of targets for the network - returning the name, address, source file and link to etherscan */ const getTarget = ({ network = 'mainnet', useOvm = false, contract, path, fs, deploymentPath, } = {}) => { const deployment = loadDeploymentFile({ network, useOvm, path, fs, deploymentPath }); if (contract) return deployment.targets[contract]; else return deployment.targets; }; /** * Retrieve the list of solidity sources for the network - returning the abi and bytecode */ const getSource = ({ network = 'mainnet', useOvm = false, contract, path, fs, deploymentPath, } = {}) => { const deployment = loadDeploymentFile({ network, useOvm, path, fs, deploymentPath }); if (contract) return deployment.sources[contract]; else return deployment.sources; }; /** * Retrieve the ASTs for the source contracts */ const getAST = ({ source, path, fs, match = /^contracts\// } = {}) => { let fullAST; if (path && fs) { const pathToAST = path.resolve( __dirname, constants.BUILD_FOLDER, constants.AST_FOLDER, constants.AST_FILENAME ); if (!fs.existsSync(pathToAST)) { throw Error('Cannot find AST'); } fullAST = JSON.parse(fs.readFileSync(pathToAST)); } else { // Note: The below cannot be required as the build folder is not stored // in code (only in the published module). // The solution involves tracking these after each commit in another file // somewhere persisted in the codebase - JJM // data.ast = require('./build/ast/asts.json'), if (!data.ast) { throw Error('AST currently not supported in browser mode'); } fullAST = data.ast; } // remove anything not matching the pattern const ast = Object.entries(fullAST) .filter(([astEntryKey]) => match.test(astEntryKey)) .reduce((memo, [key, val]) => { memo[key] = val; return memo; }, {}); if (source && source in ast) { return ast[source]; } else if (source) { // try to find the source without a path const [key, entry] = Object.entries(ast).find(([astEntryKey]) => astEntryKey.includes('/' + source)) || []; if (!key || !entry) { throw Error(`Cannot find AST entry for source: ${source}`); } return { [key]: entry }; } else { return ast; } }; const getFeeds = ({ network, path, fs, deploymentPath, useOvm = false } = {}) => { let feeds; if (!deploymentPath && network !== 'local' && (!path || !fs)) { feeds = data[getFolderNameForNetwork({ network, useOvm })].feeds; } else { const pathToFeeds = deploymentPath ? path.join(deploymentPath, constants.FEEDS_FILENAME) : getPathToNetwork({ network, path, useOvm, file: constants.FEEDS_FILENAME, }); if (!fs.existsSync(pathToFeeds)) { throw Error(`Cannot find feeds file.`); } feeds = JSON.parse(fs.readFileSync(pathToFeeds)); } const synths = getSynths({ network, useOvm, path, fs, deploymentPath, skipPopulate: true }); // now mix in the asset data return Object.entries(feeds).reduce((memo, [asset, entry]) => { memo[asset] = Object.assign( // standalone feeds are those without a synth using them // Note: ETH still used as a rate for Depot, can remove the below once the Depot uses sETH rate or is // removed from the system { standalone: !synths.find(synth => synth.asset === asset) || asset === 'ETH' }, assets[asset], entry ); return memo; }, {}); }; /** * Retrieve ths list of synths for the network - returning their names, assets underlying, category, sign, description, and * optional index and inverse properties */ const getSynths = ({ network = 'mainnet', path, fs, deploymentPath, useOvm = false, skipPopulate = false, } = {}) => { let synths; if (!deploymentPath && network !== 'local' && (!path || !fs)) { synths = data[getFolderNameForNetwork({ network, useOvm })].synths; } else { const pathToSynthList = deploymentPath ? path.join(deploymentPath, constants.SYNTHS_FILENAME) : getPathToNetwork({ network, useOvm, path, file: constants.SYNTHS_FILENAME }); if (!fs.existsSync(pathToSynthList)) { throw Error(`Cannot find synth list.`); } synths = JSON.parse(fs.readFileSync(pathToSynthList)); } if (skipPopulate) { return synths; } const feeds = getFeeds({ network, useOvm, path, fs, deploymentPath }); // copy all necessary index parameters from the longs to the corresponding shorts return synths.map(synth => { // mixin the asset details synth = Object.assign({}, assets[synth.asset], synth); if (feeds[synth.asset]) { const { feed } = feeds[synth.asset]; synth = Object.assign({ feed }, synth); } if (synth.inverted) { synth.description = `Inverse ${synth.description}`; } // replace an index placeholder with the index details if (typeof synth.index === 'string') { const { index } = synths.find(({ name }) => name === synth.index) || {}; if (!index) { throw Error( `While processing ${synth.name}, it's index mapping "${synth.index}" cannot be found - this is an error in the deployment config and should be fixed` ); } synth = Object.assign({}, synth, { index }); } if (synth.index) { synth.index = synth.index.map(indexEntry => { return Object.assign({}, assets[indexEntry.asset], indexEntry); }); } return synth; }); }; /** * Retrieve the list of staking rewards for the network - returning this names, stakingToken, and rewardToken */ const getStakingRewards = ({ network = 'mainnet', useOvm = false, path, fs, deploymentPath, } = {}) => { if (!deploymentPath && network !== 'local' && (!path || !fs)) { return data[getFolderNameForNetwork({ network, useOvm })].rewards; } const pathToStakingRewardsList = deploymentPath ? path.join(deploymentPath, constants.STAKING_REWARDS_FILENAME) : getPathToNetwork({ network, path, useOvm, file: constants.STAKING_REWARDS_FILENAME, }); if (!fs.existsSync(pathToStakingRewardsList)) { return []; } return JSON.parse(fs.readFileSync(pathToStakingRewardsList)); }; /** * Retrieve the list of system user addresses */ const getUsers = ({ network = 'mainnet', user, useOvm = false } = {}) => { const testnetOwner = '0xB64fF7a4a33Acdf48d97dab0D764afD0F6176882'; const base = { owner: testnetOwner, deployer: testnetOwner, marketClosure: testnetOwner, oracle: '0xac1e8B385230970319906C03A1d8567e3996d1d5', fee: '0xfeEFEEfeefEeFeefEEFEEfEeFeefEEFeeFEEFEeF', zero: '0x' + '0'.repeat(40), }; const map = { mainnet: Object.assign({}, base, { owner: '0xEb3107117FEAd7de89Cd14D463D340A2E6917769', deployer: '0xDe910777C787903F78C89e7a0bf7F4C435cBB1Fe', marketClosure: '0xC105Ea57Eb434Fbe44690d7Dec2702e4a2FBFCf7', oracle: '0xaC1ED4Fabbd5204E02950D68b6FC8c446AC95362', }), kovan: Object.assign({}, base), 'kovan-ovm': Object.assign({}, base), 'mainnet-ovm': Object.assign({}, base, { owner: '0xDe910777C787903F78C89e7a0bf7F4C435cBB1Fe', }), rinkeby: Object.assign({}, base), ropsten: Object.assign({}, base), goerli: Object.assign({}, base), 'goerli-ovm': Object.assign({}, base), local: Object.assign({}, base, { // Deterministic account #0 when using `npx hardhat node` owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', }), }; const users = Object.entries( map[getFolderNameForNetwork({ network, useOvm })] ).map(([key, value]) => ({ name: key, address: value })); return user ? users.find(({ name }) => name === user) : users; }; const getVersions = ({ network = 'mainnet', path, fs, deploymentPath, useOvm, byContract = false, } = {}) => { let versions; if (!deploymentPath && network !== 'local' && (!path || !fs)) { versions = data[getFolderNameForNetwork({ network, useOvm })].versions; } else { const pathToVersions = deploymentPath ? path.join(deploymentPath, constants.VERSIONS_FILENAME) : getPathToNetwork({ network, useOvm, path, file: constants.VERSIONS_FILENAME }); if (!fs.existsSync(pathToVersions)) { throw Error(`Cannot find versions for network.`); } versions = JSON.parse(fs.readFileSync(pathToVersions)); } if (byContract) { // compile from the contract perspective return Object.values(versions).reduce( (memo, { tag, release, date, commit, block, contracts }) => { for (const [contract, contractEntry] of Object.entries(contracts)) { memo[contract] = memo[contract] || []; memo[contract].push(Object.assign({ tag, release, date, commit, block }, contractEntry)); } return memo; }, {} ); } return versions; }; const getSuspensionReasons = ({ code = undefined } = {}) => { const suspensionReasonMap = { 1: 'System Upgrade', 2: 'Market Closure', 4: 'iSynth Reprice', 55: 'Circuit Breaker (Phase one)', // https://sips.synthetix.io/SIPS/sip-55 65: 'Decentralized Circuit Breaker (Phase two)', // https://sips.synthetix.io/SIPS/sip-65 99999: 'Emergency', }; return code ? suspensionReasonMap[code] : suspensionReasonMap; }; /** * Retrieve the list of tokens used in the Synthetix protocol */ const getTokens = ({ network = 'mainnet', path, fs, useOvm = false } = {}) => { const synths = getSynths({ network, useOvm, path, fs }); const targets = getTarget({ network, useOvm, path, fs }); const feeds = getFeeds({ network, useOvm, path, fs }); return [ Object.assign( { symbol: 'SNX', asset: 'SNX', name: 'Synthetix', address: targets.ProxyERC20.address, decimals: 18, }, feeds['SNX'].feed ? { feed: feeds['SNX'].feed } : {} ), ].concat( synths .filter(({ category }) => category !== 'internal') .map(synth => ({ symbol: synth.name, asset: synth.asset, name: synth.description, address: targets[`Proxy${synth.name === 'sUSD' ? 'ERC20sUSD' : synth.name}`].address, index: synth.index, inverted: synth.inverted, decimals: 18, feed: synth.feed, })) .sort((a, b) => (a.symbol > b.symbol ? 1 : -1)) ); }; const decode = ({ network = 'mainnet', fs, path, data, target, useOvm = false } = {}) => { const sources = getSource({ network, path, fs, useOvm }); for (const { abi } of Object.values(sources)) { abiDecoder.addABI(abi); } const targets = getTarget({ network, path, fs, useOvm }); let contract; if (target) { contract = Object.values(targets).filter( ({ address }) => address.toLowerCase() === target.toLowerCase() )[0].name; } return { method: abiDecoder.decodeMethod(data), contract }; }; const wrap = ({ network, deploymentPath, fs, path, useOvm = false }) => [ 'decode', 'getAST', 'getPathToNetwork', 'getSource', 'getStakingRewards', 'getFeeds', 'getSynths', 'getTarget', 'getTokens', 'getUsers', 'getVersions', ].reduce((memo, fnc) => { memo[fnc] = (prop = {}) => module.exports[fnc](Object.assign({ network, deploymentPath, fs, path, useOvm }, prop)); return memo; }, {}); module.exports = { constants, decode, defaults, getAST, getPathToNetwork, getSource, getStakingRewards, getSuspensionReasons, getFeeds, getSynths, getTarget, getTokens, getUsers, getVersions, networks, networkToChainId, toBytes32, wrap, ovmIgnored, nonUpgradeable, releases, knownAccounts, };