Skip to content

Commit

Permalink
add static methods to exchange...
Browse files Browse the repository at this point in the history
... for calculating liquidity-related values

exchange address -> Token
  • Loading branch information
NoahZinsmeister committed Feb 27, 2020
1 parent dbd3ae4 commit 4b5ca54
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 5 deletions.
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ export enum TradeType {
// exports for internal consumption
export const ZERO = JSBI.BigInt(0)
export const ONE = JSBI.BigInt(1)
export const TWO = JSBI.BigInt(2)
export const THREE = JSBI.BigInt(3)
export const FIVE = JSBI.BigInt(5)
export const TEN = JSBI.BigInt(10)
export const _100 = JSBI.BigInt(100)
export const _997 = JSBI.BigInt(997)
export const _1000 = JSBI.BigInt(1000)
export const MINIMUM_LIQUIDITY = _1000

export enum SolidityType {
uint8 = 'uint8',
Expand Down
75 changes: 72 additions & 3 deletions src/entities/exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import { getNetwork } from '@ethersproject/networks'
import { getDefaultProvider } from '@ethersproject/providers'
import { Contract } from '@ethersproject/contracts'

import { FACTORY_ADDRESS, INIT_CODE_HASH, ZERO, ONE, _997, _1000 } from '../constants'
import { BigintIsh } from '../types'
import { FACTORY_ADDRESS, INIT_CODE_HASH, ZERO, ONE, FIVE, _997, _1000, MINIMUM_LIQUIDITY } from '../constants'
import ERC20 from '../abis/ERC20.json'
import { sqrt, parseBigintIsh } from '../utils'
import { InsufficientReservesError, InsufficientInputAmountError } from '../errors'
import { Token } from './token'
import { TokenAmount } from './fractions/tokenAmount'

let CACHE: { [token0Address: string]: { [token1Address: string]: string } } = {}

export class Exchange {
public readonly address: string
public readonly liquidityToken: Token
private readonly tokenAmounts: [TokenAmount, TokenAmount]

static getAddress(tokenA: Token, tokenB: Token): string {
Expand Down Expand Up @@ -55,7 +57,13 @@ export class Exchange {
const tokenAmounts = tokenAmountA.token.sortsBefore(tokenAmountB.token) // does safety checks
? [tokenAmountA, tokenAmountB]
: [tokenAmountB, tokenAmountA]
this.address = Exchange.getAddress(tokenAmounts[0].token, tokenAmounts[1].token)
this.liquidityToken = new Token(
tokenAmounts[0].token.chainId,
Exchange.getAddress(tokenAmounts[0].token, tokenAmounts[1].token),
18,
'UNI-V2',
'Uniswap V2'
)
this.tokenAmounts = tokenAmounts as [TokenAmount, TokenAmount]
}

Expand Down Expand Up @@ -120,4 +128,65 @@ export class Exchange {
)
return [inputAmount, new Exchange(inputReserve.add(inputAmount), outputReserve.subtract(outputAmount))]
}

getLiquidityMinted(totalSupply: TokenAmount, tokenAmountA: TokenAmount, tokenAmountB: TokenAmount): TokenAmount {
invariant(totalSupply.token.equals(this.liquidityToken), 'LIQUIDITY')
const tokenAmounts = tokenAmountA.token.sortsBefore(tokenAmountB.token) // does safety checks
? [tokenAmountA, tokenAmountB]
: [tokenAmountB, tokenAmountA]
invariant(tokenAmounts[0].token.equals(this.token0) && tokenAmounts[1].token.equals(this.token1), 'TOKEN')

let liquidity: JSBI
if (JSBI.equal(totalSupply.raw, ZERO)) {
liquidity = JSBI.subtract(sqrt(JSBI.multiply(tokenAmounts[0].raw, tokenAmounts[1].raw)), MINIMUM_LIQUIDITY)
} else {
const amount0 = JSBI.divide(JSBI.multiply(tokenAmounts[0].raw, totalSupply.raw), this.reserve0.raw)
const amount1 = JSBI.divide(JSBI.multiply(tokenAmounts[1].raw, totalSupply.raw), this.reserve1.raw)
liquidity = JSBI.lessThanOrEqual(amount0, amount1) ? amount0 : amount1
}
if (!JSBI.greaterThan(liquidity, ZERO)) {
throw new InsufficientInputAmountError()
}
return new TokenAmount(this.liquidityToken, liquidity)
}

getLiquidityValue(
token: Token,
totalSupply: TokenAmount,
liquidity: TokenAmount,
feeOn: boolean = false,
kLast?: BigintIsh
): TokenAmount {
invariant(token.equals(this.token0) || token.equals(this.token1), 'TOKEN')
invariant(totalSupply.token.equals(this.liquidityToken), 'TOTAL_SUPPLY')
invariant(liquidity.token.equals(this.liquidityToken), 'LIQUIDITY')
invariant(JSBI.lessThanOrEqual(liquidity.raw, totalSupply.raw), 'LIQUIDITY')

let totalSupplyAdjusted: TokenAmount
if (!feeOn) {
totalSupplyAdjusted = totalSupply
} else {
invariant(!!kLast, 'K_LAST')
const kLastParsed = parseBigintIsh(kLast as any)
if (!JSBI.equal(kLastParsed, ZERO)) {
const rootK = sqrt(JSBI.multiply(this.reserve0.raw, this.reserve1.raw))
const rootKLast = sqrt(kLastParsed)
if (JSBI.greaterThan(rootK, rootKLast)) {
const numerator = JSBI.multiply(totalSupply.raw, JSBI.subtract(rootK, rootKLast))
const denominator = JSBI.add(JSBI.multiply(rootK, FIVE), rootKLast)
const feeLiquidity = JSBI.divide(numerator, denominator)
totalSupplyAdjusted = totalSupply.add(new TokenAmount(this.liquidityToken, feeLiquidity))
} else {
totalSupplyAdjusted = totalSupply
}
} else {
totalSupplyAdjusted = totalSupply
}
}

return new TokenAmount(
token,
JSBI.divide(JSBI.multiply(liquidity.raw, this.reserveOf(token).raw), totalSupplyAdjusted.raw)
)
}
}
20 changes: 19 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import JSBI from 'jsbi'
import { getAddress } from '@ethersproject/address'

import { BigintIsh } from './types'
import { ZERO, SolidityType, SOLIDITY_TYPE_MAXIMA } from './constants'
import { ZERO, ONE, TWO, THREE, SolidityType, SOLIDITY_TYPE_MAXIMA } from './constants'

export function validateSolidityTypeInstance(value: JSBI, solidityType: SolidityType): void {
invariant(JSBI.greaterThanOrEqual(value, ZERO), `${value} is not a ${solidityType}.`)
Expand All @@ -29,3 +29,21 @@ export function parseBigintIsh(bigintIsh: BigintIsh): JSBI {
? JSBI.BigInt(bigintIsh.toString())
: JSBI.BigInt(bigintIsh)
}

// mock the on-chain sqrt function
export function sqrt(y: JSBI): JSBI {
validateSolidityTypeInstance(y, SolidityType.uint256)
let z: JSBI = ZERO
let x: JSBI
if (JSBI.greaterThan(y, THREE)) {
z = y
x = JSBI.add(JSBI.divide(y, TWO), ONE)
while (JSBI.lessThan(x, z)) {
z = x
x = JSBI.divide(JSBI.add(JSBI.divide(y, x), x), TWO)
}
} else if (JSBI.notEqual(y, ZERO)) {
z = ONE
}
return z
}
2 changes: 1 addition & 1 deletion test/data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ describe('data', () => {
it('Exchange', async () => {
const token = new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18) // DAI
const exchange = await Exchange.fetchData(WETH[ChainId.RINKEBY], token)
expect(exchange.address).toEqual('0xC0568fA2FC63123B7352c506076DFa5623D62Db5')
expect(exchange.liquidityToken.address).toEqual('0xC0568fA2FC63123B7352c506076DFa5623D62Db5')
})
})
106 changes: 106 additions & 0 deletions test/miscellaneous.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ChainId, Token, TokenAmount, Exchange, InsufficientInputAmountError } from '../src'

describe('miscellaneous', () => {
it('getLiquidityMinted:0', async () => {
const tokenA = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000001', 18)
const tokenB = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000002', 18)
const exchange = new Exchange(new TokenAmount(tokenA, '0'), new TokenAmount(tokenB, '0'))

expect(() => {
exchange.getLiquidityMinted(
new TokenAmount(exchange.liquidityToken, '0'),
new TokenAmount(tokenA, '1000'),
new TokenAmount(tokenB, '1000')
)
}).toThrow(InsufficientInputAmountError)

expect(() => {
exchange.getLiquidityMinted(
new TokenAmount(exchange.liquidityToken, '0'),
new TokenAmount(tokenA, '1000000'),
new TokenAmount(tokenB, '1')
)
}).toThrow(InsufficientInputAmountError)

const liquidity = exchange.getLiquidityMinted(
new TokenAmount(exchange.liquidityToken, '0'),
new TokenAmount(tokenA, '1001'),
new TokenAmount(tokenB, '1001')
)

expect(liquidity.raw.toString()).toEqual('1')
})

it('getLiquidityMinted:!0', async () => {
const tokenA = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000001', 18)
const tokenB = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000002', 18)
const exchange = new Exchange(new TokenAmount(tokenA, '10000'), new TokenAmount(tokenB, '10000'))

expect(
exchange
.getLiquidityMinted(
new TokenAmount(exchange.liquidityToken, '10000'),
new TokenAmount(tokenA, '2000'),
new TokenAmount(tokenB, '2000')
)
.raw.toString()
).toEqual('2000')
})

it('getLiquidityValue:!feeOn', async () => {
const tokenA = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000001', 18)
const tokenB = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000002', 18)
const exchange = new Exchange(new TokenAmount(tokenA, '1000'), new TokenAmount(tokenB, '1000'))

{
const liquidityValue = exchange.getLiquidityValue(
tokenA,
new TokenAmount(exchange.liquidityToken, '1000'),
new TokenAmount(exchange.liquidityToken, '1000'),
false
)
expect(liquidityValue.token.equals(tokenA)).toBe(true)
expect(liquidityValue.raw.toString()).toBe('1000')
}

// 500
{
const liquidityValue = exchange.getLiquidityValue(
tokenA,
new TokenAmount(exchange.liquidityToken, '1000'),
new TokenAmount(exchange.liquidityToken, '500'),
false
)
expect(liquidityValue.token.equals(tokenA)).toBe(true)
expect(liquidityValue.raw.toString()).toBe('500')
}

// tokenB
{
const liquidityValue = exchange.getLiquidityValue(
tokenB,
new TokenAmount(exchange.liquidityToken, '1000'),
new TokenAmount(exchange.liquidityToken, '1000'),
false
)
expect(liquidityValue.token.equals(tokenB)).toBe(true)
expect(liquidityValue.raw.toString()).toBe('1000')
}
})

it('getLiquidityValue:feeOn', async () => {
const tokenA = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000001', 18)
const tokenB = new Token(ChainId.RINKEBY, '0x0000000000000000000000000000000000000002', 18)
const exchange = new Exchange(new TokenAmount(tokenA, '1000'), new TokenAmount(tokenB, '1000'))

const liquidityValue = exchange.getLiquidityValue(
tokenA,
new TokenAmount(exchange.liquidityToken, '500'),
new TokenAmount(exchange.liquidityToken, '500'),
true,
'250000' // 500 ** 2
)
expect(liquidityValue.token.equals(tokenA)).toBe(true)
expect(liquidityValue.raw.toString()).toBe('917') // ceiling(1000 - (500 * (1 / 6)))
})
})

0 comments on commit 4b5ca54

Please sign in to comment.