synthetix
Version:
The smart contracts which make up the Synthetix system. (synthetix.io)
1,012 lines (917 loc) • 29.7 kB
JavaScript
;
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 = {
mainnet: require('./publish/deployed/mainnet'),
sepolia: require('./publish/deployed/sepolia'),
'sepolia-ovm': require('./publish/deployed/sepolia-ovm'),
'local-ovm': require('./publish/deployed/local-ovm'),
'mainnet-ovm': require('./publish/deployed/mainnet-ovm'),
};
const assets = require('./publish/assets.json');
const nonUpgradeable = require('./publish/non-upgradeable.json');
const releases = require('./publish/releases.json');
const networks = ['local', 'mainnet', 'sepolia'];
const chainIdMapping = Object.entries({
1: {
network: 'mainnet',
},
// Hardhat fork of mainnet: https://hardhat.org/config/#hardhat-network
31337: {
network: 'mainnet',
fork: true,
},
10: {
network: 'mainnet',
useOvm: true,
},
11155111: {
network: 'sepolia',
},
11155420: {
network: 'sepolia',
useOvm: true,
},
'-1': {
// no chain ID for this currently
network: 'unknown',
useOvm: true,
},
// now append any defaults
}).reduce((memo, [id, body]) => {
memo[id] = Object.assign({ useOvm: false, fork: false }, body);
return memo;
}, {});
/** @type {(obj: {id: number} | number) => number} */
const getNetworkFromId = obj => {
const id = typeof obj === 'number' ? obj : obj.id;
return chainIdMapping[id];
};
const networkToChainId = Object.entries(chainIdMapping).reduce(
(memo, [id, { network, useOvm, fork }]) => {
memo[network + (useOvm ? '-ovm' : '') + (fork ? '-fork' : '')] = id;
return memo;
},
{}
);
const constants = {
BUILD_FOLDER: 'build',
CONTRACTS_FOLDER: 'contracts',
MIGRATIONS_FOLDER: 'migrations',
COMPILED_FOLDER: 'compiled',
FLATTENED_FOLDER: 'flattened',
AST_FOLDER: 'ast',
CONFIG_FILENAME: 'config.json',
RELEASES_FILENAME: 'releases.json',
PARAMS_FILENAME: 'params.json',
SYNTHS_FILENAME: 'synths.json',
STAKING_REWARDS_FILENAME: 'rewards.json',
SHORTING_REWARDS_FILENAME: 'shorting-rewards.json',
OWNER_ACTIONS_FILENAME: 'owner-actions.json',
DEPLOYMENT_FILENAME: 'deployment.json',
VERSIONS_FILENAME: 'versions.json',
FEEDS_FILENAME: 'feeds.json',
OFFCHAIN_FEEDS_FILENAME: 'offchain-feeds.json',
FUTURES_MARKETS_FILENAME: 'futures-markets.json',
PERPS_V2_MARKETS_FILENAME: 'perpsv2-markets.json',
AST_FILENAME: 'asts.json',
ZERO_ADDRESS: '0x' + '0'.repeat(40),
ZERO_BYTES32: '0x' + '0'.repeat(64),
inflationStartTimestampInSecs: 1551830400, // 2019-03-06T00:00:00+00:00
};
const knownAccounts = {
mainnet: [
{
name: 'binance', // Binance 8 Wallet
address: '0xF977814e90dA44bFA03b6295A0616a897441aceC',
},
{
name: 'renBTCWallet', // KeeperDAO wallet (has renBTC and ETH)
address: '0x35ffd6e268610e764ff6944d07760d0efe5e40e5',
},
{
name: 'loansAccount',
address: '0x62f7A1F94aba23eD2dD108F8D23Aa3e7d452565B',
},
],
};
// 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 = {
TEMP_OWNER_DEFAULT_DURATION: 60 * 60 * 24 * 60, // 60 days
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(3))
.toString(), // 1/3 = 0.3333333333 // 300% ratio
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).toString(), // 24 hours
LIQUIDATION_RATIO: w3utils
.toBN(1)
.mul(w3utils.toBN(2e18))
.div(w3utils.toBN(3))
.toString(), // 2/3 = 0.6666666667 // 150% ratio
LIQUIDATION_ESCROW_DURATION: (3600 * 24 * 365).toString(), // 1 year
LIQUIDATION_PENALTY: w3utils.toWei('0.1'), // 10% penalty (used for Collateral liquidations)
SNX_LIQUIDATION_PENALTY: w3utils.toWei('0.3'), // 30% penalty (used for SNX Liquidations)
SELF_LIQUIDATION_PENALTY: w3utils.toWei('0.2'), // 20% penalty
FLAG_REWARD: w3utils.toWei('10'), // 10 SNX
LIQUIDATE_REWARD: w3utils.toWei('20'), // 20 SNX
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'),
},
EXCHANGE_DYNAMIC_FEE_THRESHOLD: w3utils.toWei('0.0025'),
EXCHANGE_DYNAMIC_FEE_WEIGHT_DECAY: w3utils.toWei('0.95'), // dynamic fee weight decay for each round
EXCHANGE_DYNAMIC_FEE_ROUNDS: '6', // dynamic fee rounds
EXCHANGE_MAX_DYNAMIC_FEE: w3utils.toWei('0.015'), // cap max dynamic fee
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',
},
RENBTC_ERC20_ADDRESSES: {
mainnet: '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D',
// Adding zero addresses here - we don't actually support renBTC anymore.
'mainnet-ovm': '0x0000000000000000000000000000000000000000',
sepolia: '0x0000000000000000000000000000000000000000',
'sepolia-ovm': '0x0000000000000000000000000000000000000000',
},
WETH_ERC20_ADDRESSES: {
mainnet: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
sepolia: '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9',
'mainnet-ovm': '0x4200000000000000000000000000000000000006',
'sepolia-ovm': '0x4200000000000000000000000000000000000006',
},
INITIAL_ISSUANCE: w3utils.toWei(`${100e6}`),
CROSS_DOMAIN_DEPOSIT_GAS_LIMIT: `${3e6}`,
CROSS_DOMAIN_ESCROW_GAS_LIMIT: `${8e6}`,
CROSS_DOMAIN_REWARD_GAS_LIMIT: `${8e6}`,
CROSS_DOMAIN_WITHDRAWAL_GAS_LIMIT: `${3e6}`,
CROSS_DOMAIN_RELAY_GAS_LIMIT: `${8e6}`,
CROSS_DOMAIN_FEE_PERIOD_CLOSE_GAS_LIMIT: `${8e6}`,
COLLATERAL_MANAGER: {
SYNTHS: ['sUSD', 'sBTC', 'sETH'],
SHORTS: ['sBTC', 'sETH'],
MAX_DEBT: w3utils.toWei('75000000'), // 75 million sUSD
MAX_SKEW_RATE: w3utils.toWei('0.2'),
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
COLLAPSE_FEE_RATE: '0',
},
ETHER_WRAPPER_MAX_ETH: w3utils.toWei('5000'),
ETHER_WRAPPER_MINT_FEE_RATE: w3utils.toWei('0.005'), // 5 bps
ETHER_WRAPPER_BURN_FEE_RATE: '0',
FUTURES_MIN_KEEPER_FEE: w3utils.toWei('1'), // 1 sUSD min keeper fee
FUTURES_MAX_KEEPER_FEE: w3utils.toWei('1000'), // 1000 sUSD min keeper fee
FUTURES_LIQUIDATION_FEE_RATIO: w3utils.toWei('0.0035'), // 35 basis points liquidation incentive
FUTURES_LIQUIDATION_BUFFER_RATIO: w3utils.toWei('0.0025'), // 25 basis points liquidation buffer
FUTURES_MIN_INITIAL_MARGIN: w3utils.toWei('40'), // minimum initial margin for all markets
PERPSV2_KEEPER_LIQUIDATION_FEE: w3utils.toWei('2'), // 2 sUSD keeper liquidation fee (not flagger)
// SIP-120
ATOMIC_MAX_VOLUME_PER_BLOCK: w3utils.toWei(`${2e5}`), // 200k
ATOMIC_TWAP_WINDOW: '1800', // 30 mins
};
/**
* Converts a string into a hex representation of bytes32, with right padding
*/
const toBytes32 = key => w3utils.rightPad(w3utils.asciiToHex(key), 64);
const fromBytes32 = key => w3utils.hexToAscii(key);
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 = 'mainnet', path, fs, deploymentPath, useOvm = false }) => {
if (!deploymentPath && (!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 && (!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));
}
// now mix in the asset data
return Object.entries(feeds).reduce((memo, [asset, entry]) => {
memo[asset] = Object.assign(assets[asset], entry);
return memo;
}, {});
};
const getOffchainFeeds = ({ network, path, fs, deploymentPath, useOvm = false } = {}) => {
if (!deploymentPath && (!path || !fs)) {
return data[getFolderNameForNetwork({ network, useOvm })].offchainFeeds;
} else {
const pathToFeeds = deploymentPath
? path.join(deploymentPath, constants.OFFCHAIN_FEEDS_FILENAME)
: getPathToNetwork({
network,
path,
useOvm,
file: constants.OFFCHAIN_FEEDS_FILENAME,
});
if (!fs.existsSync(pathToFeeds)) {
throw Error(`Cannot find off-chain feeds file.`);
}
return JSON.parse(fs.readFileSync(pathToFeeds));
}
};
/**
* 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 && (!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);
}
// 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;
});
};
const getFuturesMarkets = ({
network = 'mainnet',
useOvm = false,
path,
fs,
deploymentPath,
} = {}) => {
let futuresMarkets;
if (!deploymentPath && (!path || !fs)) {
futuresMarkets = data[getFolderNameForNetwork({ network, useOvm })].futuresMarkets;
} else {
const pathToFuturesMarketsList = deploymentPath
? path.join(deploymentPath, constants.FUTURES_MARKETS_FILENAME)
: getPathToNetwork({
network,
path,
useOvm,
file: constants.FUTURES_MARKETS_FILENAME,
});
if (!fs.existsSync(pathToFuturesMarketsList)) {
futuresMarkets = [];
} else {
futuresMarkets = JSON.parse(fs.readFileSync(pathToFuturesMarketsList)) || [];
}
}
return futuresMarkets.map(futuresMarket => {
/**
* We expect the asset key to not start with an 's'. ie. AVAX rather than sAVAX
* Unfortunately due to some historical reasons 'sBTC', 'sETH' and 'sLINK' does not follow this format
* We adjust for that here.
*/
const marketsWithIncorrectAssetKey = ['sBTC', 'sETH', 'sLINK'];
const assetKeyNeedsAdjustment = marketsWithIncorrectAssetKey.includes(futuresMarket.asset);
const assetKey = assetKeyNeedsAdjustment ? futuresMarket.asset.slice(1) : futuresMarket.asset;
// mixin the asset details
return Object.assign({}, assets[assetKey], futuresMarket);
});
};
const getPerpsMarkets = ({
network = 'mainnet',
useOvm = false,
path,
fs,
deploymentPath,
} = {}) => {
let perpsMarkets;
if (!deploymentPath && (!path || !fs)) {
perpsMarkets = data[getFolderNameForNetwork({ network, useOvm })].perpsv2Markets;
} else {
const pathToPerpsMarketsList = deploymentPath
? path.join(deploymentPath, constants.PERPS_V2_MARKETS_FILENAME)
: getPathToNetwork({
network,
path,
useOvm,
file: constants.PERPS_V2_MARKETS_FILENAME,
});
if (!fs.existsSync(pathToPerpsMarketsList)) {
perpsMarkets = [];
} else {
perpsMarkets = JSON.parse(fs.readFileSync(pathToPerpsMarketsList)) || [];
}
}
return perpsMarkets.map(perpsMarket => {
/**
* We expect the asset key to not start with an 's'. ie. AVAX rather than sAVAX
* Unfortunately due to some historical reasons 'sBTC' and 'sETH' does not follow this format
* We adjust for that here.
*/
const marketsWithIncorrectAssetKey = ['sBTC', 'sETH'];
const assetKeyNeedsAdjustment = marketsWithIncorrectAssetKey.includes(perpsMarket.asset);
const assetKey = assetKeyNeedsAdjustment ? perpsMarket.asset.slice(1) : perpsMarket.asset;
// mixin the asset details
return Object.assign({}, assets[assetKey], perpsMarket);
});
};
const getPerpsV2ProxiedMarkets = ({ network = 'mainnet', fs, deploymentPath, path }) => {
const _analyzeAndIncludePerpsV2 = (target, targetData, sourceData, PerpsV2Proxied) => {
const proxyPrefix = 'PerpsV2Proxy';
const marketPrefix = 'PerpsV2Market';
const excludedContracts = ['PerpsV2MarketSettings', 'PerpsV2MarketData', 'PerpsV2ExchangeRate'];
const excludedLegacyContracts = ['PerpsV2DelayedOrder', 'PerpsV2OffchainDelayedOrder'];
const prefixes = [
'PerpsV2MarketViews',
'PerpsV2DelayedIntent',
'PerpsV2DelayedExecution',
'PerpsV2MarketLiquidate',
];
if (
excludedContracts.includes(target) ||
target.startsWith('PerpsV2MarketState') ||
excludedLegacyContracts.some(prefix => target.startsWith(prefix))
) {
// Markets helper or Market state. Do nothing
return;
}
// If is the proxy, get the address. Initialize object if not done yet
if (target.startsWith(proxyPrefix)) {
// get name
const marketName = target.slice(proxyPrefix.length);
if (!PerpsV2Proxied[marketName]) {
PerpsV2Proxied[marketName] = {};
PerpsV2Proxied[marketName].abi = [];
}
// get address
PerpsV2Proxied[marketName].address = targetData.address;
} else {
// Not proxy, is one of the components. First try with the long contract names because main component prefix is included in others
let nameFound = false;
let marketName;
// Identify the market name (after the prefix)
for (const prefix of prefixes) {
if (target.startsWith(prefix)) {
// get name
marketName = target.slice(prefix.length);
nameFound = true;
}
}
// if not found one the previous step, it should be PerpsV2MarketXXXXX
if (!nameFound) {
if (target.startsWith(marketPrefix)) {
// get name
marketName = target.slice(marketPrefix.length);
nameFound = true;
}
}
if (nameFound) {
// Initialize if not done yet
if (!PerpsV2Proxied[marketName]) {
PerpsV2Proxied[marketName] = {};
PerpsV2Proxied[marketName].abi = [];
}
// add fragments to abi
_consolidateAbi(sourceData.abi, PerpsV2Proxied[marketName].abi);
}
}
};
const _consolidateAbi = (currentAbi, consolidatedAbi) => {
for (const abiFragment of currentAbi) {
if (
!consolidatedAbi.find(
f =>
f.type === abiFragment.type && f.name && abiFragment.name && f.name === abiFragment.name
)
) {
if (abiFragment.type !== 'constructor') {
// don't push constructors to the consolidated abi
consolidatedAbi.push(abiFragment);
}
}
}
};
const deploymentData = loadDeploymentFile({ network, useOvm: false, path, fs, deploymentPath });
const targets = Object.keys(deploymentData.targets);
const PerpsV2Proxied = {};
for (const target of targets) {
if (!target.startsWith('PerpsV2')) {
continue;
}
const targetData = getTarget({
contract: target,
network,
useOvm: false,
path,
fs,
deploymentPath,
});
const sourceData = getSource({
contract: targetData.source,
network,
useOvm: false,
path,
fs,
deploymentPath,
});
_analyzeAndIncludePerpsV2(target, targetData, sourceData, PerpsV2Proxied);
}
return PerpsV2Proxied;
};
/**
* 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 && (!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 shorting rewards for the network - returning the names and rewardTokens
*/
const getShortingRewards = ({
network = 'mainnet',
useOvm = false,
path,
fs,
deploymentPath,
} = {}) => {
if (!deploymentPath && (!path || !fs)) {
return data[getFolderNameForNetwork({ network, useOvm })]['shorting-rewards'];
}
const pathToShortingRewardsList = deploymentPath
? path.join(deploymentPath, constants.SHORTING_REWARDS_FILENAME)
: getPathToNetwork({
network,
path,
useOvm,
file: constants.SHORTING_REWARDS_FILENAME,
});
if (!fs.existsSync(pathToShortingRewardsList)) {
return [];
}
return JSON.parse(fs.readFileSync(pathToShortingRewardsList));
};
/**
* Retrieve the list of system user addresses
*/
const getUsers = ({ network = 'mainnet', user, useOvm = false } = {}) => {
const testnetOwner = '0x48914229deDd5A9922f44441ffCCfC2Cb7856Ee9';
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: '0xEde8a407913A874Dd7e3d5B731AFcA135D30375E',
marketClosure: '0xC105Ea57Eb434Fbe44690d7Dec2702e4a2FBFCf7',
oracle: '0xaC1ED4Fabbd5204E02950D68b6FC8c446AC95362',
}),
'mainnet-ovm': Object.assign({}, base, {
owner: '0x6d4a64C57612841c2C6745dB2a4E4db34F002D20',
deployer: '0xEde8a407913A874Dd7e3d5B731AFcA135D30375E',
}),
sepolia: Object.assign({}, base, {
owner: '0x48914229deDd5A9922f44441ffCCfC2Cb7856Ee9',
deployer: '0x48914229deDd5A9922f44441ffCCfC2Cb7856Ee9',
}),
'sepolia-ovm': Object.assign({}, base, {
owner: '0x48914229deDd5A9922f44441ffCCfC2Cb7856Ee9',
deployer: '0x48914229deDd5A9922f44441ffCCfC2Cb7856Ee9',
}),
local: Object.assign({}, base, {
// Deterministic account #0 when using `npx hardhat node`
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
}),
'local-ovm': Object.assign({}, base, {
// Deterministic account #0 when using `npx hardhat node`
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
deployer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
oracle: '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 && (!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',
6: 'Index Rebalance',
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
80: 'Futures configuration', // pausing according to deployment configuration
231: 'Latency Breaker', // https://sips.synthetix.io/sips/sip-231/
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.ProxySynthetix.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}`] || {}).address,
index: synth.index,
decimals: 18,
feed: synth.feed,
}))
.sort((a, b) => (a.symbol > b.symbol ? 1 : -1))
);
};
const enhanceDecodedData = decoded => {
const decodedBytes32 = p => {
try {
return { ascii: fromBytes32(p).replaceAll('\x00', '') };
} catch (e) {
return { ascii: '\\error decoding\\' };
}
};
const formatDecimals = number => {
const exp = /(\d)(?=(\d{3})+(?!\d))/g;
const rep = '$1,';
return number.toString().replace(exp, rep);
};
const decodeUint = p => {
try {
const value = w3utils.toBN(p);
return {
bp: value.div(w3utils.toBN(1e14)).toString(),
decimal: formatDecimals(value.div(w3utils.toBN(1e18)).toString()),
number: formatDecimals(value.toString()),
};
} catch (e) {
return { ascii: '\\error decoding\\' };
}
};
const enhancedParams = decoded.method.params.map(p => {
if (p.type === 'bytes32') {
return { ...p, enhanced: decodedBytes32(p.value) };
}
if (p.type === 'bytes32[]') {
p.value = p.value.map(original => {
return { original, enhanced: decodedBytes32(original) };
});
}
if (/u?int[1-3][0-9]?./.test(p.type)) {
return { ...p, enhanced: decodeUint(p.value) };
}
if (p.type === 'tuple') {
const keys = Object.keys(p.value).filter(v => isNaN(v));
const values = [];
for (const key of keys) {
if (p.value[key].startsWith('0x')) {
if (p.value[key].length === 66) {
values[key] = { original: p.value[key], enhanced: decodedBytes32(p.value[key]) };
continue;
}
values[key] = p.value[key];
continue;
}
values[key] = { original: p.value[key], enhanced: decodeUint(p.value[key]) };
}
p.value = values;
}
return p;
});
const enhancedMethod = { ...decoded.method, params: enhancedParams };
return { ...decoded, method: enhancedMethod };
};
const decode = ({
network = 'mainnet',
fs,
path,
data,
target,
useOvm = false,
decodeMigration = false,
enhanceDecode = false,
} = {}) => {
const sources = getSource({ network, path, fs, useOvm });
for (const { abi } of Object.values(sources)) {
abiDecoder.addABI(abi);
}
if (decodeMigration) {
abiDecoder.addABI([
{
constant: false,
inputs: [],
name: 'migrate',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
]);
}
const targets = getTarget({ network, path, fs, useOvm });
let contract;
if (target) {
contract = Object.values(targets).filter(
({ address }) => address.toLowerCase() === target.toLowerCase()
)[0].name;
}
const result = { method: abiDecoder.decodeMethod(data), contract };
return enhanceDecode ? enhanceDecodedData(result) : result;
};
const wrap = ({ network, deploymentPath, fs, path, useOvm = false }) =>
[
'decode',
'getAST',
'getPathToNetwork',
'getSource',
'getStakingRewards',
'getShortingRewards',
'getFeeds',
'getOffchainFeeds',
'getSynths',
'getTarget',
'getFuturesMarkets',
'getPerpsMarkets',
'getPerpsV2ProxiedMarkets',
'getTokens',
'getUsers',
'getVersions',
].reduce((memo, fnc) => {
memo[fnc] = (prop = {}) =>
module.exports[fnc](Object.assign({ network, deploymentPath, fs, path, useOvm }, prop));
return memo;
}, {});
const getNextRelease = ({ useOvm }) => {
const release = releases.releases.find(({ released, ovm }) => !released && (useOvm ? ovm : !ovm));
return Object.assign({}, release, { releaseName: release.name.replace(/[^\w]/g, '') });
};
module.exports = {
chainIdMapping,
constants,
decode,
defaults,
getAST,
getNetworkFromId,
getNextRelease,
getPathToNetwork,
getSource,
getStakingRewards,
getShortingRewards,
getSuspensionReasons,
getFeeds,
getOffchainFeeds,
getSynths,
getFuturesMarkets,
getPerpsMarkets,
getPerpsV2ProxiedMarkets,
getTarget,
getTokens,
getUsers,
getVersions,
networks,
networkToChainId,
toBytes32,
fromBytes32,
wrap,
nonUpgradeable,
releases,
knownAccounts,
};