Add allowance/block lists support to monitor (#477)

This commit is contained in:
Kirill Fedoseev 2020-10-29 11:25:43 +03:00 committed by GitHub
parent 5fa9d21246
commit f8d85b14de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 195 additions and 85 deletions

@ -8,11 +8,16 @@
**/docs
**/*.md
monitor/**/*.env*
oracle/**/*.env*
!**/.env.example
contracts/test
contracts/build
oracle/test
monitor/test
monitor/responses
monitor/cache
commons/test
oracle/**/*.png
oracle/**/*.jpg

10
.gitignore vendored

@ -10,11 +10,8 @@ dist
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
*.env*
!.env.example
.idea
.nyc_output
logs/
@ -49,5 +46,6 @@ __pycache__
#monitor
monitor/responses/*
monitor/configs/*.env
monitor/cache/*
!monitor/cache/.gitkeep
!monitor/.gitkeep

@ -79,3 +79,6 @@ MONITOR_TX_NUMBER_THRESHOLD | If estimated number of transaction is equal to or
MONITOR_PORT | The port for the Monitor. | integer
MONITOR_BRIDGE_NAME | The name to be used in the url path for the bridge | string
MONITOR_CACHE_EVENTS | If set to true, monitor will cache obtained events for other workers runs | `true` / `false`
MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST | File with a list of addresses, separated by newlines. If set, determines the privileged set of accounts whose requests should be automatically processed by the CollectedSignatures watcher. | string
MONITOR_HOME_TO_FOREIGN_BLOCK_LIST | File with a list of addresses, separated by newlines. If set, determines the set of accounts whose requests should be marked as unclaimed. Has a lower priority than the `MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST`. | string
MONITOR_HOME_TO_FOREIGN_CHECK_SENDER | If set to `true`, instructs the oracle to do an extra check for transaction origin in the block/allowance list. `false` by default. | `true` / `false`

@ -22,3 +22,6 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR=1
MONITOR_TX_NUMBER_THRESHOLD=100
MONITOR_PORT=3003
MONITOR_CACHE_EVENTS=true
MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST=
MONITOR_HOME_TO_FOREIGN_BLOCK_LIST=

@ -1,18 +1,11 @@
require('dotenv').config()
const Web3 = require('web3')
const Web3Utils = require('web3').utils
const logger = require('./logger')('alerts')
const eventsInfo = require('./utils/events')
const { getBlockNumber } = require('./utils/contract')
const { processedMsgNotDelivered, eventWithoutReference } = require('./utils/message')
const { BRIDGE_MODES } = require('../commons')
const { COMMON_HOME_RPC_URL, COMMON_FOREIGN_RPC_URL } = process.env
const homeProvider = new Web3.providers.HttpProvider(COMMON_HOME_RPC_URL)
const web3Home = new Web3(homeProvider)
const foreignProvider = new Web3.providers.HttpProvider(COMMON_FOREIGN_RPC_URL)
const web3Foreign = new Web3(foreignProvider)
const { web3Home, web3Foreign } = require('./utils/web3')
async function main() {
const {
@ -77,10 +70,10 @@ async function main() {
* @returns {function({blockNumber?: *}): boolean[]}
*/
const findMisbehaviorRange = currentBlockNumber => ({ blockNumber }) => {
const minus60 = currentBlockNumber.sub(Web3.utils.toBN(60))
const minus180 = currentBlockNumber.sub(Web3.utils.toBN(180))
const minus720 = currentBlockNumber.sub(Web3.utils.toBN(720))
const minus17280 = currentBlockNumber.sub(Web3.utils.toBN(17280))
const minus60 = currentBlockNumber.sub(Web3Utils.toBN(60))
const minus180 = currentBlockNumber.sub(Web3Utils.toBN(180))
const minus720 = currentBlockNumber.sub(Web3Utils.toBN(720))
const minus17280 = currentBlockNumber.sub(Web3Utils.toBN(17280))
return [
minus60.lte(blockNumber),

0
monitor/cache/.gitkeep vendored Normal file

@ -1,21 +1,20 @@
require('dotenv').config()
const Web3 = require('web3')
const BN = require('bignumber.js')
const logger = require('./logger')('checkWorker')
const { getBridgeMode } = require('../commons')
const getBalances = require('./getBalances')
const getShortEventStats = require('./getShortEventStats')
const validators = require('./validators')
const { writeFile, createDir } = require('./utils/file')
const { saveCache } = require('./utils/web3Cache')
const { web3Home } = require('./utils/web3')
const { COMMON_HOME_BRIDGE_ADDRESS, COMMON_HOME_RPC_URL, MONITOR_BRIDGE_NAME } = process.env
const { COMMON_HOME_BRIDGE_ADDRESS, MONITOR_BRIDGE_NAME } = process.env
const MONITOR_VALIDATOR_HOME_TX_LIMIT = Number(process.env.MONITOR_VALIDATOR_HOME_TX_LIMIT) || 0
const MONITOR_VALIDATOR_FOREIGN_TX_LIMIT = Number(process.env.MONITOR_VALIDATOR_FOREIGN_TX_LIMIT) || 0
const MONITOR_TX_NUMBER_THRESHOLD = Number(process.env.MONITOR_TX_NUMBER_THRESHOLD) || 100
const homeProvider = new Web3.providers.HttpProvider(COMMON_HOME_RPC_URL)
const web3Home = new Web3(homeProvider)
const { HOME_ERC_TO_ERC_ABI } = require('../commons')
async function checkWorker() {
@ -31,9 +30,13 @@ async function checkWorker() {
const home = Object.assign({}, balances.home, events.home)
const foreign = Object.assign({}, balances.foreign, events.foreign)
const status = Object.assign({}, balances, events, { home }, { foreign })
if (status.balanceDiff && status.unclaimedBalance) {
status.balanceDiff = new BN(status.balanceDiff).minus(status.unclaimedBalance).toFixed()
}
if (!status) throw new Error('status is empty: ' + JSON.stringify(status))
status.health = true
writeFile(`/responses/${MONITOR_BRIDGE_NAME}/getBalances.json`, status)
saveCache()
logger.debug('calling validators()')
const vBalances = await validators(bridgeMode)

@ -3,6 +3,7 @@ const logger = require('./logger')('checkWorker2')
const eventsStats = require('./eventsStats')
const alerts = require('./alerts')
const { writeFile, createDir } = require('./utils/file')
const { saveCache } = require('./utils/web3Cache')
const { MONITOR_BRIDGE_NAME } = process.env
@ -26,6 +27,7 @@ async function checkWorker2() {
_alerts.ok = !_alerts.executeAffirmations.mostRecentTxHash && !_alerts.executeSignatures.mostRecentTxHash
_alerts.health = true
writeFile(`/responses/${MONITOR_BRIDGE_NAME}/alerts.json`, _alerts)
saveCache()
logger.debug('Done x2')
} catch (e) {
logger.error(e)

@ -1,15 +1,12 @@
require('dotenv').config()
const Web3 = require('web3')
const logger = require('./logger')('checkWorker3')
const stuckTransfers = require('./stuckTransfers')
const { writeFile, createDir } = require('./utils/file')
const { web3Home } = require('./utils/web3')
const { MONITOR_BRIDGE_NAME, COMMON_HOME_BRIDGE_ADDRESS, COMMON_HOME_RPC_URL } = process.env
const { MONITOR_BRIDGE_NAME, COMMON_HOME_BRIDGE_ADDRESS } = process.env
const { getBridgeMode, HOME_NATIVE_TO_ERC_ABI, BRIDGE_MODES } = require('../commons')
const homeProvider = new Web3.providers.HttpProvider(COMMON_HOME_RPC_URL)
const web3Home = new Web3(homeProvider)
async function checkWorker3() {
try {
const homeBridge = new web3Home.eth.Contract(HOME_NATIVE_TO_ERC_ABI, COMMON_HOME_BRIDGE_ADDRESS)

@ -4,11 +4,12 @@ services:
monitor:
image: poanetwork/tokenbridge-monitor:latest
ports:
- "${MONITOR_PORT}:${MONITOR_PORT}"
- "${MONITOR_PORT}:${MONITOR_PORT}"
env_file: ./.env
environment:
environment:
- NODE_ENV=production
volumes:
- ./responses:/mono/monitor/responses
- ./cache:/mono/monitor/cache
restart: unless-stopped
entrypoint: "yarn start"

@ -1,8 +1,20 @@
require('dotenv').config()
const eventsInfo = require('./utils/events')
const { processedMsgNotDelivered, deliveredMsgNotProcessed, eventWithoutReference } = require('./utils/message')
const {
processedMsgNotDelivered,
deliveredMsgNotProcessed,
eventWithoutReference,
unclaimedHomeToForeignRequests
} = require('./utils/message')
const { getHomeTxSender } = require('./utils/web3Cache')
const { BRIDGE_MODES } = require('../commons')
const {
MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST,
MONITOR_HOME_TO_FOREIGN_BLOCK_LIST,
MONITOR_HOME_TO_FOREIGN_CHECK_SENDER
} = process.env
async function main() {
const {
homeToForeignRequests,
@ -33,17 +45,30 @@ async function main() {
lastChecked: Math.floor(Date.now() / 1000)
}
} else {
const onlyInHomeDeposits = homeToForeignRequests.filter(eventWithoutReference(homeToForeignConfirmations))
let onlyInHomeDeposits = homeToForeignRequests.filter(eventWithoutReference(homeToForeignConfirmations))
const onlyInForeignDeposits = homeToForeignConfirmations.filter(eventWithoutReference(homeToForeignRequests))
const onlyInHomeWithdrawals = foreignToHomeConfirmations.filter(eventWithoutReference(foreignToHomeRequests))
const onlyInForeignWithdrawals = foreignToHomeRequests.filter(eventWithoutReference(foreignToHomeConfirmations))
const unclaimedStats = {}
if (MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST || MONITOR_HOME_TO_FOREIGN_BLOCK_LIST) {
const unclaimedFilter = unclaimedHomeToForeignRequests()
if (MONITOR_HOME_TO_FOREIGN_CHECK_SENDER === 'true') {
for (let i = 0; i < onlyInHomeDeposits.length; i++) {
onlyInHomeDeposits[i].sender = await getHomeTxSender(onlyInHomeDeposits[i].transactionHash)
}
}
unclaimedStats.unclaimedHomeDeposits = onlyInHomeDeposits.filter(unclaimedFilter)
onlyInHomeDeposits = onlyInHomeDeposits.filter(e => !unclaimedFilter(e))
}
return {
onlyInHomeDeposits,
onlyInForeignDeposits,
onlyInHomeWithdrawals,
onlyInForeignWithdrawals,
...unclaimedStats,
lastChecked: Math.floor(Date.now() / 1000)
}
}

@ -1,23 +1,11 @@
require('dotenv').config()
const BN = require('bignumber.js')
const Web3 = require('web3')
const Web3Utils = require('web3').utils
const logger = require('./logger')('getBalances')
const { BRIDGE_MODES } = require('../commons')
const { web3Home, web3Foreign } = require('./utils/web3')
const Web3Utils = Web3.utils
const {
COMMON_HOME_RPC_URL,
COMMON_FOREIGN_RPC_URL,
COMMON_HOME_BRIDGE_ADDRESS,
COMMON_FOREIGN_BRIDGE_ADDRESS
} = process.env
const homeProvider = new Web3.providers.HttpProvider(COMMON_HOME_RPC_URL)
const web3Home = new Web3(homeProvider)
const foreignProvider = new Web3.providers.HttpProvider(COMMON_FOREIGN_RPC_URL)
const web3Foreign = new Web3(foreignProvider)
const { COMMON_HOME_BRIDGE_ADDRESS, COMMON_FOREIGN_BRIDGE_ADDRESS } = process.env
const {
ERC20_ABI,

@ -1,6 +1,16 @@
require('dotenv').config()
const BN = require('bignumber.js')
const Web3Utils = require('web3').utils
const eventsInfo = require('./utils/events')
const { eventWithoutReference, unclaimedHomeToForeignRequests } = require('./utils/message')
const { BRIDGE_MODES } = require('../commons')
const { getHomeTxSender } = require('./utils/web3Cache')
const {
MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST,
MONITOR_HOME_TO_FOREIGN_BLOCK_LIST,
MONITOR_HOME_TO_FOREIGN_CHECK_SENDER
} = process.env
async function main(bridgeMode) {
const {
@ -24,9 +34,26 @@ async function main(bridgeMode) {
}
}
} else {
return {
const stats = {
depositsDiff: homeToForeignRequests.length - homeToForeignConfirmations.length,
withdrawalDiff: foreignToHomeConfirmations.length - foreignToHomeRequests.length,
withdrawalDiff: foreignToHomeConfirmations.length - foreignToHomeRequests.length
}
if (MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST || MONITOR_HOME_TO_FOREIGN_BLOCK_LIST) {
const onlyInHomeDeposits = homeToForeignRequests.filter(eventWithoutReference(homeToForeignConfirmations))
if (MONITOR_HOME_TO_FOREIGN_CHECK_SENDER === 'true') {
for (let i = 0; i < onlyInHomeDeposits.length; i++) {
onlyInHomeDeposits[i].sender = await getHomeTxSender(onlyInHomeDeposits[i].transactionHash)
}
}
const unclaimedPool = onlyInHomeDeposits.filter(unclaimedHomeToForeignRequests())
stats.depositsDiff -= unclaimedPool.length
stats.unclaimedDiff = unclaimedPool.length
stats.unclaimedBalance = Web3Utils.fromWei(BN.sum(...unclaimedPool.map(e => e.value)).toFixed())
}
return {
...stats,
home: {
deposits: homeToForeignRequests.length,
withdrawals: foreignToHomeConfirmations.length

@ -14,7 +14,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"bignumber.js": "^6.0.0",
"bignumber.js": "^9.0.1",
"cors": "^2.8.5",
"dotenv": "^5.0.1",
"express": "^4.16.3",

@ -2,6 +2,10 @@
CONFIGDIR="configs"
RESPONSESDIR="responses"
ACLDIR="access-lists"
ALLOWANCEFILE="allowance_list.txt"
BLOCKFILE="block_list.txt"
CACHEDIR="cache"
IMAGETAG="latest"
cd $(dirname $0)/..
@ -26,10 +30,17 @@ if /usr/local/bin/docker-compose ps | grep -q -i 'monitor'; then
fi
done
alist=`source ${file} && echo ${MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST}`
blist=`source ${file} && echo ${MONITOR_HOME_TO_FOREIGN_BLOCK_LIST}`
al_param="$(pwd)/${ACLDIR}/${bridgename}/${ALLOWANCEFILE}:/mono/monitor/access-lists/allowance_list.txt"
bl_param="$(pwd)/${ACLDIR}/${bridgename}/${BLOCKFILE}:/mono/monitor/access-lists/block_list.txt"
containername=${bridgename}"-checker"
docker container stats --no-stream ${containername} 2>/dev/null 1>&2
if [ ! "$?" == "0" ]; then
docker run --rm --env-file $file -v $(pwd)/${RESPONSESDIR}:/mono/monitor/responses \
${alist:+"-v"} ${alist:+"$al_param"} ${blist:+"-v"} ${blist:+"$bl_param"} \
-v $(pwd)/${CACHEDIR}/${bridgename}:/mono/monitor/cache \
--name ${containername} poanetwork/tokenbridge-monitor:${IMAGETAG} \
/bin/bash -c 'yarn check-all'
shasum -a 256 -s -c ${checksumfile}
@ -57,4 +68,4 @@ if /usr/local/bin/docker-compose ps | grep -q -i 'monitor'; then
else
echo "Monitor is not running, skipping checks."
fi
fi

@ -1,14 +1,11 @@
require('dotenv').config()
const Web3 = require('web3')
const logger = require('./logger')('stuckTransfers.js')
const { FOREIGN_V1_ABI } = require('../commons/abis')
const { web3Foreign } = require('./utils/web3')
const { COMMON_FOREIGN_RPC_URL, COMMON_FOREIGN_BRIDGE_ADDRESS } = process.env
const { COMMON_FOREIGN_BRIDGE_ADDRESS } = process.env
const MONITOR_FOREIGN_START_BLOCK = Number(process.env.MONITOR_FOREIGN_START_BLOCK) || 0
const foreignProvider = new Web3.providers.HttpProvider(COMMON_FOREIGN_RPC_URL)
const web3Foreign = new Web3(foreignProvider)
const ABITransferWithoutData = [
{
anonymous: false,

@ -1,5 +1,4 @@
require('dotenv').config()
const Web3 = require('web3')
const { toBN } = require('web3').utils
const logger = require('../logger')('eventsUtils')
const {
@ -19,23 +18,12 @@ const {
const { normalizeEventInformation } = require('./message')
const { filterTransferBeforeES } = require('./tokenUtils')
const { writeFile, readCacheFile } = require('./file')
const { web3Home, web3Foreign } = require('./web3')
const {
COMMON_HOME_RPC_URL,
COMMON_FOREIGN_RPC_URL,
COMMON_HOME_BRIDGE_ADDRESS,
COMMON_FOREIGN_BRIDGE_ADDRESS,
MONITOR_CACHE_EVENTS
} = process.env
const { COMMON_HOME_BRIDGE_ADDRESS, COMMON_FOREIGN_BRIDGE_ADDRESS, MONITOR_CACHE_EVENTS } = process.env
const MONITOR_HOME_START_BLOCK = toBN(Number(process.env.MONITOR_HOME_START_BLOCK) || 0)
const MONITOR_FOREIGN_START_BLOCK = toBN(Number(process.env.MONITOR_FOREIGN_START_BLOCK) || 0)
const homeProvider = new Web3.providers.HttpProvider(COMMON_HOME_RPC_URL)
const web3Home = new Web3(homeProvider)
const foreignProvider = new Web3.providers.HttpProvider(COMMON_FOREIGN_RPC_URL)
const web3Foreign = new Web3(foreignProvider)
const { getBlockNumber } = require('./contract')
const cacheFilePath = '/tmp/cachedEvents.json'

@ -38,9 +38,19 @@ function readCacheFile(filePath) {
}
}
function readAccessListFile(filePath) {
const data = fs.readFileSync(filePath)
return data
.toString()
.split('\n')
.map(addr => addr.trim().toLowerCase())
.filter(addr => addr.length === 42)
}
module.exports = {
readFile,
writeFile,
createDir,
readCacheFile
readCacheFile,
readAccessListFile
}

@ -1,4 +1,7 @@
const { parseAMBMessage } = require('../../commons')
const { readAccessListFile } = require('./file')
const { MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST, MONITOR_HOME_TO_FOREIGN_BLOCK_LIST } = process.env
const keyAMB = e => [e.messageId, e.sender, e.executor].join(',').toLowerCase()
@ -51,9 +54,22 @@ const eventWithoutReference = otherSideEvents => {
return e => !keys.has(key(e))
}
const unclaimedHomeToForeignRequests = () => {
if (MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST) {
const allowanceList = readAccessListFile(MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST)
return e => !allowanceList.includes(e.recipient.toLowerCase()) && !(e.sender && allowanceList.includes(e.sender))
} else if (MONITOR_HOME_TO_FOREIGN_BLOCK_LIST) {
const blockList = readAccessListFile(MONITOR_HOME_TO_FOREIGN_BLOCK_LIST)
return e => blockList.includes(e.recipient.toLowerCase()) || (e.sender && blockList.includes(e.sender))
} else {
return () => false
}
}
module.exports = {
deliveredMsgNotProcessed,
processedMsgNotDelivered,
normalizeEventInformation,
eventWithoutReference
eventWithoutReference,
unclaimedHomeToForeignRequests
}

15
monitor/utils/web3.js Normal file

@ -0,0 +1,15 @@
require('dotenv').config()
const Web3 = require('web3')
const { COMMON_HOME_RPC_URL, COMMON_FOREIGN_RPC_URL } = process.env
const homeProvider = new Web3.providers.HttpProvider(COMMON_HOME_RPC_URL)
const web3Home = new Web3(homeProvider)
const foreignProvider = new Web3.providers.HttpProvider(COMMON_FOREIGN_RPC_URL)
const web3Foreign = new Web3(foreignProvider)
module.exports = {
web3Home,
web3Foreign
}

@ -0,0 +1,29 @@
const logger = require('../logger')('web3Cache')
const { readCacheFile, writeFile } = require('./file')
const { web3Home } = require('./web3')
let isDirty = false
const homeTxSendersCacheFile = './cache/homeTxSenders.json'
const cachedHomeTxSenders = readCacheFile(homeTxSendersCacheFile) || {}
async function getHomeTxSender(txHash) {
if (!cachedHomeTxSenders[txHash]) {
logger.debug(`Fetching sender for tx ${txHash}`)
cachedHomeTxSenders[txHash] = (await web3Home.eth.getTransaction(txHash)).from.toLowerCase()
isDirty = true
}
return cachedHomeTxSenders[txHash]
}
function saveCache() {
if (isDirty) {
logger.debug('Saving cache on disk')
writeFile(homeTxSendersCacheFile, cachedHomeTxSenders)
}
}
module.exports = {
getHomeTxSender,
saveCache
}

@ -1,13 +1,12 @@
require('dotenv').config()
const Web3 = require('web3')
const Web3Utils = require('web3').utils
const fetch = require('node-fetch')
const logger = require('./logger')('validators')
const { getBridgeABIs, BRIDGE_VALIDATORS_ABI, getValidatorList, gasPriceFromSupplier } = require('../commons')
const { getBlockNumber } = require('./utils/contract')
const { web3Home, web3Foreign } = require('./utils/web3')
const {
COMMON_HOME_RPC_URL,
COMMON_FOREIGN_RPC_URL,
COMMON_HOME_BRIDGE_ADDRESS,
COMMON_FOREIGN_BRIDGE_ADDRESS,
COMMON_HOME_GAS_PRICE_SUPPLIER_URL,
@ -24,14 +23,6 @@ const MONITOR_FOREIGN_START_BLOCK = Number(process.env.MONITOR_FOREIGN_START_BLO
const MONITOR_VALIDATOR_HOME_TX_LIMIT = Number(process.env.MONITOR_VALIDATOR_HOME_TX_LIMIT) || 0
const MONITOR_VALIDATOR_FOREIGN_TX_LIMIT = Number(process.env.MONITOR_VALIDATOR_FOREIGN_TX_LIMIT) || 0
const Web3Utils = Web3.utils
const homeProvider = new Web3.providers.HttpProvider(COMMON_HOME_RPC_URL)
const web3Home = new Web3(homeProvider)
const foreignProvider = new Web3.providers.HttpProvider(COMMON_FOREIGN_RPC_URL)
const web3Foreign = new Web3(foreignProvider)
const homeGasPriceSupplierOpts = {
speedType: COMMON_HOME_GAS_PRICE_SPEED_TYPE,
factor: COMMON_HOME_GAS_PRICE_FACTOR,

@ -118,7 +118,10 @@ async function readAccessListFile(fileName, logger) {
.split('\n')
.map(addr => addr.trim().toLowerCase())
.filter(addr => addr.length === 42)
logger.info({ fileName }, `Access list was read successfully, ${data.length} addresses found`)
logger.info(
{ fileName },
`Access list was read successfully, ${readAccessLists[fileName].length} addresses found`
)
logger.debug({ addresses: readAccessLists[fileName] }, `Read addresses from the file`)
} catch (e) {
readAccessLists[fileName] = []

@ -5073,6 +5073,11 @@ bignumber.js@^6.0.0:
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-6.0.0.tgz#bbfa047644609a5af093e9cbd83b0461fa3f6002"
integrity sha512-x247jIuy60/+FtMRvscqfxtVHQf8AGx2hm9c6btkgC0x/hp9yt+teISNhvF8WlwRkCc5yF2fDECH8SIMe8j+GA==
bignumber.js@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5"
integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==
bignumber.js@~8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-8.0.2.tgz#d8c4e1874359573b1ef03011a2d861214aeef137"