infrastructure-upgrade/lib/v3-core/test/SqrtPriceMath.spec.ts

380 lines
13 KiB
TypeScript
Raw Permalink Normal View History

2023-04-08 18:46:18 +00:00
import { BigNumber, constants } from 'ethers'
import { ethers } from 'hardhat'
import { SqrtPriceMathTest } from '../typechain/SqrtPriceMathTest'
import { expect } from './shared/expect'
import snapshotGasCost from './shared/snapshotGasCost'
import { encodePriceSqrt, expandTo18Decimals, MaxUint128 } from './shared/utilities'
const {
constants: { MaxUint256 },
} = ethers
describe('SqrtPriceMath', () => {
let sqrtPriceMath: SqrtPriceMathTest
before(async () => {
const sqrtPriceMathTestFactory = await ethers.getContractFactory('SqrtPriceMathTest')
sqrtPriceMath = (await sqrtPriceMathTestFactory.deploy()) as SqrtPriceMathTest
})
describe('#getNextSqrtPriceFromInput', () => {
it('fails if price is zero', async () => {
await expect(sqrtPriceMath.getNextSqrtPriceFromInput(0, 0, expandTo18Decimals(1).div(10), false)).to.be.reverted
})
it('fails if liquidity is zero', async () => {
await expect(sqrtPriceMath.getNextSqrtPriceFromInput(1, 0, expandTo18Decimals(1).div(10), true)).to.be.reverted
})
it('fails if input amount overflows the price', async () => {
const price = BigNumber.from(2).pow(160).sub(1)
const liquidity = 1024
const amountIn = 1024
await expect(sqrtPriceMath.getNextSqrtPriceFromInput(price, liquidity, amountIn, false)).to.be.reverted
})
it('any input amount cannot underflow the price', async () => {
const price = 1
const liquidity = 1
const amountIn = BigNumber.from(2).pow(255)
expect(await sqrtPriceMath.getNextSqrtPriceFromInput(price, liquidity, amountIn, true)).to.eq(1)
})
it('returns input price if amount in is zero and zeroForOne = true', async () => {
const price = encodePriceSqrt(1, 1)
expect(await sqrtPriceMath.getNextSqrtPriceFromInput(price, expandTo18Decimals(1).div(10), 0, true)).to.eq(price)
})
it('returns input price if amount in is zero and zeroForOne = false', async () => {
const price = encodePriceSqrt(1, 1)
expect(await sqrtPriceMath.getNextSqrtPriceFromInput(price, expandTo18Decimals(1).div(10), 0, false)).to.eq(price)
})
it('returns the minimum price for max inputs', async () => {
const sqrtP = BigNumber.from(2).pow(160).sub(1)
const liquidity = MaxUint128
const maxAmountNoOverflow = MaxUint256.sub(liquidity.shl(96).div(sqrtP))
expect(await sqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, liquidity, maxAmountNoOverflow, true)).to.eq('1')
})
it('input amount of 0.1 token1', async () => {
const sqrtQ = await sqrtPriceMath.getNextSqrtPriceFromInput(
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
expandTo18Decimals(1).div(10),
false
)
expect(sqrtQ).to.eq('87150978765690771352898345369')
})
it('input amount of 0.1 token0', async () => {
const sqrtQ = await sqrtPriceMath.getNextSqrtPriceFromInput(
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
expandTo18Decimals(1).div(10),
true
)
expect(sqrtQ).to.eq('72025602285694852357767227579')
})
it('amountIn > type(uint96).max and zeroForOne = true', async () => {
expect(
await sqrtPriceMath.getNextSqrtPriceFromInput(
encodePriceSqrt(1, 1),
expandTo18Decimals(10),
BigNumber.from(2).pow(100),
true
)
// perfect answer:
// https://www.wolframalpha.com/input/?i=624999999995069620+-+%28%281e19+*+1+%2F+%281e19+%2B+2%5E100+*+1%29%29+*+2%5E96%29
).to.eq('624999999995069620')
})
it('can return 1 with enough amountIn and zeroForOne = true', async () => {
expect(
await sqrtPriceMath.getNextSqrtPriceFromInput(encodePriceSqrt(1, 1), 1, constants.MaxUint256.div(2), true)
).to.eq(1)
})
it('zeroForOne = true gas', async () => {
await snapshotGasCost(
sqrtPriceMath.getGasCostOfGetNextSqrtPriceFromInput(
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
expandTo18Decimals(1).div(10),
true
)
)
})
it('zeroForOne = false gas', async () => {
await snapshotGasCost(
sqrtPriceMath.getGasCostOfGetNextSqrtPriceFromInput(
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
expandTo18Decimals(1).div(10),
false
)
)
})
})
describe('#getNextSqrtPriceFromOutput', () => {
it('fails if price is zero', async () => {
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(0, 0, expandTo18Decimals(1).div(10), false)).to.be.reverted
})
it('fails if liquidity is zero', async () => {
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(1, 0, expandTo18Decimals(1).div(10), true)).to.be.reverted
})
it('fails if output amount is exactly the virtual reserves of token0', async () => {
const price = '20282409603651670423947251286016'
const liquidity = 1024
const amountOut = 4
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(price, liquidity, amountOut, false)).to.be.reverted
})
it('fails if output amount is greater than virtual reserves of token0', async () => {
const price = '20282409603651670423947251286016'
const liquidity = 1024
const amountOut = 5
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(price, liquidity, amountOut, false)).to.be.reverted
})
it('fails if output amount is greater than virtual reserves of token1', async () => {
const price = '20282409603651670423947251286016'
const liquidity = 1024
const amountOut = 262145
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(price, liquidity, amountOut, true)).to.be.reverted
})
it('fails if output amount is exactly the virtual reserves of token1', async () => {
const price = '20282409603651670423947251286016'
const liquidity = 1024
const amountOut = 262144
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(price, liquidity, amountOut, true)).to.be.reverted
})
it('succeeds if output amount is just less than the virtual reserves of token1', async () => {
const price = '20282409603651670423947251286016'
const liquidity = 1024
const amountOut = 262143
const sqrtQ = await sqrtPriceMath.getNextSqrtPriceFromOutput(price, liquidity, amountOut, true)
expect(sqrtQ).to.eq('77371252455336267181195264')
})
it('puzzling echidna test', async () => {
const price = '20282409603651670423947251286016'
const liquidity = 1024
const amountOut = 4
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(price, liquidity, amountOut, false)).to.be.reverted
})
it('returns input price if amount in is zero and zeroForOne = true', async () => {
const price = encodePriceSqrt(1, 1)
expect(await sqrtPriceMath.getNextSqrtPriceFromOutput(price, expandTo18Decimals(1).div(10), 0, true)).to.eq(price)
})
it('returns input price if amount in is zero and zeroForOne = false', async () => {
const price = encodePriceSqrt(1, 1)
expect(await sqrtPriceMath.getNextSqrtPriceFromOutput(price, expandTo18Decimals(1).div(10), 0, false)).to.eq(
price
)
})
it('output amount of 0.1 token1', async () => {
const sqrtQ = await sqrtPriceMath.getNextSqrtPriceFromOutput(
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
expandTo18Decimals(1).div(10),
false
)
expect(sqrtQ).to.eq('88031291682515930659493278152')
})
it('output amount of 0.1 token1', async () => {
const sqrtQ = await sqrtPriceMath.getNextSqrtPriceFromOutput(
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
expandTo18Decimals(1).div(10),
true
)
expect(sqrtQ).to.eq('71305346262837903834189555302')
})
it('reverts if amountOut is impossible in zero for one direction', async () => {
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(encodePriceSqrt(1, 1), 1, constants.MaxUint256, true)).to.be
.reverted
})
it('reverts if amountOut is impossible in one for zero direction', async () => {
await expect(sqrtPriceMath.getNextSqrtPriceFromOutput(encodePriceSqrt(1, 1), 1, constants.MaxUint256, false)).to
.be.reverted
})
it('zeroForOne = true gas', async () => {
await snapshotGasCost(
sqrtPriceMath.getGasCostOfGetNextSqrtPriceFromOutput(
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
expandTo18Decimals(1).div(10),
true
)
)
})
it('zeroForOne = false gas', async () => {
await snapshotGasCost(
sqrtPriceMath.getGasCostOfGetNextSqrtPriceFromOutput(
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
expandTo18Decimals(1).div(10),
false
)
)
})
})
describe('#getAmount0Delta', () => {
it('returns 0 if liquidity is 0', async () => {
const amount0 = await sqrtPriceMath.getAmount0Delta(encodePriceSqrt(1, 1), encodePriceSqrt(2, 1), 0, true)
expect(amount0).to.eq(0)
})
it('returns 0 if prices are equal', async () => {
const amount0 = await sqrtPriceMath.getAmount0Delta(encodePriceSqrt(1, 1), encodePriceSqrt(1, 1), 0, true)
expect(amount0).to.eq(0)
})
it('returns 0.1 amount1 for price of 1 to 1.21', async () => {
const amount0 = await sqrtPriceMath.getAmount0Delta(
encodePriceSqrt(1, 1),
encodePriceSqrt(121, 100),
expandTo18Decimals(1),
true
)
expect(amount0).to.eq('90909090909090910')
const amount0RoundedDown = await sqrtPriceMath.getAmount0Delta(
encodePriceSqrt(1, 1),
encodePriceSqrt(121, 100),
expandTo18Decimals(1),
false
)
expect(amount0RoundedDown).to.eq(amount0.sub(1))
})
it('works for prices that overflow', async () => {
const amount0Up = await sqrtPriceMath.getAmount0Delta(
encodePriceSqrt(BigNumber.from(2).pow(90), 1),
encodePriceSqrt(BigNumber.from(2).pow(96), 1),
expandTo18Decimals(1),
true
)
const amount0Down = await sqrtPriceMath.getAmount0Delta(
encodePriceSqrt(BigNumber.from(2).pow(90), 1),
encodePriceSqrt(BigNumber.from(2).pow(96), 1),
expandTo18Decimals(1),
false
)
expect(amount0Up).to.eq(amount0Down.add(1))
})
it(`gas cost for amount0 where roundUp = true`, async () => {
await snapshotGasCost(
sqrtPriceMath.getGasCostOfGetAmount0Delta(
encodePriceSqrt(100, 121),
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
true
)
)
})
it(`gas cost for amount0 where roundUp = true`, async () => {
await snapshotGasCost(
sqrtPriceMath.getGasCostOfGetAmount0Delta(
encodePriceSqrt(100, 121),
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
false
)
)
})
})
describe('#getAmount1Delta', () => {
it('returns 0 if liquidity is 0', async () => {
const amount1 = await sqrtPriceMath.getAmount1Delta(encodePriceSqrt(1, 1), encodePriceSqrt(2, 1), 0, true)
expect(amount1).to.eq(0)
})
it('returns 0 if prices are equal', async () => {
const amount1 = await sqrtPriceMath.getAmount0Delta(encodePriceSqrt(1, 1), encodePriceSqrt(1, 1), 0, true)
expect(amount1).to.eq(0)
})
it('returns 0.1 amount1 for price of 1 to 1.21', async () => {
const amount1 = await sqrtPriceMath.getAmount1Delta(
encodePriceSqrt(1, 1),
encodePriceSqrt(121, 100),
expandTo18Decimals(1),
true
)
expect(amount1).to.eq('100000000000000000')
const amount1RoundedDown = await sqrtPriceMath.getAmount1Delta(
encodePriceSqrt(1, 1),
encodePriceSqrt(121, 100),
expandTo18Decimals(1),
false
)
expect(amount1RoundedDown).to.eq(amount1.sub(1))
})
it(`gas cost for amount0 where roundUp = true`, async () => {
await snapshotGasCost(
sqrtPriceMath.getGasCostOfGetAmount0Delta(
encodePriceSqrt(100, 121),
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
true
)
)
})
it(`gas cost for amount0 where roundUp = false`, async () => {
await snapshotGasCost(
sqrtPriceMath.getGasCostOfGetAmount0Delta(
encodePriceSqrt(100, 121),
encodePriceSqrt(1, 1),
expandTo18Decimals(1),
false
)
)
})
})
describe('swap computation', () => {
it('sqrtP * sqrtQ overflows', async () => {
// getNextSqrtPriceInvariants(1025574284609383690408304870162715216695788925244,50015962439936049619261659728067971248,406,true)
const sqrtP = '1025574284609383690408304870162715216695788925244'
const liquidity = '50015962439936049619261659728067971248'
const zeroForOne = true
const amountIn = '406'
const sqrtQ = await sqrtPriceMath.getNextSqrtPriceFromInput(sqrtP, liquidity, amountIn, zeroForOne)
expect(sqrtQ).to.eq('1025574284609383582644711336373707553698163132913')
const amount0Delta = await sqrtPriceMath.getAmount0Delta(sqrtQ, sqrtP, liquidity, true)
expect(amount0Delta).to.eq('406')
})
})
})