Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
Merge branch 'master' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
arjun-io committed Jul 24, 2023
2 parents dba7e8a + fffff1f commit 0a1f477
Show file tree
Hide file tree
Showing 13 changed files with 791 additions and 161 deletions.
4 changes: 2 additions & 2 deletions packages/perennial-vaults/deploy/01_deploy_vault_alpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const controller = (await get('Controller_Proxy')).address
const long = (await get('Product_LongEther')).address
const short = (await get('Product_ShortEther')).address
const targetLeverage = ethers.utils.parseEther('2')
const maxCollateral = ethers.utils.parseEther('4000000')
const targetLeverage = ethers.utils.parseEther('1.5')
const maxCollateral = ethers.utils.parseEther('6000000')

const vaultImpl = await deploy('PerennialVaultAlpha_Impl', {
contract: 'BalancedVault',
Expand Down
2 changes: 1 addition & 1 deletion packages/perennial-vaults/deploy/02_deploy_vault_bravo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const controller = (await get('Controller_Proxy')).address
const long = (await get('Product_LongArbitrum')).address
const short = (await get('Product_ShortArbitrum')).address
const targetLeverage = ethers.utils.parseEther('2')
const targetLeverage = ethers.utils.parseEther('1')
const maxCollateral = ethers.utils.parseEther('500000')

const vaultImpl = await deploy('PerennialVaultBravo_Impl', {
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Binary file not shown.
28 changes: 14 additions & 14 deletions packages/perennial/deployments/arbitrumGoerli/Product_Impl.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions packages/perennial/tasks/checkLiquidatable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { formatEther } from 'ethers/lib/utils'
import { task } from 'hardhat/config'
import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types'
import { providers } from '@0xsequence/multicall'
import { getDeposits } from './checkSolvency'

export function chunk<T>(arr: T[], size: number): T[][] {
return Array.from({ length: Math.ceil(arr.length / size) }, (_: T, i: number) => arr.slice(i * size, i * size + size))
Expand All @@ -16,14 +17,18 @@ export default task('checkLiquidatable', 'Checks all Product users to see if liq
deployments: { get },
} = HRE
const multicall = new providers.MulticallProvider(ethers.provider)
const collateral = (await ethers.getContractAt('ICollateral', (await get('Collateral_Proxy')).address)).connect(
multicall,
)
const collateralDeployment = await get('Collateral_Proxy')
const collateral = (await ethers.getContractAt('ICollateral', collateralDeployment.address)).connect(multicall)
const lens = (await ethers.getContractAt('IPerennialLens', (await get('PerennialLens_V01')).address)).connect(
multicall,
)

const deposits = await collateral.queryFilter(collateral.filters.Deposit(null, args.product))
const deposits = await getDeposits(
collateral,
args.product,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
collateralDeployment.receipt!.blockNumber,
)
const users = Array.from(new Set([...deposits].map(e => e.args.user.toLowerCase())))
const usersChunked = chunk<string>(users, 50)

Expand Down
114 changes: 87 additions & 27 deletions packages/perennial/tasks/checkSolvency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import '@nomiclabs/hardhat-ethers'
import { BigNumber, utils } from 'ethers'
import { task } from 'hardhat/config'
import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types'
import { DepositEvent } from '../types/generated/contracts/interfaces/ICollateral'
import { DepositEvent, ICollateral } from '../types/generated/contracts/interfaces/ICollateral'
import { chunk } from './checkLiquidatable'

export default task('checkSolvency', 'Checks if Product is solvent')
.addPositionalParam('product', 'Product Address to Check')
.addFlag('findShortfall', 'Finds the users causing a shortfall')
.setAction(async (args: TaskArguments, HRE: HardhatRuntimeEnvironment) => {
const {
ethers,
Expand All @@ -22,51 +23,110 @@ export default task('checkSolvency', 'Checks if Product is solvent')
)
const currentBlock = await multicall.getBlockNumber()

const deposits: DepositEvent[] = []
let hasMore = true
let page = 0

while (hasMore) {
console.log(
`Fetching deposits from block ${currentBlock - (page + 1) * 2000000} to ${currentBlock - page * 2000000}...`,
)
deposits.push(
...(await collateral.queryFilter(
collateral.filters.Deposit(null, args.product),
currentBlock - (page + 1) * 2000000,
currentBlock - page * 2000000,
)),
)
const deposits = await getDeposits(
collateral,
args.product,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
hasMore = currentBlock - page * 2000000 > collateralDeployment.receipt!.blockNumber - 1
page = page + 1
}
collateralDeployment.receipt!.blockNumber,
)

const users = Array.from(new Set([...deposits].map(e => e.args.user.toLowerCase())))
const usersChunked = chunk<string>(users, 200)

let totalUserCollateral = BigNumber.from(0)
console.log(`Checking if Product is solvent. Total users ${users.length}. Groups ${usersChunked.length}`)
console.log(
`Checking if Product ${args.product} is solvent. Total users ${users.length}. Groups ${usersChunked.length}`,
)
if (args.findShortfall) console.log('Finding shortfall users, may take longer...')

const allUserCollaterals: { account: string; shortfall: BigNumber; after: BigNumber }[] = []
const startingShortfall = await lens.callStatic.shortfall(args.product, { blockTag: currentBlock })
console.log(`Starting shortfall: $${utils.formatEther(startingShortfall)}`)

for (let i = 0; i < usersChunked.length; i++) {
console.log(`Checking group ${i + 1} of ${usersChunked.length}...`)
const userGroup = usersChunked[i]
let groupHasShortfall = false
const userCollaterals = await Promise.all(
userGroup.map(account =>
lens.callStatic['collateral(address,address)'](account, args.product, { blockTag: currentBlock }),
),
userGroup.map(async account => {
const [after, shortfall] = await Promise.all([
lens.callStatic['collateral(address,address)'](account, args.product, {
blockTag: currentBlock,
}),
lens.callStatic.shortfall(args.product, {
blockTag: currentBlock,
}),
])
if (shortfall.gt(startingShortfall)) groupHasShortfall = true
return {
account,
after,
shortfall,
}
}),
)
totalUserCollateral = totalUserCollateral.add(userCollaterals.reduce((a, b) => a.add(b), BigNumber.from(0)))

if (groupHasShortfall && args.findShortfall) {
console.log('Shortfall found in group. Searching each user...')
for (const account of userGroup) {
const [, shortfall] = await Promise.all([
lens.callStatic['collateral(address,address)'](account, args.product, {
blockTag: currentBlock,
}),
lens.callStatic.shortfall(args.product, {
blockTag: currentBlock,
}),
])
if (shortfall.gt(startingShortfall)) {
console.log(`User ${account} has shortfall of $${utils.formatEther(shortfall)}`)
}
}
}

totalUserCollateral = totalUserCollateral.add(userCollaterals.reduce((a, b) => a.add(b.after), BigNumber.from(0)))
allUserCollaterals.push(...userCollaterals)
}

const productCollateral = await lens.callStatic['collateral(address)'](args.product, { blockTag: currentBlock })
const delta = productCollateral.sub(totalUserCollateral)

if (!delta.isZero()) {
if (delta.isNegative()) console.log('Product Insolvent')
else console.log('Product solvent')
console.log(`Delta: ${utils.formatEther(delta)}`)
if (delta.isNegative()) {
console.log('Product Insolvent')
} else {
console.log('Product solvent')
}

console.log(`Delta: $${utils.formatEther(delta)}`)
}

console.log('done.')
})

export async function getDeposits(
collateral: ICollateral,
productAddress: string,
deploymentBlockNumber: number,
): Promise<DepositEvent[]> {
const currentBlock = await collateral.provider.getBlockNumber()
const deposits: DepositEvent[] = []
let hasMore = true
let page = 0

while (hasMore) {
console.log(
`Fetching deposits from block ${currentBlock - (page + 1) * 2000000} to ${currentBlock - page * 2000000}...`,
)
deposits.push(
...(await collateral.queryFilter(
collateral.filters.Deposit(null, productAddress),
currentBlock - (page + 1) * 2000000,
currentBlock - page * 2000000,
)),
)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
hasMore = currentBlock - page * 2000000 > deploymentBlockNumber - 1
page = page + 1
}
return deposits
}
190 changes: 190 additions & 0 deletions packages/perennial/tasks/collectBucketedMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import '@nomiclabs/hardhat-ethers'
import { task } from 'hardhat/config'
import { HardhatRuntimeEnvironment, TaskArguments } from 'hardhat/types'
import { existsSync } from 'fs'
import { appendFile, writeFile } from 'fs/promises'
import { request, gql } from 'graphql-request'
import { isArbitrum } from '../../common/testutil/network'
import { BigNumber, utils } from 'ethers'

const { formatEther } = utils
const QUERY_PAGE_SIZE = 1000

export function bucketTimestamp(timestampSeconds: number, bucket: 'hourly' | 'daily' | 'weekly'): number {
let bucketSize
if (bucket === 'hourly') {
bucketSize = 3600
} else if (bucket === 'daily') {
bucketSize = 86400
} else if (bucket === 'weekly') {
bucketSize = 604800
} else {
throw new Error(`Invalid bucket: ${bucket}`)
}

return Math.floor(timestampSeconds / bucketSize) * bucketSize
}

type VolumeQueryResult = {
bucketedVolumes: {
periodStartTimestamp: string
periodStartVersion: string
periodEndVersion: string
product: string
makerNotional: string
makerFees: string
takerNotional: string
takerFees: string
}[]
lastSettle: { blockNumber: string }[]
}

type SettleQueryResult = {
settles: {
product: string
preRate: string
toRate: string
preMakerPosition: string
toMakerPosition: string
}[]
}

export default task('collectBucketedMetrics', 'Collects metrics for a given date range')
.addPositionalParam('startDateString', 'Start date string to collect stats for. YYYY-MM-DD format')
.addPositionalParam('endDateString', 'End date string to collect stats for. YYYY-MM-DD format')
.addPositionalParam('bucket', 'Bucket granularity: hourly, daily, or weekly')
.addVariadicPositionalParam('markets', 'Address of market to collect stats for')
.addOptionalParam('output', 'Output file path')
.addFlag('csv', 'Output as CSV')
.setAction(async (args: TaskArguments, HRE: HardhatRuntimeEnvironment) => {
const bucket = args.bucket
if (bucket !== 'hourly' && bucket !== 'daily' && bucket !== 'weekly') {
throw new Error('Invalid bucket')
}

const startDate = bucketTimestamp(new Date(args.startDateString).getTime() / 1000, bucket)
const endDate = bucketTimestamp(new Date(args.endDateString).setUTCHours(23, 59, 59, 999) / 1000, bucket)
const outputFile = args.output
const csv = args.csv

const {
deployments: { getNetworkName },
} = HRE
const graphURL = isArbitrum(getNetworkName()) ? process.env.ARBITRUM_GRAPH_URL : process.env.ETHEREUM_GRAPH_URL

if (!graphURL) {
console.log('Invalid Network.')
return
}

const markets: string[] = args.markets

let page = 0
let res: VolumeQueryResult = await request(graphURL, GET_BUCKETED_VOLUMES_QUERY, {
markets,
bucket,
fromTs: startDate.toString(),
toTs: endDate.toString(),
first: QUERY_PAGE_SIZE,
skip: page * QUERY_PAGE_SIZE,
})
const rawData = res
while (res.bucketedVolumes.length === QUERY_PAGE_SIZE) {
page += 1
res = await request(graphURL, GET_BUCKETED_VOLUMES_QUERY, {
markets,
bucket,
fromTs: startDate.toString(),
toTs: endDate.toString(),
first: QUERY_PAGE_SIZE,
skip: page * QUERY_PAGE_SIZE,
})
rawData.bucketedVolumes = [...rawData.bucketedVolumes, ...res.bucketedVolumes]
}

const hourlyVolumes = await Promise.all(
rawData.bucketedVolumes.map(
async ({ periodStartTimestamp, periodStartVersion, periodEndVersion, product, takerFees }) => {
const settles: SettleQueryResult = await request(graphURL, GET_SETTLES_QUERY, {
markets: [product],
fromVersion: periodStartVersion,
toVersion: periodEndVersion,
})
// TODO(arjun): this should be a weighted average
const averageRate = settles.settles.length
? settles.settles
.reduce((acc, { preRate, toRate }) => {
return acc.add(BigNumber.from(preRate).add(toRate).div(2))
}, BigNumber.from(0))
.div(settles.settles.length)
: BigNumber.from(0)
const averageLiquidity = settles.settles.length
? settles.settles
.reduce((acc, { preMakerPosition, toMakerPosition }) => {
return acc.add(BigNumber.from(preMakerPosition).add(toMakerPosition).div(2))
}, BigNumber.from(0))
.div(settles.settles.length)
: BigNumber.from(0)
return {
timestamp: parseInt(periodStartTimestamp),
product: product.toLowerCase(),
takerFees: `$${formatEther(BigNumber.from(takerFees))}`,
averageRateAnnualized: `${formatEther(averageRate.mul(60 * 60 * 24 * 365).mul(100))}%`,
averageLiquidity: `$${formatEther(averageLiquidity)}`,
}
},
),
)

const str = csv
? hourlyVolumes.map(o => Object.values(o).join(',')).join('\n')
: JSON.stringify(hourlyVolumes, null, outputFile ? 0 : 2)
if (outputFile) {
if (!existsSync(outputFile)) {
await writeFile(outputFile, csv ? Object.keys(hourlyVolumes[0]).join(',') : '')
}

await appendFile(outputFile, `\n${str}`)
return
}

console.log(str)
})

const GET_BUCKETED_VOLUMES_QUERY = gql`
query getData($markets: [Bytes]!, $fromTs: BigInt!, $toTs: BigInt!, $bucket: Bucket!, $first: Int!, $skip: Int!) {
bucketedVolumes(
first: $first
skip: $skip
where: {
bucket: $bucket
product_in: $markets
periodStartTimestamp_gte: $fromTs
periodStartTimestamp_lte: $toTs
}
orderBy: periodStartBlock
orderDirection: asc
) {
periodStartTimestamp
periodStartVersion
periodEndVersion
product
makerNotional
makerFees
takerNotional
takerFees
}
}
`

const GET_SETTLES_QUERY = gql`
query getSettles($markets: [Bytes]!, $fromVersion: BigInt!, $toVersion: BigInt!) {
settles(where: { product_in: $markets, toVersion_gte: $fromVersion, toVersion_lte: $toVersion }) {
product
preRate
toRate
preMakerPosition
toMakerPosition
}
}
`
Loading

0 comments on commit 0a1f477

Please sign in to comment.