380 lines
13 KiB
TypeScript
380 lines
13 KiB
TypeScript
|
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')
|
||
|
})
|
||
|
})
|
||
|
})
|