From 07eb4e6563ab464f8d0b4a4df4aeedfe814e3b9a Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Sun, 20 Feb 2022 20:13:51 +0000 Subject: [PATCH 01/11] Activated Gyro2 pool. --- src/pools/index.ts | 7 ++++- src/router/helpersClass.ts | 45 ++++++++++++------------------ test/gyro2Pool.spec.ts | 57 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/src/pools/index.ts b/src/pools/index.ts index 45ea9bc5..a1f37a71 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -4,6 +4,7 @@ import { MetaStablePool } from './metaStablePool/metaStablePool'; import { LinearPool } from './linearPool/linearPool'; import { ElementPool } from './elementPool/elementPool'; import { PhantomStablePool } from './phantomStablePool/phantomStablePool'; +import { Gyro2Pool } from './gyro2Pool/gyro2Pool'; import { BigNumber as OldBigNumber, INFINITY, @@ -28,6 +29,7 @@ export function parseNewPool( | LinearPool | MetaStablePool | PhantomStablePool + | Gyro2Pool | undefined { // We're not interested in any pools which don't allow swapping if (!pool.swapEnabled) return undefined; @@ -38,7 +40,8 @@ export function parseNewPool( | ElementPool | LinearPool | MetaStablePool - | PhantomStablePool; + | PhantomStablePool + | Gyro2Pool; try { if ( @@ -58,6 +61,8 @@ export function parseNewPool( newPool = LinearPool.fromPool(pool); else if (pool.poolType === 'StablePhantom') newPool = PhantomStablePool.fromPool(pool); + else if (pool.poolType === 'Gyro2Pool') + newPool = Gyro2Pool.fromPool(pool); else { console.error( `Unknown pool type or type field missing: ${pool.poolType} ${pool.id}` diff --git a/src/router/helpersClass.ts b/src/router/helpersClass.ts index f80be7b8..96c58ff8 100644 --- a/src/router/helpersClass.ts +++ b/src/router/helpersClass.ts @@ -326,51 +326,40 @@ export function EVMgetOutputAmountSwap( return INFINITY; } if (swapType === SwapTypes.SwapExactIn) { - // TODO we will be able to remove pooltype check once Element EVM maths is available - if ( - pool.poolType === PoolTypes.Weighted || - pool.poolType === PoolTypes.Stable || - pool.poolType === PoolTypes.MetaStable || - pool.poolType === PoolTypes.Linear - ) { + if (pool.poolType === PoolTypes.Element) { + // TODO this will just be part of below once maths available + returnAmount = getOutputAmountSwap( + pool, + poolPairData, + swapType, + amount + ); + } else if (pool.poolType in PoolTypes) { // Will accept/return normalised values returnAmount = pool._exactTokenInForTokenOut( poolPairData, amount, true ); - } else if (pool.poolType === PoolTypes.Element) { - // TODO this will just be part of above once maths available + } else { + throw Error('Unsupported swap'); + } + } else { + if (pool.poolType === PoolTypes.Element) { + // TODO this will just be part of below once maths available returnAmount = getOutputAmountSwap( pool, poolPairData, swapType, amount ); - } else { - throw Error('Unsupported swap'); - } - } else { - // TODO we will be able to remove pooltype check once Element EVM maths is available - if ( - pool.poolType === PoolTypes.Weighted || - pool.poolType === PoolTypes.Stable || - pool.poolType === PoolTypes.MetaStable || - pool.poolType === PoolTypes.Linear - ) { + } else if (pool.poolType in PoolTypes) { + // Will accept/return normalised values returnAmount = pool._tokenInForExactTokenOut( poolPairData, amount, true ); - } else if (pool.poolType === PoolTypes.Element) { - // TODO this will just be part of above once maths available - returnAmount = getOutputAmountSwap( - pool, - poolPairData, - swapType, - amount - ); } else { throw Error('Unsupported swap'); } diff --git a/test/gyro2Pool.spec.ts b/test/gyro2Pool.spec.ts index 74a7c53e..e7353f6a 100644 --- a/test/gyro2Pool.spec.ts +++ b/test/gyro2Pool.spec.ts @@ -1,13 +1,17 @@ +import 'dotenv/config'; import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { JsonRpcProvider } from '@ethersproject/providers'; import { bnum } from '../src/utils/bignumber'; -import { USDC, DAI } from './lib/constants'; -import { SwapTypes } from '../src'; +import { USDC, DAI, sorConfigEth } from './lib/constants'; +import { SwapTypes, SOR, SwapInfo } from '../src'; // Add new PoolType import { Gyro2Pool } from '../src/pools/gyro2Pool/gyro2Pool'; // Add new pool test data in Subgraph Schema format import testPools from './testData/gyro2Pools/gyro2TestPool.json'; +import { MockPoolDataService } from './lib/mockPoolDataService'; +import { mockTokenPriceService } from './lib/mockTokenPriceService'; describe('Gyro2Pool tests USDC > DAI', () => { const testPool = cloneDeep(testPools).pools[0]; @@ -143,5 +147,54 @@ describe('Gyro2Pool tests USDC > DAI', () => { expect(derivative.toString()).to.eq('0.000000903885627537'); }); }); + + context('FullSwap', () => { + it(`Full Swap - swapExactIn, Token>Token`, async () => { + const pools = cloneDeep(testPools.pools); + const tokenIn = USDC.address; + const tokenOut = DAI.address; + const swapType = SwapTypes.SwapExactIn; + const swapAmt = parseFixed('13.5', 6); + + const gasPrice = parseFixed('30', 9); + const maxPools = 4; + const provider = new JsonRpcProvider( + `https://mainnet.infura.io/v3/${process.env.INFURA}` + ); + + const sor = new SOR( + provider, + sorConfigEth, + new MockPoolDataService(pools), + mockTokenPriceService + ); + const fetchSuccess = await sor.fetchPools(); + expect(fetchSuccess).to.be.true; + + const swapInfo: SwapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmt, + { gasPrice, maxPools } + ); + + console.log(`Return amt:`); + console.log(swapInfo.returnAmount.toString()); + // This value is hard coded as sanity check if things unexpectedly change. Taken from V2 test run (with extra fee logic added). + // TO DO - expect(swapInfo.returnAmount.toString()).eq('999603'); + expect(swapInfo.swaps.length).eq(1); + expect(swapInfo.swaps[0].amount.toString()).eq( + swapAmt.toString() + ); + expect(swapInfo.swaps[0].poolId).eq(testPools.pools[0].id); + expect( + swapInfo.tokenAddresses[swapInfo.swaps[0].assetInIndex] + ).eq(tokenIn); + expect( + swapInfo.tokenAddresses[swapInfo.swaps[0].assetOutIndex] + ).eq(tokenOut); + }); + }); }); }); From f0e5c0c1cc13c0436cbb683dea9ddd15e798bb73 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 18 May 2022 11:06:20 +0100 Subject: [PATCH 02/11] Update code based off beta Subgraph. --- src/pools/index.ts | 9 ++++++--- src/types.ts | 2 ++ test/lib/onchainData.ts | 6 ++++++ test/lib/subgraphPoolDataService.ts | 4 ++++ test/testData/gyro2Pools/gyro2TestPool.json | 2 +- test/testData/gyro3Pools/gyro3TestPool.json | 2 +- test/testScripts/swapExample.ts | 20 +++++++++++++++----- 7 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/pools/index.ts b/src/pools/index.ts index c2e7820f..f6fc832f 100644 --- a/src/pools/index.ts +++ b/src/pools/index.ts @@ -5,6 +5,7 @@ import { LinearPool } from './linearPool/linearPool'; import { ElementPool } from './elementPool/elementPool'; import { PhantomStablePool } from './phantomStablePool/phantomStablePool'; import { Gyro2Pool } from './gyro2Pool/gyro2Pool'; +import { Gyro3Pool } from './gyro3Pool/gyro3Pool'; import { BigNumber as OldBigNumber, INFINITY, @@ -30,6 +31,7 @@ export function parseNewPool( | MetaStablePool | PhantomStablePool | Gyro2Pool + | Gyro3Pool | undefined { // We're not interested in any pools which don't allow swapping if (!pool.swapEnabled) return undefined; @@ -41,7 +43,8 @@ export function parseNewPool( | LinearPool | MetaStablePool | PhantomStablePool - | Gyro2Pool; + | Gyro2Pool + | Gyro3Pool; try { if (pool.poolType === 'Weighted' || pool.poolType === 'Investment') { @@ -59,8 +62,8 @@ export function parseNewPool( newPool = LinearPool.fromPool(pool); else if (pool.poolType === 'StablePhantom') newPool = PhantomStablePool.fromPool(pool); - else if (pool.poolType === 'Gyro2Pool') - newPool = Gyro2Pool.fromPool(pool); + else if (pool.poolType === 'Gyro2') newPool = Gyro2Pool.fromPool(pool); + else if (pool.poolType === 'Gyro3') newPool = Gyro3Pool.fromPool(pool); else { console.error( `Unknown pool type or type field missing: ${pool.poolType} ${pool.id}` diff --git a/src/types.ts b/src/types.ts index c9ae56f1..7e8e2a21 100644 --- a/src/types.ts +++ b/src/types.ts @@ -164,6 +164,8 @@ export enum PoolFilter { AaveLinear = 'AaveLinear', StablePhantom = 'StablePhantom', ERC4626Linear = 'ERC4626Linear', + Gyro2 = 'Gyro2', + Gyro3 = 'Gyro3', } export interface PoolBase { diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index 1071c2c3..6899857f 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -98,6 +98,12 @@ export async function getOnChainBalances( pool.address, 'getWrappedTokenRate' ); + } else if (pool.poolType.toString().includes('Gyro')) { + multiPool.call( + `${pool.id}.swapFee`, + pool.address, + 'getSwapFeePercentage' + ); } }); diff --git a/test/lib/subgraphPoolDataService.ts b/test/lib/subgraphPoolDataService.ts index 92c6c0c8..8470e258 100644 --- a/test/lib/subgraphPoolDataService.ts +++ b/test/lib/subgraphPoolDataService.ts @@ -35,6 +35,8 @@ const queryWithLinear = ` mainIndex lowerTarget upperTarget + gyro2PriceBounds + gyro3PriceBounds } pool1000: pools( first: 1000, @@ -67,6 +69,8 @@ const queryWithLinear = ` mainIndex lowerTarget upperTarget + gyro2PriceBounds + gyro3PriceBounds } } `; diff --git a/test/testData/gyro2Pools/gyro2TestPool.json b/test/testData/gyro2Pools/gyro2TestPool.json index 554e0108..dde61db0 100644 --- a/test/testData/gyro2Pools/gyro2TestPool.json +++ b/test/testData/gyro2Pools/gyro2TestPool.json @@ -38,7 +38,7 @@ ], "totalWeight": "15", "totalShares": "100", - "poolType": "Gyro2Pool", + "poolType": "Gyro2", "swapEnabled": true, "gyro2PriceBounds": { "lowerBound": "0.999", diff --git a/test/testData/gyro3Pools/gyro3TestPool.json b/test/testData/gyro3Pools/gyro3TestPool.json index 066b4d90..b7b9f077 100644 --- a/test/testData/gyro3Pools/gyro3TestPool.json +++ b/test/testData/gyro3Pools/gyro3TestPool.json @@ -47,7 +47,7 @@ ], "totalWeight": "15", "totalShares": "100", - "poolType": "Gyro3Pool", + "poolType": "Gyro3", "swapEnabled": true, "gyro3PriceBounds": { "alpha": "0.987" diff --git a/test/testScripts/swapExample.ts b/test/testScripts/swapExample.ts index 7becc83b..61bb2970 100644 --- a/test/testScripts/swapExample.ts +++ b/test/testScripts/swapExample.ts @@ -99,7 +99,7 @@ export const SUBGRAPH_URLS = { [Network.GOERLI]: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-goerli-v2', [Network.KOVAN]: - 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-kovan-v2', + 'https://api.thegraph.com/subgraphs/name/mendesfabio/balancer-kovan-v2', [Network.POLYGON]: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-polygon-v2', [Network.ARBITRUM]: `https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-arbitrum-v2`, @@ -256,6 +256,16 @@ export const ADDRESSES = { decimals: 18, symbol: 'STABAL3', }, + GYRO1: { + address: '0x11fb9071e69628d804bf0b197cc61eeacd4aaecf', + decimals: 18, + symbol: 'GYRO1', + }, + GYRO2: { + address: '0x4ea2110a3e277b10c9b098f61d72f58efa8655db', + decimals: 18, + symbol: 'GYRO2', + }, }, [Network.POLYGON]: { MATIC: { @@ -714,13 +724,13 @@ async function makeRelayerTrade( } export async function simpleSwap() { - const networkId = Network.MAINNET; + const networkId = Network.KOVAN; // Pools source can be Subgraph URL or pools data set passed directly // Update pools list with most recent onchain balances - const tokenIn = ADDRESSES[networkId].DAI; - const tokenOut = ADDRESSES[networkId].WETH; + const tokenIn = ADDRESSES[networkId].GYRO1; + const tokenOut = ADDRESSES[networkId].GYRO2; const swapType = SwapTypes.SwapExactIn; - const swapAmount = parseFixed('1000000', 18); + const swapAmount = parseFixed('1', 18); const executeTrade = true; const provider = new JsonRpcProvider(PROVIDER_URLS[networkId]); From 0e7993dcae11c7ba850da66ef1b9a1162b7b5f89 Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Wed, 25 May 2022 12:48:29 +0100 Subject: [PATCH 03/11] Update Gyro priceBounds to interface with subgraph" --- src/pools/gyro2Pool/gyro2Pool.ts | 65 ++++++--------------- src/pools/gyro3Pool/gyro3Pool.ts | 19 ++---- src/types.ts | 17 +----- test/gyro2Math.spec.ts | 20 ++++--- test/gyro2Pool.spec.ts | 30 ++++++---- test/gyro3Math.spec.ts | 30 ++++------ test/gyro3Pool.spec.ts | 23 +++++--- test/lib/subgraphPoolDataService.ts | 10 ++-- test/testData/gyro2Pools/gyro2TestPool.json | 8 +-- test/testData/gyro3Pools/gyro3TestPool.json | 4 +- 10 files changed, 93 insertions(+), 133 deletions(-) diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index 96dce4be..560fff9c 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -10,7 +10,6 @@ import { SubgraphToken, SwapTypes, SubgraphPoolBase, - Gyro2PriceBounds, } from '../../types'; import { isSameAddress } from '../../utils'; import { @@ -46,35 +45,18 @@ export class Gyro2Pool implements PoolBase { tokens: Gyro2PoolToken[]; swapFee: BigNumber; totalShares: BigNumber; - priceBounds: Gyro2PriceBounds; + sqrtAlpha: BigNumber; + sqrtBeta: BigNumber; // Max In/Out Ratios MAX_IN_RATIO = parseFixed('0.3', 18); MAX_OUT_RATIO = parseFixed('0.3', 18); static fromPool(pool: SubgraphPoolBase): Gyro2Pool { - if (!pool.gyro2PriceBounds) - throw new Error('Pool missing gyro2PriceBounds'); - - const { lowerBound, upperBound } = pool.gyro2PriceBounds; - if (Number(lowerBound) <= 0 || Number(upperBound) <= 0) - throw new Error('Invalid price bounds in gyro2PriceBounds'); - - const tokenInAddress = pool.gyro2PriceBounds.tokenInAddress; - const tokenOutAddress = pool.gyro2PriceBounds.tokenOutAddress; - - const tokenInIndex = pool.tokens.findIndex( - (t) => getAddress(t.address) === getAddress(tokenInAddress) - ); - const tokenOutIndex = pool.tokens.findIndex( - (t) => getAddress(t.address) === getAddress(tokenOutAddress) - ); - - if (tokenInIndex < 0) - throw new Error('Gyro2Pool priceBounds tokenIn not in tokens'); - - if (tokenOutIndex < 0) - throw new Error('Gyro2Pool priceBounds tokenOut not in tokens'); + if (!pool.sqrtAlpha || !pool.sqrtBeta) + throw new Error( + 'Pool missing Gyro2 sqrtAlpha and/or sqrtBeta params' + ); return new Gyro2Pool( pool.id, @@ -83,7 +65,8 @@ export class Gyro2Pool implements PoolBase { pool.totalShares, pool.tokens as Gyro2PoolToken[], pool.tokensList, - pool.gyro2PriceBounds as Gyro2PriceBounds + pool.sqrtAlpha, + pool.sqrtBeta ); } @@ -94,7 +77,8 @@ export class Gyro2Pool implements PoolBase { totalShares: string, tokens: Gyro2PoolToken[], tokensList: string[], - priceBounds: Gyro2PriceBounds + sqrtAlpha: BigNumber, + sqrtBeta: BigNumber ) { this.id = id; this.address = address; @@ -102,7 +86,8 @@ export class Gyro2Pool implements PoolBase { this.totalShares = parseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; - this.priceBounds = priceBounds; + this.sqrtAlpha = sqrtAlpha; + this.sqrtBeta = sqrtBeta; } parsePoolPairData(tokenIn: string, tokenOut: string): Gyro2PoolPairData { @@ -122,23 +107,7 @@ export class Gyro2Pool implements PoolBase { const balanceOut = tO.balance; const decimalsOut = tO.decimals; - const sqrtAlpha = isSameAddress( - tI.address, - this.priceBounds.tokenInAddress - ) - ? _squareRoot(parseFixed(this.priceBounds.lowerBound, 18)) - : _squareRoot( - ONE.mul(ONE).div(parseFixed(this.priceBounds.upperBound, 18)) - ); - - const sqrtBeta = isSameAddress( - tI.address, - this.priceBounds.tokenInAddress - ) - ? _squareRoot(parseFixed(this.priceBounds.upperBound, 18)) - : _squareRoot( - ONE.mul(ONE).div(parseFixed(this.priceBounds.lowerBound, 18)) - ); + const tokenInIsToken0 = tokenInIndex === 0; const poolPairData: Gyro2PoolPairData = { id: this.id, @@ -151,8 +120,12 @@ export class Gyro2Pool implements PoolBase { balanceIn: parseFixed(balanceIn, decimalsIn), balanceOut: parseFixed(balanceOut, decimalsOut), swapFee: this.swapFee, - sqrtAlpha, - sqrtBeta, + sqrtAlpha: tokenInIsToken0 + ? this.sqrtAlpha + : ONE.mul(ONE).div(this.sqrtBeta), + sqrtBeta: tokenInIsToken0 + ? this.sqrtBeta + : ONE.mul(ONE).div(this.sqrtAlpha), }; return poolPairData; diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index 41f702d0..a76ba628 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -10,7 +10,6 @@ import { SubgraphToken, SwapTypes, SubgraphPoolBase, - Gyro3PriceBounds, } from '../../types'; import { isSameAddress } from '../../utils'; import { @@ -59,12 +58,10 @@ export class Gyro3Pool implements PoolBase { } static fromPool(pool: SubgraphPoolBase): Gyro3Pool { - if (!pool.gyro3PriceBounds) - throw new Error('Pool missing gyro3PriceBounds'); + if (!pool.root3Alpha) throw new Error('Pool missing root3Alpha'); - const { alpha } = pool.gyro3PriceBounds; - if (!(Number(alpha) > 0 && Number(alpha) < 1)) - throw new Error('Invalid alpha price bound in gyro3PriceBounds'); + if (!(pool.root3Alpha.gt(0) && pool.root3Alpha.lt(ONE))) + throw new Error('Invalid root3Alpha parameter'); if (pool.tokens.length !== 3) throw new Error('Gyro3Pool must contain three tokens only'); @@ -76,7 +73,7 @@ export class Gyro3Pool implements PoolBase { pool.totalShares, pool.tokens as Gyro3PoolToken[], pool.tokensList, - pool.gyro3PriceBounds as Gyro3PriceBounds + pool.root3Alpha ); } @@ -87,7 +84,7 @@ export class Gyro3Pool implements PoolBase { totalShares: string, tokens: Gyro3PoolToken[], tokensList: string[], - priceBounds: Gyro3PriceBounds + root3Alpha: BigNumber ) { this.id = id; this.address = address; @@ -95,12 +92,6 @@ export class Gyro3Pool implements PoolBase { this.totalShares = parseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; - - const root3Alpha = parseFixed( - Math.pow(Number(priceBounds.alpha), 1 / 3).toString(), - 18 - ); - this.root3Alpha = root3Alpha; } diff --git a/src/types.ts b/src/types.ts index 7e8e2a21..b2cbceb6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,10 +94,11 @@ export interface SubgraphPoolBase { upperTarget?: string; // Gyro2 specific field - gyro2PriceBounds?: Gyro2PriceBounds; + sqrtAlpha?: BigNumber; + sqrtBeta?: BigNumber; // Gyro3 specific field - gyro3PriceBounds?: Gyro3PriceBounds; + root3Alpha?: BigNumber; } export type SubgraphToken = { @@ -228,15 +229,3 @@ export interface TokenPriceService { export interface PoolDataService { getPools(): Promise; } - -export type Gyro2PriceBounds = { - lowerBound: string; - upperBound: string; - tokenInAddress: string; - tokenOutAddress: string; -}; - -export type Gyro3PriceBounds = { - alpha: string; // Assume symmetric price bounds for Gyro 3 pool - // (The price range for any asset pair is equal to [alpha, 1/alpha]) -}; diff --git a/test/gyro2Math.spec.ts b/test/gyro2Math.spec.ts index 6aad772a..d431bf70 100644 --- a/test/gyro2Math.spec.ts +++ b/test/gyro2Math.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; import { USDC, DAI } from './lib/constants'; // Add new PoolType import { Gyro2Pool } from '../src/pools/gyro2Pool/gyro2Pool'; @@ -17,8 +17,9 @@ import { } from '../src/pools/gyro2Pool/gyro2Math'; describe('gyro2Math tests', () => { - const testPool = cloneDeep(testPools).pools[0]; + const testPool: any = cloneDeep(testPools).pools[0]; const pool = Gyro2Pool.fromPool(testPool); + updatePoolParams(testPool); const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); @@ -50,13 +51,13 @@ describe('gyro2Math tests', () => { poolPairData.sqrtBeta ); - expect(formatFixed(a, 18)).to.equal('0.000999500499625375'); - expect(formatFixed(mb, 18)).to.equal('2230.884220610725033295'); + expect(formatFixed(a, 18)).to.equal('0.00099950047470021'); + expect(formatFixed(mb, 18)).to.equal('2230.884220626971757449'); expect(formatFixed(mc, 18)).to.equal('1232000.0'); const L = _calculateQuadratic(a, mb, mc); - expect(formatFixed(L, 18)).to.equal('2232551.215824107930236259'); + expect(formatFixed(L, 18)).to.equal('2232551.271501112084098627'); }); it(`should correctly calculate virtual parameters`, async () => { @@ -66,8 +67,13 @@ describe('gyro2Math tests', () => { poolPairData.sqrtBeta ); - expect(formatFixed(a, 18)).to.equal('2231434.661007672178972479'); - expect(formatFixed(b, 18)).to.equal('2231435.776725839468265783'); + expect(formatFixed(a, 18)).to.equal('2231434.660924038777489798'); + expect(formatFixed(b, 18)).to.equal('2231435.776865147462654764'); }); }); }); + +function updatePoolParams(pool) { + pool.sqrtAlpha = BigNumber.from(pool.sqrtAlpha); + pool.sqrtBeta = BigNumber.from(pool.sqrtBeta); +} diff --git a/test/gyro2Pool.spec.ts b/test/gyro2Pool.spec.ts index 1f1c1d53..20912c25 100644 --- a/test/gyro2Pool.spec.ts +++ b/test/gyro2Pool.spec.ts @@ -2,7 +2,7 @@ import 'dotenv/config'; // TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyro2Pool.spec.ts import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; import { JsonRpcProvider } from '@ethersproject/providers'; import { bnum } from '../src/utils/bignumber'; import { USDC, DAI, sorConfigEth } from './lib/constants'; @@ -15,7 +15,9 @@ import { MockPoolDataService } from './lib/mockPoolDataService'; import { mockTokenPriceService } from './lib/mockTokenPriceService'; describe('Gyro2Pool tests USDC > DAI', () => { - const testPool = cloneDeep(testPools).pools[0]; + const testPool: any = cloneDeep(testPools).pools[0]; + updatePoolParams(testPool); + const pool = Gyro2Pool.fromPool(testPool); const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); @@ -76,7 +78,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { pool.getNormalizedLiquidity(poolPairData); expect(Number(normalizedLiquidity.toString())).to.be.approximately( - 2252709.0423891, + 2252709.0984593313, 0.00001 ); }); @@ -86,7 +88,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { pool.getNormalizedLiquidity(poolPairData2); expect(Number(normalizedLiquidity.toString())).to.be.approximately( - 2252944.2752, + 2252944.3314978145, 0.00001 ); }); @@ -101,7 +103,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(amountOut.toString()).to.eq('13.379816829921106482'); + expect(amountOut.toString()).to.eq('13.379816831223949756'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = @@ -109,7 +111,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(newSpotPrice.toString()).to.eq('1.008988469289267733'); + expect(newSpotPrice.toString()).to.eq('1.008988469190824523'); }); it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { const derivative = @@ -117,7 +119,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(derivative.toString()).to.eq('0.000000895794710891'); + expect(derivative.toString()).to.eq('0.000000895794688507'); }); }); @@ -129,7 +131,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(amountIn.toString()).to.eq('45.977973900999006919'); + expect(amountIn.toString()).to.eq('45.977973896503961218'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = @@ -137,7 +139,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(newSpotPrice.toString()).to.eq('1.009017563096232974'); + expect(newSpotPrice.toString()).to.eq('1.00901756299705875'); }); it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { const derivative = @@ -145,13 +147,14 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(derivative.toString()).to.eq('0.000000903885627537'); + expect(derivative.toString()).to.eq('0.000000903885604863'); }); }); context('FullSwap', () => { it(`Full Swap - swapExactIn, Token>Token`, async () => { - const pools = cloneDeep(testPools.pools); + const pools: any = cloneDeep(testPools.pools); + pools.forEach(updatePoolParams); const tokenIn = USDC.address; const tokenOut = DAI.address; const swapType = SwapTypes.SwapExactIn; @@ -199,3 +202,8 @@ describe('Gyro2Pool tests USDC > DAI', () => { }); }); }); + +function updatePoolParams(pool) { + pool.sqrtAlpha = BigNumber.from(pool.sqrtAlpha); + pool.sqrtBeta = BigNumber.from(pool.sqrtBeta); +} diff --git a/test/gyro3Math.spec.ts b/test/gyro3Math.spec.ts index 64c1dc0c..1611fd3a 100644 --- a/test/gyro3Math.spec.ts +++ b/test/gyro3Math.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; import { USDC, DAI } from './lib/constants'; // Add new PoolType import { Gyro3Pool } from '../src/pools/gyro3Pool/gyro3Pool'; @@ -14,8 +14,14 @@ import { _normalizeBalances, } from '../src/pools/gyro3Pool/gyro3Math'; +function updatePoolParams(pool) { + pool.root3Alpha = BigNumber.from(pool.root3Alpha); +} + describe('gyro3Math tests', () => { - const testPool = cloneDeep(testPools).pools[0]; + const testPool: any = cloneDeep(testPools).pools[0]; + updatePoolParams(testPool); + const pool = Gyro3Pool.fromPool(testPool); const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); @@ -39,11 +45,9 @@ describe('gyro3Math tests', () => { pool.root3Alpha ); - expect(formatFixed(a, 18)).to.equal('0.013000000000000127'); - expect(formatFixed(mb, 18)).to.equal('245387.995391323689889516'); - expect(formatFixed(mc, 18)).to.equal( - '20335328582.7366683195765956' - ); + expect(formatFixed(a, 18)).to.equal('0.013000000252593788'); + expect(formatFixed(mb, 18)).to.equal('245387.995349457123073152'); + expect(formatFixed(mc, 18)).to.equal('20335328581.001924952'); expect(formatFixed(md, 18)).to.equal('561707977531810.0'); }); @@ -140,15 +144,3 @@ describe('gyro3Math tests', () => { }); }); }); - -// a = 0.013000000000000127 -// mb = 245387.995391323689889516 -// mc = 20335328582.7366683195765956 -// md = 561707977531810.0 - -// l0 = 18937948.911434007702525325 -// l1 = 18,958,673.94622423087229328 -// l2 = 18,958,628.782372398581970363 -// l3 = 18,958,628.782157684771854429 -// l4 = 18,958,628.782157684767400291 -// lFinal = 18958628.782157684762946293 diff --git a/test/gyro3Pool.spec.ts b/test/gyro3Pool.spec.ts index 5cc3d6c8..24daad15 100644 --- a/test/gyro3Pool.spec.ts +++ b/test/gyro3Pool.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; -import { formatFixed, parseFixed } from '@ethersproject/bignumber'; +import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; import { bnum } from '../src/utils/bignumber'; import { USDC, USDT } from './lib/constants'; import { SwapTypes } from '../src'; @@ -10,7 +10,8 @@ import { Gyro3Pool } from '../src/pools/gyro3Pool/gyro3Pool'; import testPools from './testData/gyro3Pools/gyro3TestPool.json'; describe('Gyro3Pool tests USDC > DAI', () => { - const testPool = cloneDeep(testPools).pools[0]; + const testPool: any = cloneDeep(testPools).pools[0]; + updatePoolParams(testPool); const pool = Gyro3Pool.fromPool(testPool); const poolPairData = pool.parsePoolPairData(USDT.address, USDC.address); @@ -55,7 +56,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { pool.getNormalizedLiquidity(poolPairData); expect(normalizedLiquidity.toString()).to.equal( - '19016283.981512596845077192' + '19016283.610415153991329405' ); }); }); @@ -69,7 +70,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(amountOut.toString()).to.eq('233.62822068392501182'); + expect(amountOut.toString()).to.eq('233.628220683475858449'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = @@ -77,7 +78,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(newSpotPrice.toString()).to.eq('1.003120202974438177'); + expect(newSpotPrice.toString()).to.eq('1.003120202976607933'); }); it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { const derivative = @@ -85,7 +86,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(derivative.toString()).to.eq('0.000000105499880184'); + expect(derivative.toString()).to.eq('0.000000105499882243'); }); }); @@ -97,7 +98,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(amountIn.toString()).to.eq('4538.618912825809655998'); + expect(amountIn.toString()).to.eq('4538.618912854584506276'); }); it('should correctly calculate newSpotPrice', async () => { @@ -106,7 +107,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(newSpotPrice.toString()).to.eq('1.003574353766587852'); + expect(newSpotPrice.toString()).to.eq('1.003574353777625146'); }); it('should correctly calculate derivative of spot price function at newSpotPrice', async () => { @@ -115,8 +116,12 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(derivative.toString()).to.eq('0.000000105900938637'); + expect(derivative.toString()).to.eq('0.000000105900940706'); }); }); }); }); + +function updatePoolParams(pool) { + pool.root3Alpha = BigNumber.from(pool.root3Alpha); +} diff --git a/test/lib/subgraphPoolDataService.ts b/test/lib/subgraphPoolDataService.ts index 8470e258..0dd3520b 100644 --- a/test/lib/subgraphPoolDataService.ts +++ b/test/lib/subgraphPoolDataService.ts @@ -35,8 +35,9 @@ const queryWithLinear = ` mainIndex lowerTarget upperTarget - gyro2PriceBounds - gyro3PriceBounds + sqrtAlpha + sqrtBeta + root3Alpha } pool1000: pools( first: 1000, @@ -69,8 +70,9 @@ const queryWithLinear = ` mainIndex lowerTarget upperTarget - gyro2PriceBounds - gyro3PriceBounds + sqrtAlpha + sqrtBeta + root3Alpha } } `; diff --git a/test/testData/gyro2Pools/gyro2TestPool.json b/test/testData/gyro2Pools/gyro2TestPool.json index dde61db0..77d9a208 100644 --- a/test/testData/gyro2Pools/gyro2TestPool.json +++ b/test/testData/gyro2Pools/gyro2TestPool.json @@ -40,12 +40,8 @@ "totalShares": "100", "poolType": "Gyro2", "swapEnabled": true, - "gyro2PriceBounds": { - "lowerBound": "0.999", - "upperBound": "1.001", - "tokenInAddress": "0x6b175474e89094c44da98b954eedeac495271d0f", - "tokenOutAddress": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" - } + "sqrtAlpha": "999499874900000000", + "sqrtBeta": "1000499875000000000" } ] } diff --git a/test/testData/gyro3Pools/gyro3TestPool.json b/test/testData/gyro3Pools/gyro3TestPool.json index b7b9f077..845afd82 100644 --- a/test/testData/gyro3Pools/gyro3TestPool.json +++ b/test/testData/gyro3Pools/gyro3TestPool.json @@ -49,9 +49,7 @@ "totalShares": "100", "poolType": "Gyro3", "swapEnabled": true, - "gyro3PriceBounds": { - "alpha": "0.987" - } + "root3Alpha": "995647752000000000" } ] } From 0bdddaa090884a50dcffd164dec44af21b2c0a47 Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Wed, 25 May 2022 13:17:41 +0100 Subject: [PATCH 04/11] Allow for BigDecimal params --- src/pools/gyro2Pool/gyro2Pool.ts | 8 ++++---- src/pools/gyro3Pool/gyro3Pool.ts | 9 ++++++--- src/types.ts | 6 +++--- test/gyro2Math.spec.ts | 6 ------ test/gyro2Pool.spec.ts | 10 +--------- test/gyro3Math.spec.ts | 5 ----- test/gyro3Pool.spec.ts | 5 ----- test/testData/gyro2Pools/gyro2TestPool.json | 4 ++-- test/testData/gyro3Pools/gyro3TestPool.json | 2 +- 9 files changed, 17 insertions(+), 38 deletions(-) diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index 560fff9c..d8080db2 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -77,8 +77,8 @@ export class Gyro2Pool implements PoolBase { totalShares: string, tokens: Gyro2PoolToken[], tokensList: string[], - sqrtAlpha: BigNumber, - sqrtBeta: BigNumber + sqrtAlpha: string, + sqrtBeta: string ) { this.id = id; this.address = address; @@ -86,8 +86,8 @@ export class Gyro2Pool implements PoolBase { this.totalShares = parseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; - this.sqrtAlpha = sqrtAlpha; - this.sqrtBeta = sqrtBeta; + this.sqrtAlpha = parseFixed(sqrtAlpha, 18); + this.sqrtBeta = parseFixed(sqrtBeta, 18); } parsePoolPairData(tokenIn: string, tokenOut: string): Gyro2PoolPairData { diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index a76ba628..6b4457e4 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -60,7 +60,10 @@ export class Gyro3Pool implements PoolBase { static fromPool(pool: SubgraphPoolBase): Gyro3Pool { if (!pool.root3Alpha) throw new Error('Pool missing root3Alpha'); - if (!(pool.root3Alpha.gt(0) && pool.root3Alpha.lt(ONE))) + if ( + parseFixed(pool.root3Alpha, 18).lte(0) || + parseFixed(pool.root3Alpha, 18).gte(ONE) + ) throw new Error('Invalid root3Alpha parameter'); if (pool.tokens.length !== 3) @@ -84,7 +87,7 @@ export class Gyro3Pool implements PoolBase { totalShares: string, tokens: Gyro3PoolToken[], tokensList: string[], - root3Alpha: BigNumber + root3Alpha: string ) { this.id = id; this.address = address; @@ -92,7 +95,7 @@ export class Gyro3Pool implements PoolBase { this.totalShares = parseFixed(totalShares, 18); this.tokens = tokens; this.tokensList = tokensList; - this.root3Alpha = root3Alpha; + this.root3Alpha = parseFixed(root3Alpha, 18); } parsePoolPairData(tokenIn: string, tokenOut: string): Gyro3PoolPairData { diff --git a/src/types.ts b/src/types.ts index b2cbceb6..d18c2738 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,11 +94,11 @@ export interface SubgraphPoolBase { upperTarget?: string; // Gyro2 specific field - sqrtAlpha?: BigNumber; - sqrtBeta?: BigNumber; + sqrtAlpha?: string; + sqrtBeta?: string; // Gyro3 specific field - root3Alpha?: BigNumber; + root3Alpha?: string; } export type SubgraphToken = { diff --git a/test/gyro2Math.spec.ts b/test/gyro2Math.spec.ts index d431bf70..64cd66e9 100644 --- a/test/gyro2Math.spec.ts +++ b/test/gyro2Math.spec.ts @@ -19,7 +19,6 @@ import { describe('gyro2Math tests', () => { const testPool: any = cloneDeep(testPools).pools[0]; const pool = Gyro2Pool.fromPool(testPool); - updatePoolParams(testPool); const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); @@ -72,8 +71,3 @@ describe('gyro2Math tests', () => { }); }); }); - -function updatePoolParams(pool) { - pool.sqrtAlpha = BigNumber.from(pool.sqrtAlpha); - pool.sqrtBeta = BigNumber.from(pool.sqrtBeta); -} diff --git a/test/gyro2Pool.spec.ts b/test/gyro2Pool.spec.ts index 20912c25..30470a5b 100644 --- a/test/gyro2Pool.spec.ts +++ b/test/gyro2Pool.spec.ts @@ -15,9 +15,7 @@ import { MockPoolDataService } from './lib/mockPoolDataService'; import { mockTokenPriceService } from './lib/mockTokenPriceService'; describe('Gyro2Pool tests USDC > DAI', () => { - const testPool: any = cloneDeep(testPools).pools[0]; - updatePoolParams(testPool); - + const testPool = cloneDeep(testPools).pools[0]; const pool = Gyro2Pool.fromPool(testPool); const poolPairData = pool.parsePoolPairData(USDC.address, DAI.address); @@ -154,7 +152,6 @@ describe('Gyro2Pool tests USDC > DAI', () => { context('FullSwap', () => { it(`Full Swap - swapExactIn, Token>Token`, async () => { const pools: any = cloneDeep(testPools.pools); - pools.forEach(updatePoolParams); const tokenIn = USDC.address; const tokenOut = DAI.address; const swapType = SwapTypes.SwapExactIn; @@ -202,8 +199,3 @@ describe('Gyro2Pool tests USDC > DAI', () => { }); }); }); - -function updatePoolParams(pool) { - pool.sqrtAlpha = BigNumber.from(pool.sqrtAlpha); - pool.sqrtBeta = BigNumber.from(pool.sqrtBeta); -} diff --git a/test/gyro3Math.spec.ts b/test/gyro3Math.spec.ts index 1611fd3a..2347875c 100644 --- a/test/gyro3Math.spec.ts +++ b/test/gyro3Math.spec.ts @@ -14,13 +14,8 @@ import { _normalizeBalances, } from '../src/pools/gyro3Pool/gyro3Math'; -function updatePoolParams(pool) { - pool.root3Alpha = BigNumber.from(pool.root3Alpha); -} - describe('gyro3Math tests', () => { const testPool: any = cloneDeep(testPools).pools[0]; - updatePoolParams(testPool); const pool = Gyro3Pool.fromPool(testPool); diff --git a/test/gyro3Pool.spec.ts b/test/gyro3Pool.spec.ts index 24daad15..8430bb0c 100644 --- a/test/gyro3Pool.spec.ts +++ b/test/gyro3Pool.spec.ts @@ -11,7 +11,6 @@ import testPools from './testData/gyro3Pools/gyro3TestPool.json'; describe('Gyro3Pool tests USDC > DAI', () => { const testPool: any = cloneDeep(testPools).pools[0]; - updatePoolParams(testPool); const pool = Gyro3Pool.fromPool(testPool); const poolPairData = pool.parsePoolPairData(USDT.address, USDC.address); @@ -121,7 +120,3 @@ describe('Gyro3Pool tests USDC > DAI', () => { }); }); }); - -function updatePoolParams(pool) { - pool.root3Alpha = BigNumber.from(pool.root3Alpha); -} diff --git a/test/testData/gyro2Pools/gyro2TestPool.json b/test/testData/gyro2Pools/gyro2TestPool.json index 77d9a208..614d8dee 100644 --- a/test/testData/gyro2Pools/gyro2TestPool.json +++ b/test/testData/gyro2Pools/gyro2TestPool.json @@ -40,8 +40,8 @@ "totalShares": "100", "poolType": "Gyro2", "swapEnabled": true, - "sqrtAlpha": "999499874900000000", - "sqrtBeta": "1000499875000000000" + "sqrtAlpha": "0.9994998749", + "sqrtBeta": "1.000499875" } ] } diff --git a/test/testData/gyro3Pools/gyro3TestPool.json b/test/testData/gyro3Pools/gyro3TestPool.json index 845afd82..313e1642 100644 --- a/test/testData/gyro3Pools/gyro3TestPool.json +++ b/test/testData/gyro3Pools/gyro3TestPool.json @@ -49,7 +49,7 @@ "totalShares": "100", "poolType": "Gyro3", "swapEnabled": true, - "root3Alpha": "995647752000000000" + "root3Alpha": "0.995647752" } ] } From 787ec012c7973bc918ea0be18e1a874c611fecd1 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 26 May 2022 14:56:29 +0100 Subject: [PATCH 05/11] Update swapExample to show compare clearly. --- src/pools/gyro3Pool/gyro3Pool.ts | 6 +++- test/testScripts/swapExample.ts | 59 +++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index 6b4457e4..db9bdf6e 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -240,7 +240,11 @@ export class Gyro3Pool implements PoolBase { inAmountLessFee, virtualOffsetInOut ); - + console.log( + `!!!!!!! ${amount.toString()} ${bnum( + formatFixed(outAmount, 18) + ).toString()}` + ); return bnum(formatFixed(outAmount, 18)); } diff --git a/test/testScripts/swapExample.ts b/test/testScripts/swapExample.ts index 61bb2970..3452b28d 100644 --- a/test/testScripts/swapExample.ts +++ b/test/testScripts/swapExample.ts @@ -99,7 +99,7 @@ export const SUBGRAPH_URLS = { [Network.GOERLI]: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-goerli-v2', [Network.KOVAN]: - 'https://api.thegraph.com/subgraphs/name/mendesfabio/balancer-kovan-v2', + 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-kovan-v2-beta', [Network.POLYGON]: 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-polygon-v2', [Network.ARBITRUM]: `https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-arbitrum-v2`, @@ -256,15 +256,25 @@ export const ADDRESSES = { decimals: 18, symbol: 'STABAL3', }, - GYRO1: { + GYRO2_TEST1: { + address: '0x8176d96e1b3115a9422fd47adc2e78248bed36c9', + decimals: 18, + symbol: 'GYRO2_TEST1', + }, + GYRO2_TEST2: { + address: '0xe5a9422a0b774eefe8cb66dea034a7e7b73a3df3', + decimals: 18, + symbol: 'GYRO2_TEST2', + }, + GYRO3_TEST1: { address: '0x11fb9071e69628d804bf0b197cc61eeacd4aaecf', decimals: 18, - symbol: 'GYRO1', + symbol: 'GYRO3_TEST1', }, - GYRO2: { + GYRO3_TEST2: { address: '0x4ea2110a3e277b10c9b098f61d72f58efa8655db', decimals: 18, - symbol: 'GYRO2', + symbol: 'GYRO3_TEST2', }, }, [Network.POLYGON]: { @@ -432,12 +442,22 @@ async function getSwap( console.log( `Token Out: ${tokenOut.symbol}, Amt: ${amtOutScaled.toString()}` ); - console.log(`Cost to swap: ${costToSwapScaled.toString()}`); - console.log(`Return Considering Fees: ${returnWithFeesScaled.toString()}`); console.log(`Swaps:`); console.log(swapInfo.swaps); console.log(swapInfo.tokenAddresses); + console.log( + `${ + swapType === SwapTypes.SwapExactIn + ? swapAmount.toString() + : swapInfo.returnAmount.toString() + },${ + swapType === SwapTypes.SwapExactIn + ? swapInfo.returnAmount.toString() + : swapAmount.toString() + } SOR Result` + ); + return swapInfo; } @@ -512,10 +532,10 @@ async function makeTrade( ); const deadline = MaxUint256; - console.log(funds); - console.log(swapInfo.tokenAddresses); - console.log(limits); - console.log('Swapping...'); + // console.log(funds); + // console.log(swapInfo.tokenAddresses); + // console.log(limits); + // console.log('Swapping...'); const overRides = {}; // overRides['gasLimit'] = '200000'; @@ -531,7 +551,12 @@ async function makeTrade( swapInfo.tokenAddresses, funds ); - console.log(deltas.toString()); + + console.log( + `${deltas[0].toString()},${ + deltas[1].toString().split('-')[1] + } QueryBatchSwap (EVM Result)` + ); // const tx = await vaultContract // .connect(wallet) @@ -727,10 +752,10 @@ export async function simpleSwap() { const networkId = Network.KOVAN; // Pools source can be Subgraph URL or pools data set passed directly // Update pools list with most recent onchain balances - const tokenIn = ADDRESSES[networkId].GYRO1; - const tokenOut = ADDRESSES[networkId].GYRO2; - const swapType = SwapTypes.SwapExactIn; - const swapAmount = parseFixed('1', 18); + const tokenIn = ADDRESSES[networkId].GYRO2_TEST1; + const tokenOut = ADDRESSES[networkId].GYRO2_TEST2; + const swapType = SwapTypes.SwapExactOut; + const swapAmount = parseFixed('0.0000000000001', 18); const executeTrade = true; const provider = new JsonRpcProvider(PROVIDER_URLS[networkId]); @@ -780,7 +805,7 @@ export async function simpleSwap() { console.log('RELAYER SWAP'); await makeRelayerTrade(provider, swapInfo, swapType, networkId); } else { - console.log('VAULT SWAP'); + // console.log('VAULT SWAP'); await makeTrade(provider, swapInfo, swapType); } } From 163308c2e16cb6fc8a964a401db5856d9ac26d9d Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Wed, 29 Jun 2022 18:20:46 +0100 Subject: [PATCH 06/11] Gyro2 and Gyro3 Math fix --- src/pools/gyro2Pool/constants.ts | 41 ++++++ src/pools/gyro2Pool/gyro2Math.ts | 53 +------ src/pools/gyro2Pool/gyro2Pool.ts | 5 +- src/pools/gyro2Pool/helpers.ts | 242 +++++++++++++++++++++++++++++++ src/pools/gyro3Pool/constants.ts | 31 ++++ src/pools/gyro3Pool/gyro3Math.ts | 204 ++++++++++++-------------- src/pools/gyro3Pool/gyro3Pool.ts | 6 +- src/pools/gyro3Pool/helpers.ts | 210 +++++++++++++++++++++++++++ test/testScripts/swapExample.ts | 8 +- 9 files changed, 628 insertions(+), 172 deletions(-) create mode 100644 src/pools/gyro2Pool/constants.ts create mode 100644 src/pools/gyro2Pool/helpers.ts create mode 100644 src/pools/gyro3Pool/constants.ts create mode 100644 src/pools/gyro3Pool/helpers.ts diff --git a/src/pools/gyro2Pool/constants.ts b/src/pools/gyro2Pool/constants.ts new file mode 100644 index 00000000..6a621297 --- /dev/null +++ b/src/pools/gyro2Pool/constants.ts @@ -0,0 +1,41 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; + +// Swap limits: amounts swapped may not be larger than this percentage of total balance. + +export const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); +export const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); +export const MAX_POW_RELATIVE_ERROR = BigNumber.from(10000); +export const LN_36_LOWER_BOUND: BigNumber = ONE.sub(ONE.div(10)); +export const LN_36_UPPER_BOUND: BigNumber = ONE.add(ONE.div(10)); +export const MILD_EXPONENT_BOUND = BigNumber.from(2).pow(254).div(ONE.mul(100)); + +// 18 decimal constants +export const x0 = BigNumber.from('128000000000000000000'); // 2ˆ7 +export const a0 = BigNumber.from( + '38877084059945950922200000000000000000000000000000000000' +); // eˆ(x0) (no decimals) +export const x1 = BigNumber.from('64000000000000000000'); // 2ˆ6 +export const a1 = BigNumber.from('6235149080811616882910000000'); // eˆ(x1) (no decimals) + +// 20 decimal constants +export const x2 = BigNumber.from('3200000000000000000000'); // 2ˆ5 +export const a2 = BigNumber.from('7896296018268069516100000000000000'); // eˆ(x2) +export const x3 = BigNumber.from('1600000000000000000000'); // 2ˆ4 +export const a3 = BigNumber.from('888611052050787263676000000'); // eˆ(x3) +export const x4 = BigNumber.from('800000000000000000000'); // 2ˆ3 +export const a4 = BigNumber.from('298095798704172827474000'); // eˆ(x4) +export const x5 = BigNumber.from('400000000000000000000'); // 2ˆ2 +export const a5 = BigNumber.from('5459815003314423907810'); // eˆ(x5) +export const x6 = BigNumber.from('200000000000000000000'); // 2ˆ1 +export const a6 = BigNumber.from('738905609893065022723'); // eˆ(x6) +export const x7 = BigNumber.from('100000000000000000000'); // 2ˆ0 +export const a7 = BigNumber.from('271828182845904523536'); // eˆ(x7) +export const x8 = BigNumber.from('50000000000000000000'); // 2ˆ-1 +export const a8 = BigNumber.from('164872127070012814685'); // eˆ(x8) +export const x9 = BigNumber.from('25000000000000000000'); // 2ˆ-2 +export const a9 = BigNumber.from('128402541668774148407'); // eˆ(x9) +export const x10 = BigNumber.from('12500000000000000000'); // 2ˆ-3 +export const a10 = BigNumber.from('113314845306682631683'); // eˆ(x10) +export const x11 = BigNumber.from('6250000000000000000'); // 2ˆ-4 +export const a11 = BigNumber.from('106449445891785942956'); // eˆ(x11) diff --git a/src/pools/gyro2Pool/gyro2Math.ts b/src/pools/gyro2Pool/gyro2Math.ts index c4b88fe0..746775fa 100644 --- a/src/pools/gyro2Pool/gyro2Math.ts +++ b/src/pools/gyro2Pool/gyro2Math.ts @@ -1,46 +1,8 @@ -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { BigNumber } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import bn from 'bignumber.js'; +import { _squareRoot, mulUp, divUp } from './helpers'; +import { _MAX_IN_RATIO, _MAX_OUT_RATIO } from './constants'; -// Swap limits: amounts swapped may not be larger than this percentage of total balance. - -const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); -const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); - -// Helpers -export function _squareRoot(value: BigNumber): BigNumber { - return BigNumber.from( - new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] - ); -} - -export function _normalizeBalances( - balances: BigNumber[], - decimalsIn: number, - decimalsOut: number -): BigNumber[] { - const scalingFactors = [ - parseFixed('1', decimalsIn), - parseFixed('1', decimalsOut), - ]; - - return balances.map((bal, index) => - bal.mul(ONE).div(scalingFactors[index]) - ); -} - -///////// -/// Fee calculations -///////// - -export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - const feeAmount = amountIn.mul(swapFee).div(ONE); - return amountIn.sub(feeAmount); -} - -export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - return amountIn.mul(ONE).div(ONE.sub(swapFee)); -} ///////// /// Virtual Parameter calculations ///////// @@ -108,6 +70,7 @@ export function _calculateQuadratic( // The minus sign in the radicand cancels out in this special case, so we add const radicand = bSquare.add(addTerm); const sqrResult = _squareRoot(radicand); + // The minus sign in the numerator cancels out in this special case const numerator = mb.add(sqrResult); const invariant = numerator.mul(ONE).div(denominator); @@ -146,8 +109,8 @@ export function _calcOutGivenIn( const virtIn = balanceIn.add(virtualParamIn); const denominator = virtIn.add(amountIn); - const invSquare = currentInvariant.mul(currentInvariant).div(ONE); - const subtrahend = invSquare.mul(ONE).div(denominator); + const invSquare = mulUp(currentInvariant, currentInvariant); + const subtrahend = divUp(invSquare, denominator); const virtOut = balanceOut.add(virtualParamOut); return virtOut.sub(subtrahend); } @@ -178,8 +141,8 @@ export function _calcInGivenOut( const virtOut = balanceOut.add(virtualParamOut); const denominator = virtOut.sub(amountOut); - const invSquare = currentInvariant.mul(currentInvariant).div(ONE); - const term = invSquare.mul(ONE).div(denominator); + const invSquare = mulUp(currentInvariant, currentInvariant); + const term = divUp(invSquare, denominator); const virtIn = balanceIn.add(virtualParamIn); return term.sub(virtIn); } diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index d8080db2..ef3dcb86 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -13,19 +13,16 @@ import { } from '../../types'; import { isSameAddress } from '../../utils'; import { - _squareRoot, - _normalizeBalances, _calculateInvariant, _calcOutGivenIn, _calcInGivenOut, _findVirtualParams, _calculateNewSpotPrice, - _reduceFee, - _addFee, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, _getNormalizedLiquidity, } from './gyro2Math'; +import { _normalizeBalances, _reduceFee, _addFee } from './helpers'; export type Gyro2PoolPairData = PoolPairBase & { sqrtAlpha: BigNumber; diff --git a/src/pools/gyro2Pool/helpers.ts b/src/pools/gyro2Pool/helpers.ts new file mode 100644 index 00000000..ecbf974e --- /dev/null +++ b/src/pools/gyro2Pool/helpers.ts @@ -0,0 +1,242 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { + MAX_POW_RELATIVE_ERROR, + MILD_EXPONENT_BOUND, + LN_36_LOWER_BOUND, + LN_36_UPPER_BOUND, + a0, + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + x0, + x1, + x2, + x3, + x4, + x5, + x6, + x7, + x8, + x9, + x10, + x11, +} from './constants'; + +// Helpers +export function _squareRoot(input: BigNumber): BigNumber { + return powDown(input, ONE.div(2)); +} + +function powDown(a: BigNumber, b: BigNumber) { + const raw = logExpMathPow(a, b); + const maxError = mulUp(raw, MAX_POW_RELATIVE_ERROR).add(1); + + if (raw.lt(maxError)) { + return BigNumber.from(0); + } else { + return raw.sub(maxError); + } +} + +function logExpMathPow(x: BigNumber, y: BigNumber): BigNumber { + if (y.isZero()) { + return ONE; + } + if (x.isZero()) { + return BigNumber.from(0); + } + + // Instead of computing x^y directly, we instead rely on the properties of logarithms and exponentiation to + // arrive at that result. In particular, exp(ln(x)) = x, and ln(x^y) = y * ln(x). This means + // x^y = exp(y * ln(x)). + + // The ln function takes a signed value, so we need to make sure x fits in the signed 256 bit range. + if (x.gte(BigNumber.from(2).pow(255))) + throw new Error('logExpMathPow error: Input out of bounds'); + + if (y.gte(MILD_EXPONENT_BOUND)) + throw new Error('logExpMathPow error: Exponent out of bounds'); + + let logXTimesY = BigNumber.from(0); + let isPos = true; + if (x.gt(LN_36_LOWER_BOUND) && x.lt(LN_36_UPPER_BOUND)) { + const ln36A = _ln_36(x); + logXTimesY = ln36A.div(ONE).mul(y).add(ln36A.mod(ONE).mul(y).div(ONE)); + } else { + const [lnA, lnAPos] = _ln(x); + if (!lnAPos) isPos = false; + logXTimesY = lnA.mul(y); + } + logXTimesY = logXTimesY.div(ONE); + + return exp(logXTimesY, isPos); +} + +function _ln_36(x: BigNumber): BigNumber { + x = x.mul(ONE); + const ONE36 = ONE.mul(ONE); + + const z = x.sub(ONE36).mul(ONE36).div(x.add(ONE36)); + const zSquared = z.mul(z).div(ONE36); + + let num = z; + let seriesSum = num; + + for (let i = 3; i <= 15; i = i + 2) { + num = num.mul(zSquared).div(ONE36); + seriesSum = seriesSum.add(num.div(i)); + } + + return seriesSum.mul(2); +} + +function _ln(a: BigNumber): [BigNumber, boolean] { + if (a.lt(ONE)) { + return [_ln(ONE.mul(ONE).div(a))[0], false]; + } + + let sum = BigNumber.from(0); + + if (a.gte(a0.mul(ONE))) { + a = a.div(a0); + sum = sum.add(x0); + } + + if (a.gte(a1.mul(ONE))) { + a = a.div(a1); + sum = sum.add(x1); + } + + sum = sum.mul(100); + a = a.mul(100); + const ONE20 = ONE.mul(100); + + [ + [a2, x2], + [a3, x3], + [a4, x4], + [a5, x5], + [a6, x6], + [a7, x7], + [a8, x8], + [a9, x9], + [a10, x10], + [a11, x11], + ].forEach(([aNum, xNum]) => { + if (a.gte(aNum)) { + a = a.mul(ONE20).div(aNum); + sum = sum.add(xNum); + } + }); + + const z = a.sub(ONE20).mul(ONE20).div(a.add(ONE20)); + const zSquared = z.mul(z).div(ONE20); + + let num = z; + let seriesSum = num; + + for (let i = 3; i <= 11; i = i + 2) { + num = num.mul(zSquared).div(ONE20); + seriesSum = seriesSum.add(num.div(i)); + } + + seriesSum = seriesSum.mul(2); + + return [sum.add(seriesSum).div(100), true]; +} + +function exp(x: BigNumber, isPos: boolean) { + if (!isPos) { + return ONE.mul(ONE).div(exp(x, true)); + } + + let firstAN = BigNumber.from(0); + if (x.gte(x0)) { + x = x.sub(x0); + firstAN = a0; + } else if (x.gte(x1)) { + x = x.sub(x1); + firstAN = a1; + } else { + firstAN = BigNumber.from(1); + } + + x = x.mul(100); + const ONE20 = ONE.mul(100); + + let product = ONE20; + + [ + [a2, x2], + [a3, x3], + [a4, x4], + [a5, x5], + [a6, x6], + [a7, x7], + [a8, x8], + [a9, x9], + ].forEach(([aNum, xNum]) => { + if (x.gte(xNum)) { + x = x.sub(xNum); + product = product.mul(aNum).div(ONE20); + } + }); + + let seriesSum = ONE20; + let term = x; + seriesSum = seriesSum.add(term); + + for (let i = 2; i <= 12; i++) { + term = term.mul(x).div(ONE20).div(i); + seriesSum = seriesSum.add(term); + } + + return product.mul(seriesSum).div(ONE20).mul(firstAN).div(100); +} + +export function mulUp(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + return product.sub(1).div(ONE).add(1); +} + +export function divUp(a: BigNumber, b: BigNumber) { + const aInflated = a.mul(ONE); + return aInflated.sub(1).div(b).add(1); +} + +export function _normalizeBalances( + balances: BigNumber[], + decimalsIn: number, + decimalsOut: number +): BigNumber[] { + const scalingFactors = [ + parseFixed('1', decimalsIn), + parseFixed('1', decimalsOut), + ]; + + return balances.map((bal, index) => + bal.mul(ONE).div(scalingFactors[index]) + ); +} + +///////// +/// Fee calculations +///////// + +export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + const feeAmount = amountIn.mul(swapFee).div(ONE); + return amountIn.sub(feeAmount); +} + +export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + return amountIn.mul(ONE).div(ONE.sub(swapFee)); +} diff --git a/src/pools/gyro3Pool/constants.ts b/src/pools/gyro3Pool/constants.ts new file mode 100644 index 00000000..3e65a819 --- /dev/null +++ b/src/pools/gyro3Pool/constants.ts @@ -0,0 +1,31 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; + +// Swap limits: amounts swapped may not be larger than this percentage of total balance. + +export const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); +export const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); + +// SQRT constants + +export const SQRT_1E_NEG_1 = BigNumber.from('316227766016837933'); +export const SQRT_1E_NEG_3 = BigNumber.from('31622776601683793'); +export const SQRT_1E_NEG_5 = BigNumber.from('3162277660168379'); +export const SQRT_1E_NEG_7 = BigNumber.from('316227766016837'); +export const SQRT_1E_NEG_9 = BigNumber.from('31622776601683'); +export const SQRT_1E_NEG_11 = BigNumber.from('3162277660168'); +export const SQRT_1E_NEG_13 = BigNumber.from('316227766016'); +export const SQRT_1E_NEG_15 = BigNumber.from('31622776601'); +export const SQRT_1E_NEG_17 = BigNumber.from('3162277660'); + +// POW3 constant +// Threshold of x where the normal method of computing x^3 would overflow and we need a workaround. +// Equal to 4.87e13 scaled; 4.87e13 is the point x where x**3 * 10**36 = (x**2 native) * (x native) ~ 2**256 +export const _SAFE_LARGE_POW3_THRESHOLD = BigNumber.from(10).pow(29).mul(487); +export const MIDDECIMAL = BigNumber.from(10).pow(9); // splits the fixed point decimals into two equal parts. + +// Stopping criterion for the Newton iteration that computes the invariant: +// - Stop if the step width doesn't shrink anymore by at least a factor _INVARIANT_SHRINKING_FACTOR_PER_STEP. +// - ... but in any case, make at least _INVARIANT_MIN_ITERATIONS iterations. This is useful to compensate for a +// less-than-ideal starting point, which is important when alpha is small. +export const _INVARIANT_SHRINKING_FACTOR_PER_STEP = 8; +export const _INVARIANT_MIN_ITERATIONS = 5; diff --git a/src/pools/gyro3Pool/gyro3Math.ts b/src/pools/gyro3Pool/gyro3Math.ts index 2737f0c8..a58ee6d7 100644 --- a/src/pools/gyro3Pool/gyro3Math.ts +++ b/src/pools/gyro3Pool/gyro3Math.ts @@ -1,54 +1,18 @@ -import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import { BigNumber } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import bn from 'bignumber.js'; - -// Swap limits: amounts swapped may not be larger than this percentage of total balance. - -const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); -const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); - -// Helpers -export function _squareRoot(value: BigNumber): BigNumber { - return BigNumber.from( - new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] - ); -} - -export function _normalizeBalances( - balances: BigNumber[], - decimals: number[] -): BigNumber[] { - const scalingFactors = decimals.map((d) => parseFixed('1', d)); - - return balances.map((bal, index) => - bal.mul(ONE).div(scalingFactors[index]) - ); -} - -///////// -/// Fee calculations -///////// - -export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - const feeAmount = amountIn.mul(swapFee).div(ONE); - return amountIn.sub(feeAmount); -} - -export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { - return amountIn.mul(ONE).div(ONE.sub(swapFee)); -} +import { + _MAX_IN_RATIO, + _MAX_OUT_RATIO, + _SAFE_LARGE_POW3_THRESHOLD, + _INVARIANT_MIN_ITERATIONS, + _INVARIANT_SHRINKING_FACTOR_PER_STEP, +} from './constants'; +import { mulUp, divUp, newtonSqrt, _safeLargePow3ADown } from './helpers'; ///////// /// Invariant Calculation ///////// -// Stopping criterion for the Newton iteration that computes the invariant: -// - Stop if the step width doesn't shrink anymore by at least a factor _INVARIANT_SHRINKING_FACTOR_PER_STEP. -// - ... but in any case, make at least _INVARIANT_MIN_ITERATIONS iterations. This is useful to compensate for a -// less-than-ideal starting point, which is important when alpha is small. -const _INVARIANT_SHRINKING_FACTOR_PER_STEP = 10; -const _INVARIANT_MIN_ITERATIONS = 2; - // Invariant is used to collect protocol swap fees by comparing its value between two times. // So we can round always to the same direction. It is also used to initiate the BPT amount // and, because there is a minimum BPT, we round down the invariant. @@ -67,7 +31,7 @@ export function _calculateInvariant( // taking mb = -b and mc = -c /**********************************************************************************************/ const [a, mb, mc, md] = _calculateCubicTerms(balances, root3Alpha); - return _calculateCubic(a, mb, mc, md); + return _calculateCubic(a, mb, mc, md, root3Alpha); } /** @dev Prepares quadratic terms for input to _calculateCubic @@ -83,7 +47,7 @@ export function _calculateCubicTerms( const alpha = alpha23.mul(root3Alpha).div(ONE); const a = ONE.sub(alpha); const bterm = balances[0].add(balances[1]).add(balances[2]); - const mb = bterm.mul(alpha23).div(ONE); + const mb = bterm.mul(root3Alpha).div(ONE).mul(root3Alpha).div(ONE); const cterm = balances[0] .mul(balances[1]) .div(ONE) @@ -101,21 +65,11 @@ export function _calculateCubic( a: BigNumber, mb: BigNumber, mc: BigNumber, - md: BigNumber + md: BigNumber, + root3Alpha: BigNumber ): BigNumber { - let rootEst: BigNumber; - if (md.isZero()) { - // lower-order special case - const radic = mb - .mul(mb) - .div(ONE) - .add(a.mul(mc).div(ONE).mul(BigNumber.from(4))); - rootEst = mb.add(_squareRoot(radic)).div(BigNumber.from(2).mul(a)); - } else { - rootEst = _calculateCubicStartingPoint(a, mb, mc); - rootEst = _runNewtonIteration(a, mb, mc, md, rootEst); - } - + let rootEst = _calculateCubicStartingPoint(a, mb, mc); + rootEst = _runNewtonIteration(a, mb, mc, md, root3Alpha, rootEst); return rootEst; } @@ -126,17 +80,15 @@ export function _calculateCubicStartingPoint( mb: BigNumber, mc: BigNumber ): BigNumber { - const radic: BigNumber = mb - .mul(mb) - .div(ONE) - .add(a.mul(mc).div(ONE).mul(BigNumber.from(3))); - const lmin = mb - .mul(ONE) - .div(a.mul(BigNumber.from(3))) - .add(_squareRoot(radic).mul(ONE).div(BigNumber.from(3).mul(a))); - // The factor 3/2 is a magic number found experimentally for our invariant. All factors > 1 are safe. - const l0 = lmin.mul(BigNumber.from(3)).div(BigNumber.from(2)); - + const radic = mulUp(mb, mb).add(mulUp(mulUp(a, mc), ONE.mul(3))); + const lmin = divUp(mb, a.mul(3)).add( + divUp(newtonSqrt(radic, BigNumber.from(5)), a.mul(3)) + ); + // This formula has been found experimentally. It is exact for alpha -> 1, where the factor is 1.5. All + // factors > 1 are safe. For small alpha values, it is more efficient to fallback to a larger factor. + const alpha = ONE.sub(a); // We know that a is in [0, 1]. + const factor = alpha.gte(ONE.div(2)) ? ONE.mul(3).div(2) : ONE.mul(2); + const l0 = mulUp(lmin, factor); return l0; } @@ -150,19 +102,28 @@ export function _runNewtonIteration( mb: BigNumber, mc: BigNumber, md: BigNumber, + root3Alpha: BigNumber, rootEst: BigNumber ): BigNumber { let deltaAbsPrev = BigNumber.from(0); for (let iteration = 0; iteration < 255; ++iteration) { // The delta to the next step can be positive or negative, so we represent a positive and a negative part // separately. The signed delta is delta_plus - delta_minus, but we only ever consider its absolute value. - const [deltaAbs, deltaIsPos] = _calcNewtonDelta(a, mb, mc, md, rootEst); + const [deltaAbs, deltaIsPos] = _calcNewtonDelta( + a, + mb, + mc, + md, + root3Alpha, + rootEst + ); + // ^ Note: If we ever set _INVARIANT_MIN_ITERATIONS=0, the following should include `iteration >= 1`. if ( - deltaAbs.isZero() || + deltaAbs.lte(1) || (iteration >= _INVARIANT_MIN_ITERATIONS && deltaIsPos) ) - // Iteration literally stopped or numerical error dominates + // This should mathematically never happen. Thus, the numerical error dominates at this point. return rootEst; if ( iteration >= _INVARIANT_MIN_ITERATIONS && @@ -172,9 +133,8 @@ export function _runNewtonIteration( ) ) ) { - // stalled - // Move one more step to the left to ensure we're underestimating, rather than overestimating, L - return rootEst.sub(deltaAbs); + // The iteration has stalled and isn't making significant progress anymore. + return rootEst; } deltaAbsPrev = deltaAbs; if (deltaIsPos) rootEst = rootEst.add(deltaAbs); @@ -185,33 +145,40 @@ export function _runNewtonIteration( 'Gyro3Pool: Newton Method did not converge on required invariant' ); } -let first = 0; + // -f(l)/f'(l), represented as an absolute value and a sign. Require that l is sufficiently large so that f is strictly increasing. export function _calcNewtonDelta( a: BigNumber, mb: BigNumber, mc: BigNumber, md: BigNumber, + root3Alpha: BigNumber, rootEst: BigNumber ): [BigNumber, boolean] { - const dfRootEst = BigNumber.from(3) - .mul(a) - .mul(rootEst) - .div(ONE) - .sub(BigNumber.from(2).mul(mb)) - .mul(rootEst) - .div(ONE) - .sub(mc); // Does not underflow since rootEst >> 0 by assumption. - // We know that a rootEst^2 / dfRootEst ~ 1. (this is pretty exact actually, see the Mathematica notebook). We use this - // multiplication order to prevent overflows that can otherwise occur when computing l^3 for very large - // reserves. - - let deltaMinus = a.mul(rootEst).div(ONE).mul(rootEst).div(ONE); - deltaMinus = deltaMinus.mul(ONE).div(dfRootEst).mul(rootEst).div(ONE); - // use multiple statements to prevent 'stack too deep'. The order of operations is chosen to prevent overflows - // for very large numbers. - let deltaPlus = mb.mul(rootEst).div(ONE).add(mc).mul(ONE).div(dfRootEst); - deltaPlus = deltaPlus.mul(rootEst).div(ONE).add(md.mul(ONE).div(dfRootEst)); + // The following is equal to dfRootEst^3 * a but with an order of operations optimized for precision. + // Subtraction does not underflow since rootEst is chosen so that it's always above the (only) local minimum. + let dfRootEst = BigNumber.from(0); + + const rootEst2 = rootEst.mul(rootEst).div(ONE); + dfRootEst = rootEst2.mul(3); + dfRootEst = dfRootEst.sub( + dfRootEst + .mul(root3Alpha) + .div(ONE) + .mul(root3Alpha) + .div(ONE) + .mul(root3Alpha) + .div(ONE) + ); + dfRootEst = dfRootEst.sub(rootEst.mul(mb).div(ONE).mul(2)).sub(mc); + + const deltaMinus = _safeLargePow3ADown(rootEst, root3Alpha, dfRootEst); + + // NB: We could order the operations here in much the same way we did above to reduce errors. But tests show + // that this has no significant effect, and it would lead to more complex code. + let deltaPlus = rootEst.mul(rootEst).div(ONE).mul(mb).div(ONE); + deltaPlus = deltaPlus.add(rootEst.mul(mc).div(ONE)).mul(ONE).div(dfRootEst); + deltaPlus = deltaPlus.add(md.mul(ONE).div(dfRootEst)); const deltaIsPos = deltaPlus.gte(deltaMinus); const deltaAbs = deltaIsPos @@ -233,7 +200,7 @@ export function _calcOutGivenIn( balanceIn: BigNumber, balanceOut: BigNumber, amountIn: BigNumber, - virtualOffsetInOut: BigNumber + virtualOffset: BigNumber ): BigNumber { /********************************************************************************************** // Described for X = `in' asset and Z = `out' asset, but equivalent for the other case // @@ -251,11 +218,13 @@ export function _calcOutGivenIn( if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) throw new Error('Swap Amount In Too Large'); - const virtIn = balanceIn.add(virtualOffsetInOut); - const virtOut = balanceOut.add(virtualOffsetInOut); - const denominator = virtIn.add(amountIn); - const subtrahend = virtIn.mul(virtOut).div(denominator); - const amountOut = virtOut.sub(subtrahend); + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18, compensating for the maximum multiplicative error in the invariant + // computation. + + const virtInOver = balanceIn.add(mulUp(virtualOffset, ONE.add(2))); + const virtOutUnder = balanceOut.add(virtualOffset.mul(ONE.sub(1)).div(ONE)); + const amountOut = virtOutUnder.mul(amountIn).div(virtInOver.add(amountIn)); // Note that this in particular reverts if amountOut > balanceOut, i.e., if the out-amount would be more than // the balance. @@ -273,7 +242,7 @@ export function _calcInGivenOut( balanceIn: BigNumber, balanceOut: BigNumber, amountOut: BigNumber, - virtualOffsetInOut: BigNumber + virtualOffset: BigNumber ): BigNumber { /********************************************************************************************** // Described for X = `in' asset and Z = `out' asset, but equivalent for the other case // @@ -281,12 +250,15 @@ export function _calcInGivenOut( // dZ = incrZ = amountOut < 0 // // x = balanceIn x' = x + virtualOffset // // z = balanceOut z' = z + virtualOffset // - // L = inv.Liq / x' * z' \ // - // dX = | -------------------------- | - x' // - // x' = virtIn \ ( z' + dZ) / // + // L = inv.Liq / x' * z' \ x' * dZ // + // dX = | -------------------------- | - x' = --------------- // + // x' = virtIn \ ( z' + dZ) / z' - dZ // // z' = virtOut // // Note that dz < 0 < dx. // - // We exploit the fact that this formula is symmetric up to virtualParam{X,Y,Z}. // + // We exploit the fact that this formula is symmetric and does not depend on which asset is // + // which. + // We assume that the virtualOffset carries a relative +/- 3e-18 error due to the invariant // + // calculation add an appropriate safety margin. // **********************************************************************************************/ // Note that this in particular reverts if amountOut > balanceOut, i.e., if the trader tries to take more out of @@ -294,12 +266,16 @@ export function _calcInGivenOut( if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) throw new Error('Swap Amount Out Too Large'); - const virtIn = balanceIn.add(virtualOffsetInOut); - const virtOut = balanceOut.add(virtualOffsetInOut); - const denominator = virtOut.sub(amountOut); - const minuend = virtIn.mul(virtOut).div(denominator); + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18, compensating for the maximum multiplicative error in the invariant + // computation. + const virtInOver = balanceIn.add(mulUp(virtualOffset, ONE.add(2))); + const virtOutUnder = balanceOut.add(virtualOffset.mul(ONE.sub(1)).div(ONE)); - const amountIn = minuend.sub(virtIn); + const amountIn = divUp( + mulUp(virtInOver, amountOut), + virtOutUnder.sub(amountOut) + ); if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) throw new Error('Resultant Swap Amount In Too Large'); @@ -430,9 +406,7 @@ export function _getNormalizedLiquidity( **********************************************************************************************/ const virtIn = balances[0].add(virtualParamIn); - const afterFeeMultiplier = ONE.sub(swapFee); - const normalizedLiquidity = virtIn.mul(ONE).div(afterFeeMultiplier); return normalizedLiquidity; diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index db9bdf6e..0f238e43 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -13,18 +13,17 @@ import { } from '../../types'; import { isSameAddress } from '../../utils'; import { - _normalizeBalances, _calculateInvariant, _calcOutGivenIn, _calcInGivenOut, - _reduceFee, - _addFee, _calculateNewSpotPrice, _derivativeSpotPriceAfterSwapExactTokenInForTokenOut, _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, _getNormalizedLiquidity, } from './gyro3Math'; +import { _normalizeBalances, _reduceFee, _addFee } from './helpers'; + export type Gyro3PoolPairData = PoolPairBase & { balanceTertiary: BigNumber; // Balance of the unchanged asset decimalsTertiary: number; // Decimals of the unchanged asset @@ -230,7 +229,6 @@ export class Gyro3Pool implements PoolBase { ); const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); - const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); diff --git a/src/pools/gyro3Pool/helpers.ts b/src/pools/gyro3Pool/helpers.ts new file mode 100644 index 00000000..9819a036 --- /dev/null +++ b/src/pools/gyro3Pool/helpers.ts @@ -0,0 +1,210 @@ +import { BigNumber, parseFixed } from '@ethersproject/bignumber'; +import bn from 'bignumber.js'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; +import { + _MAX_IN_RATIO, + _MAX_OUT_RATIO, + SQRT_1E_NEG_1, + SQRT_1E_NEG_3, + SQRT_1E_NEG_5, + SQRT_1E_NEG_7, + SQRT_1E_NEG_9, + SQRT_1E_NEG_11, + SQRT_1E_NEG_13, + SQRT_1E_NEG_15, + SQRT_1E_NEG_17, + _SAFE_LARGE_POW3_THRESHOLD, + MIDDECIMAL, +} from './constants'; + +// Helpers +export function _squareRoot(value: BigNumber): BigNumber { + return BigNumber.from( + new bn(value.mul(ONE).toString()).sqrt().toFixed().split('.')[0] + ); +} + +export function mulUp(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + return product.sub(1).div(ONE).add(1); +} + +export function divUp(a: BigNumber, b: BigNumber) { + const aInflated = a.mul(ONE); + return aInflated.sub(1).div(b).add(1); +} + +export function newtonSqrt(input: BigNumber, tolerance: BigNumber) { + if (input.isZero()) { + return BigNumber.from(0); + } + let guess = _makeInitialGuess(input); + + // 7 iterations + for (let i of new Array(7).fill(0)) { + guess = guess.add(input.mul(ONE).div(guess)).div(2); + } + + // Check in some epsilon range + // Check square is more or less correct + const guessSquared = guess.mul(guess).div(ONE); + + if ( + !( + guessSquared.lte(input.add(mulUp(guess, tolerance))) && + guessSquared.gte(input.sub(mulUp(guess, tolerance))) + ) + ) + throw new Error('Gyro3Pool: newtonSqrt failed'); + + return guess; +} + +function _makeInitialGuess(input: BigNumber) { + if (input.gte(ONE)) { + return BigNumber.from(2) + .pow(_intLog2Halved(input.div(ONE))) + .mul(ONE); + } else { + if (input.lte('10')) { + return SQRT_1E_NEG_17; + } + if (input.lte('100')) { + return BigNumber.from('10000000000'); + } + if (input.lte('1000')) { + return SQRT_1E_NEG_15; + } + if (input.lte('10000')) { + return BigNumber.from('100000000000'); + } + if (input.lte('100000')) { + return SQRT_1E_NEG_13; + } + if (input.lte('1000000')) { + return BigNumber.from('1000000000000'); + } + if (input.lte('10000000')) { + return SQRT_1E_NEG_11; + } + if (input.lte('100000000')) { + return BigNumber.from('10000000000000'); + } + if (input.lte('1000000000')) { + return SQRT_1E_NEG_9; + } + if (input.lte('10000000000')) { + return BigNumber.from('100000000000000'); + } + if (input.lte('100000000000')) { + return SQRT_1E_NEG_7; + } + if (input.lte('1000000000000')) { + return BigNumber.from('1000000000000000'); + } + if (input.lte('10000000000000')) { + return SQRT_1E_NEG_5; + } + if (input.lte('100000000000000')) { + return BigNumber.from('10000000000000000'); + } + if (input.lte('1000000000000000')) { + return SQRT_1E_NEG_3; + } + if (input.lte('10000000000000000')) { + return BigNumber.from('100000000000000000'); + } + if (input.lte('100000000000000000')) { + return SQRT_1E_NEG_1; + } + return input; + } +} + +function _intLog2Halved(x: BigNumber) { + let n = 0; + + for (let i = 128; i >= 2; i = i / 2) { + const factor = BigNumber.from(2).pow(i); + if (x.gte(factor)) { + x = x.div(factor); + n += i / 2; + } + } + + return n; +} + +export function _safeLargePow3ADown( + l: BigNumber, + root3Alpha: BigNumber, + d: BigNumber +) { + let ret = BigNumber.from(0); + if (l.lte(_SAFE_LARGE_POW3_THRESHOLD)) { + // Simple case where there is no overflow + ret = l.mul(l).div(ONE).mul(l).div(ONE); + ret = ret.sub( + ret + .mul(root3Alpha) + .div(ONE) + .mul(root3Alpha) + .div(ONE) + .mul(root3Alpha) + .div(ONE) + ); + ret = ret.mul(ONE).div(d); + } else { + ret = l.mul(l).div(ONE); + + // Compute l^2 * l * (1 - root3Alpha^3) + // The following products split up the factors into different groups of decimal places to reduce temorary + // blowup and prevent overflow. + // No precision is lost. + ret = ret.mul(l.div(ONE)).add(ret.mul(l.mod(ONE)).div(ONE)); + + let x = ret; + + for (let i = 0; i < 3; i++) { + x = x + .mul(root3Alpha.div(MIDDECIMAL)) + .div(MIDDECIMAL) + .add(x.mul(root3Alpha.mod(MIDDECIMAL))); + } + ret = ret.sub(x); + + // We perform half-precision division to reduce blowup. + // In contrast to the above multiplications, this loses precision if d is small. However, tests show that, + // for the l and d values considered here, the precision lost would be below the precision of the fixed + // point type itself, so nothing is actually lost. + ret = ret.mul(MIDDECIMAL).div(d.div(MIDDECIMAL)); + } + return ret; +} + +///////// +/// Fee calculations +///////// + +export function _reduceFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + const feeAmount = amountIn.mul(swapFee).div(ONE); + return amountIn.sub(feeAmount); +} + +export function _addFee(amountIn: BigNumber, swapFee: BigNumber): BigNumber { + return amountIn.mul(ONE).div(ONE.sub(swapFee)); +} + +//////// +/// Normalize balances +//////// +export function _normalizeBalances( + balances: BigNumber[], + decimals: number[] +): BigNumber[] { + const scalingFactors = decimals.map((d) => parseFixed('1', d)); + + return balances.map((bal, index) => + bal.mul(ONE).div(scalingFactors[index]) + ); +} diff --git a/test/testScripts/swapExample.ts b/test/testScripts/swapExample.ts index 3452b28d..0be3585e 100644 --- a/test/testScripts/swapExample.ts +++ b/test/testScripts/swapExample.ts @@ -257,12 +257,12 @@ export const ADDRESSES = { symbol: 'STABAL3', }, GYRO2_TEST1: { - address: '0x8176d96e1b3115a9422fd47adc2e78248bed36c9', + address: '0x8021ae68696046c40daea1953bb8767a6f9fdd6f', decimals: 18, symbol: 'GYRO2_TEST1', }, GYRO2_TEST2: { - address: '0xe5a9422a0b774eefe8cb66dea034a7e7b73a3df3', + address: '0xb2bb9bf1bf645fc19a8135074cff49224a5d2577', decimals: 18, symbol: 'GYRO2_TEST2', }, @@ -754,8 +754,8 @@ export async function simpleSwap() { // Update pools list with most recent onchain balances const tokenIn = ADDRESSES[networkId].GYRO2_TEST1; const tokenOut = ADDRESSES[networkId].GYRO2_TEST2; - const swapType = SwapTypes.SwapExactOut; - const swapAmount = parseFixed('0.0000000000001', 18); + const swapType = SwapTypes.SwapExactIn; + const swapAmount = parseFixed('100', 18); const executeTrade = true; const provider = new JsonRpcProvider(PROVIDER_URLS[networkId]); From 59281f2c35ffb6240035972c69dfce846751c939 Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Thu, 30 Jun 2022 15:18:33 +0100 Subject: [PATCH 07/11] sqrt fix and refactor --- src/pools/gyro2Pool/constants.ts | 43 +---- src/pools/gyro2Pool/gyro2Math.ts | 136 +++++++++------ src/pools/gyro2Pool/gyro2Pool.ts | 34 ++-- src/pools/gyro2Pool/helpers.ts | 284 +++++++++++-------------------- src/pools/gyro3Pool/gyro3Math.ts | 79 +++++---- src/pools/gyro3Pool/gyro3Pool.ts | 26 +-- src/pools/gyro3Pool/helpers.ts | 10 ++ test/testScripts/swapExample.ts | 6 +- 8 files changed, 282 insertions(+), 336 deletions(-) diff --git a/src/pools/gyro2Pool/constants.ts b/src/pools/gyro2Pool/constants.ts index 6a621297..cd93f639 100644 --- a/src/pools/gyro2Pool/constants.ts +++ b/src/pools/gyro2Pool/constants.ts @@ -1,41 +1,16 @@ import { BigNumber, parseFixed } from '@ethersproject/bignumber'; -import { WeiPerEther as ONE } from '@ethersproject/constants'; // Swap limits: amounts swapped may not be larger than this percentage of total balance. export const _MAX_IN_RATIO: BigNumber = parseFixed('0.3', 18); export const _MAX_OUT_RATIO: BigNumber = parseFixed('0.3', 18); -export const MAX_POW_RELATIVE_ERROR = BigNumber.from(10000); -export const LN_36_LOWER_BOUND: BigNumber = ONE.sub(ONE.div(10)); -export const LN_36_UPPER_BOUND: BigNumber = ONE.add(ONE.div(10)); -export const MILD_EXPONENT_BOUND = BigNumber.from(2).pow(254).div(ONE.mul(100)); -// 18 decimal constants -export const x0 = BigNumber.from('128000000000000000000'); // 2ˆ7 -export const a0 = BigNumber.from( - '38877084059945950922200000000000000000000000000000000000' -); // eˆ(x0) (no decimals) -export const x1 = BigNumber.from('64000000000000000000'); // 2ˆ6 -export const a1 = BigNumber.from('6235149080811616882910000000'); // eˆ(x1) (no decimals) - -// 20 decimal constants -export const x2 = BigNumber.from('3200000000000000000000'); // 2ˆ5 -export const a2 = BigNumber.from('7896296018268069516100000000000000'); // eˆ(x2) -export const x3 = BigNumber.from('1600000000000000000000'); // 2ˆ4 -export const a3 = BigNumber.from('888611052050787263676000000'); // eˆ(x3) -export const x4 = BigNumber.from('800000000000000000000'); // 2ˆ3 -export const a4 = BigNumber.from('298095798704172827474000'); // eˆ(x4) -export const x5 = BigNumber.from('400000000000000000000'); // 2ˆ2 -export const a5 = BigNumber.from('5459815003314423907810'); // eˆ(x5) -export const x6 = BigNumber.from('200000000000000000000'); // 2ˆ1 -export const a6 = BigNumber.from('738905609893065022723'); // eˆ(x6) -export const x7 = BigNumber.from('100000000000000000000'); // 2ˆ0 -export const a7 = BigNumber.from('271828182845904523536'); // eˆ(x7) -export const x8 = BigNumber.from('50000000000000000000'); // 2ˆ-1 -export const a8 = BigNumber.from('164872127070012814685'); // eˆ(x8) -export const x9 = BigNumber.from('25000000000000000000'); // 2ˆ-2 -export const a9 = BigNumber.from('128402541668774148407'); // eˆ(x9) -export const x10 = BigNumber.from('12500000000000000000'); // 2ˆ-3 -export const a10 = BigNumber.from('113314845306682631683'); // eˆ(x10) -export const x11 = BigNumber.from('6250000000000000000'); // 2ˆ-4 -export const a11 = BigNumber.from('106449445891785942956'); // eˆ(x11) +export const SQRT_1E_NEG_1 = BigNumber.from('316227766016837933'); +export const SQRT_1E_NEG_3 = BigNumber.from('31622776601683793'); +export const SQRT_1E_NEG_5 = BigNumber.from('3162277660168379'); +export const SQRT_1E_NEG_7 = BigNumber.from('316227766016837'); +export const SQRT_1E_NEG_9 = BigNumber.from('31622776601683'); +export const SQRT_1E_NEG_11 = BigNumber.from('3162277660168'); +export const SQRT_1E_NEG_13 = BigNumber.from('316227766016'); +export const SQRT_1E_NEG_15 = BigNumber.from('31622776601'); +export const SQRT_1E_NEG_17 = BigNumber.from('3162277660'); diff --git a/src/pools/gyro2Pool/gyro2Math.ts b/src/pools/gyro2Pool/gyro2Math.ts index 746775fa..6a9f5cea 100644 --- a/src/pools/gyro2Pool/gyro2Math.ts +++ b/src/pools/gyro2Pool/gyro2Math.ts @@ -1,6 +1,6 @@ import { BigNumber } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; -import { _squareRoot, mulUp, divUp } from './helpers'; +import { _sqrt, mulUp, divUp, mulDown, divDown } from './helpers'; import { _MAX_IN_RATIO, _MAX_OUT_RATIO } from './constants'; ///////// @@ -12,10 +12,7 @@ export function _findVirtualParams( sqrtAlpha: BigNumber, sqrtBeta: BigNumber ): [BigNumber, BigNumber] { - return [ - invariant.mul(ONE).div(sqrtBeta), - invariant.mul(sqrtAlpha).div(ONE), - ]; + return [divDown(invariant, sqrtBeta), mulDown(invariant, sqrtAlpha)]; } ///////// @@ -38,9 +35,13 @@ export function _calculateInvariant( // 2 * a // // // **********************************************************************************************/ - const [a, mb, mc] = _calculateQuadraticTerms(balances, sqrtAlpha, sqrtBeta); + const [a, mb, bSquare, mc] = _calculateQuadraticTerms( + balances, + sqrtAlpha, + sqrtBeta + ); - const invariant = _calculateQuadratic(a, mb, mc); + const invariant = _calculateQuadratic(a, mb, bSquare, mc); return invariant; } @@ -49,31 +50,52 @@ export function _calculateQuadraticTerms( balances: BigNumber[], sqrtAlpha: BigNumber, sqrtBeta: BigNumber -): [BigNumber, BigNumber, BigNumber] { - const a = ONE.sub(sqrtAlpha.mul(ONE).div(sqrtBeta)); - const bterm0 = balances[1].mul(ONE).div(sqrtBeta); - const bterm1 = balances[0].mul(sqrtAlpha).div(ONE); +): [BigNumber, BigNumber, BigNumber, BigNumber] { + const a = ONE.sub(divDown(sqrtAlpha, sqrtBeta)); + const bterm0 = divDown(balances[1], sqrtBeta); + const bterm1 = mulDown(balances[0], sqrtAlpha); const mb = bterm0.add(bterm1); - const mc = balances[0].mul(balances[1]).div(ONE); - - return [a, mb, mc]; + const mc = mulDown(balances[0], balances[1]); + + // For better fixed point precision, calculate in expanded form w/ re-ordering of multiplications + // b^2 = x^2 * alpha + x*y*2*sqrt(alpha/beta) + y^2 / beta + let bSquare = mulDown( + mulDown(mulDown(balances[0], balances[0]), sqrtAlpha), + sqrtAlpha + ); + const bSq2 = divDown( + mulDown( + mulDown(mulDown(balances[0], balances[1]), ONE.mul(2)), + sqrtAlpha + ), + sqrtBeta + ); + + const bSq3 = divDown( + mulDown(balances[1], balances[1]), + mulUp(sqrtBeta, sqrtBeta) + ); + + bSquare = bSquare.add(bSq2).add(bSq3); + + return [a, mb, bSquare, mc]; } export function _calculateQuadratic( a: BigNumber, mb: BigNumber, + bSquare: BigNumber, mc: BigNumber ): BigNumber { - const denominator = a.mul(BigNumber.from(2)); - const bSquare = mb.mul(mb).div(ONE); - const addTerm = a.mul(mc.mul(BigNumber.from(4))).div(ONE); + const denominator = mulUp(a, ONE.mul(2)); + // order multiplications for fixed point precision + const addTerm = mulDown(mulDown(mc, ONE.mul(4)), a); // The minus sign in the radicand cancels out in this special case, so we add const radicand = bSquare.add(addTerm); - const sqrResult = _squareRoot(radicand); - + const sqrResult = _sqrt(radicand, BigNumber.from(5)); // The minus sign in the numerator cancels out in this special case const numerator = mb.add(sqrResult); - const invariant = numerator.mul(ONE).div(denominator); + const invariant = divDown(numerator, denominator); return invariant; } @@ -88,8 +110,7 @@ export function _calcOutGivenIn( balanceOut: BigNumber, amountIn: BigNumber, virtualParamIn: BigNumber, - virtualParamOut: BigNumber, - currentInvariant: BigNumber + virtualParamOut: BigNumber ): BigNumber { /********************************************************************************************** // Described for X = `in' asset and Y = `out' asset, but equivalent for the other case // @@ -104,15 +125,26 @@ export function _calcOutGivenIn( // Note that -dy > 0 is what the trader receives. // // We exploit the fact that this formula is symmetric up to virtualParam{X,Y}. // **********************************************************************************************/ - if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) + if (amountIn.gt(mulDown(balanceIn, _MAX_IN_RATIO))) throw new Error('Swap Amount Too Large'); - const virtIn = balanceIn.add(virtualParamIn); - const denominator = virtIn.add(amountIn); - const invSquare = mulUp(currentInvariant, currentInvariant); - const subtrahend = divUp(invSquare, denominator); - const virtOut = balanceOut.add(virtualParamOut); - return virtOut.sub(subtrahend); + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18. + const virtInOver = balanceIn.add(mulUp(virtualParamIn, ONE.add(2))); + const virtOutUnder = balanceOut.add(mulDown(virtualParamOut, ONE.sub(1))); + + const amountOut = divDown( + mulDown(virtOutUnder, amountIn), + virtInOver.add(amountIn) + ); + + if (amountOut.gte(balanceOut)) throw new Error('ASSET_BOUNDS_EXCEEDED'); + + // This in particular ensures amountOut < balanceOut. + if (amountOut.gt(mulDown(balanceOut, _MAX_OUT_RATIO))) + throw new Error('MAX_OUT_RATIO'); + + return amountOut; } // SwapType = 'swapExactOut' export function _calcInGivenOut( @@ -120,8 +152,7 @@ export function _calcInGivenOut( balanceOut: BigNumber, amountOut: BigNumber, virtualParamIn: BigNumber, - virtualParamOut: BigNumber, - currentInvariant: BigNumber + virtualParamOut: BigNumber ): BigNumber { /********************************************************************************************** // dX = incrX = amountIn > 0 // @@ -136,15 +167,23 @@ export function _calcInGivenOut( // Note that dy < 0 < dx. // **********************************************************************************************/ - if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) + if (amountOut.gt(mulDown(balanceOut, _MAX_OUT_RATIO))) throw new Error('Swap Amount Too Large'); - const virtOut = balanceOut.add(virtualParamOut); - const denominator = virtOut.sub(amountOut); - const invSquare = mulUp(currentInvariant, currentInvariant); - const term = divUp(invSquare, denominator); - const virtIn = balanceIn.add(virtualParamIn); - return term.sub(virtIn); + // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets + // very slightly larger than 3e-18. + const virtInOver = balanceIn.add(mulUp(virtualParamIn, ONE.add(2))); + const virtOutUnder = balanceOut.add(mulDown(virtualParamOut, ONE.sub(1))); + + const amountIn = divUp( + mulUp(virtInOver, amountOut), + virtOutUnder.sub(amountOut) + ); + + if (amountIn.gt(mulDown(balanceIn, _MAX_IN_RATIO))) + throw new Error('Max_IN_RATIO'); + + return amountIn; } // ///////// @@ -174,10 +213,10 @@ export function _calculateNewSpotPrice( const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualParamIn); // x + virtualParamX = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const numerator = virtIn.add(mulDown(afterFeeMultiplier, inAmount)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualParamOut); // y + virtualParamY = y' - const denominator = afterFeeMultiplier.mul(virtOut.sub(outAmount)).div(ONE); // (1 - s) * (y' + dy) - const newSpotPrice = numerator.mul(ONE).div(denominator); + const denominator = mulDown(afterFeeMultiplier, virtOut.sub(outAmount)); // (1 - s) * (y' + dy) + const newSpotPrice = divDown(numerator, denominator); return newSpotPrice; } @@ -208,7 +247,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( const virtOut = balances[1].add(virtualParamOut); // y' = y + virtualParamY const denominator = virtOut.sub(outAmount); // y' + dy - const derivative = TWO.mul(ONE).div(denominator); + const derivative = divDown(TWO, denominator); return derivative; } @@ -238,15 +277,12 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( const TWO = BigNumber.from(2).mul(ONE); const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualParamIn); // x + virtualParamX = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const numerator = virtIn.add(mulDown(afterFeeMultiplier, inAmount)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualParamOut); // y + virtualParamY = y' - const denominator = virtOut - .sub(outAmount) - .mul(virtOut.sub(outAmount)) - .div(ONE); // (y' + dy)^2 - const factor = TWO.mul(ONE).div(afterFeeMultiplier); // 2 / (1 - s) + const denominator = mulDown(virtOut.sub(outAmount), virtOut.sub(outAmount)); // (y' + dy)^2 + const factor = divDown(TWO, afterFeeMultiplier); // 2 / (1 - s) - const derivative = factor.mul(numerator.mul(ONE).div(denominator)).div(ONE); + const derivative = mulDown(factor, divDown(numerator, denominator)); return derivative; } @@ -271,7 +307,7 @@ export function _getNormalizedLiquidity( const virtIn = balances[0].add(virtualParamIn); const afterFeeMultiplier = ONE.sub(swapFee); - const normalizedLiquidity = virtIn.mul(ONE).div(afterFeeMultiplier); + const normalizedLiquidity = divDown(virtIn, afterFeeMultiplier); return normalizedLiquidity; } diff --git a/src/pools/gyro2Pool/gyro2Pool.ts b/src/pools/gyro2Pool/gyro2Pool.ts index ef3dcb86..ad7ba5c6 100644 --- a/src/pools/gyro2Pool/gyro2Pool.ts +++ b/src/pools/gyro2Pool/gyro2Pool.ts @@ -22,7 +22,13 @@ import { _derivativeSpotPriceAfterSwapTokenInForExactTokenOut, _getNormalizedLiquidity, } from './gyro2Math'; -import { _normalizeBalances, _reduceFee, _addFee } from './helpers'; +import { + _normalizeBalances, + _reduceFee, + _addFee, + mulDown, + divDown, +} from './helpers'; export type Gyro2PoolPairData = PoolPairBase & { sqrtAlpha: BigNumber; @@ -119,10 +125,10 @@ export class Gyro2Pool implements PoolBase { swapFee: this.swapFee, sqrtAlpha: tokenInIsToken0 ? this.sqrtAlpha - : ONE.mul(ONE).div(this.sqrtBeta), + : divDown(ONE, this.sqrtBeta), sqrtBeta: tokenInIsToken0 ? this.sqrtBeta - : ONE.mul(ONE).div(this.sqrtAlpha), + : divDown(ONE, this.sqrtAlpha), }; return poolPairData; @@ -161,14 +167,14 @@ export class Gyro2Pool implements PoolBase { if (swapType === SwapTypes.SwapExactIn) { return bnum( formatFixed( - poolPairData.balanceIn.mul(this.MAX_IN_RATIO).div(ONE), + mulDown(poolPairData.balanceIn, this.MAX_IN_RATIO), poolPairData.decimalsIn ) ); } else { return bnum( formatFixed( - poolPairData.balanceOut.mul(this.MAX_OUT_RATIO).div(ONE), + mulDown(poolPairData.balanceOut, this.MAX_OUT_RATIO), poolPairData.decimalsOut ) ); @@ -216,8 +222,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], inAmountLessFee, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); return bnum(formatFixed(outAmount, 18)); @@ -249,8 +254,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], outAmount, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); @@ -284,8 +288,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], inAmountLessFee, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const newSpotPrice = _calculateNewSpotPrice( normalizedBalances, @@ -324,8 +327,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], outAmount, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); const newSpotPrice = _calculateNewSpotPrice( @@ -367,8 +369,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], inAmountLessFee, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const derivative = _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( normalizedBalances, @@ -405,8 +406,7 @@ export class Gyro2Pool implements PoolBase { normalizedBalances[1], outAmount, virtualParamIn, - virtualParamOut, - invariant + virtualParamOut ); const inAmount = _addFee(inAmountLessFee, poolPairData.swapFee); diff --git a/src/pools/gyro2Pool/helpers.ts b/src/pools/gyro2Pool/helpers.ts index ecbf974e..318eb406 100644 --- a/src/pools/gyro2Pool/helpers.ts +++ b/src/pools/gyro2Pool/helpers.ts @@ -1,206 +1,118 @@ import { BigNumber, parseFixed } from '@ethersproject/bignumber'; import { WeiPerEther as ONE } from '@ethersproject/constants'; import { - MAX_POW_RELATIVE_ERROR, - MILD_EXPONENT_BOUND, - LN_36_LOWER_BOUND, - LN_36_UPPER_BOUND, - a0, - a1, - a2, - a3, - a4, - a5, - a6, - a7, - a8, - a9, - a10, - a11, - x0, - x1, - x2, - x3, - x4, - x5, - x6, - x7, - x8, - x9, - x10, - x11, + SQRT_1E_NEG_1, + SQRT_1E_NEG_3, + SQRT_1E_NEG_5, + SQRT_1E_NEG_7, + SQRT_1E_NEG_9, + SQRT_1E_NEG_11, + SQRT_1E_NEG_13, + SQRT_1E_NEG_15, + SQRT_1E_NEG_17, } from './constants'; // Helpers -export function _squareRoot(input: BigNumber): BigNumber { - return powDown(input, ONE.div(2)); -} - -function powDown(a: BigNumber, b: BigNumber) { - const raw = logExpMathPow(a, b); - const maxError = mulUp(raw, MAX_POW_RELATIVE_ERROR).add(1); - - if (raw.lt(maxError)) { - return BigNumber.from(0); - } else { - return raw.sub(maxError); - } -} -function logExpMathPow(x: BigNumber, y: BigNumber): BigNumber { - if (y.isZero()) { - return ONE; - } - if (x.isZero()) { +export function _sqrt(input: BigNumber, tolerance: BigNumber) { + if (input.isZero()) { return BigNumber.from(0); } + let guess = _makeInitialGuess(input); - // Instead of computing x^y directly, we instead rely on the properties of logarithms and exponentiation to - // arrive at that result. In particular, exp(ln(x)) = x, and ln(x^y) = y * ln(x). This means - // x^y = exp(y * ln(x)). - - // The ln function takes a signed value, so we need to make sure x fits in the signed 256 bit range. - if (x.gte(BigNumber.from(2).pow(255))) - throw new Error('logExpMathPow error: Input out of bounds'); - - if (y.gte(MILD_EXPONENT_BOUND)) - throw new Error('logExpMathPow error: Exponent out of bounds'); - - let logXTimesY = BigNumber.from(0); - let isPos = true; - if (x.gt(LN_36_LOWER_BOUND) && x.lt(LN_36_UPPER_BOUND)) { - const ln36A = _ln_36(x); - logXTimesY = ln36A.div(ONE).mul(y).add(ln36A.mod(ONE).mul(y).div(ONE)); - } else { - const [lnA, lnAPos] = _ln(x); - if (!lnAPos) isPos = false; - logXTimesY = lnA.mul(y); + // 7 iterations + for (let i of new Array(7).fill(0)) { + guess = guess.add(input.mul(ONE).div(guess)).div(2); } - logXTimesY = logXTimesY.div(ONE); - - return exp(logXTimesY, isPos); -} -function _ln_36(x: BigNumber): BigNumber { - x = x.mul(ONE); - const ONE36 = ONE.mul(ONE); + // Check in some epsilon range + // Check square is more or less correct + const guessSquared = guess.mul(guess).div(ONE); - const z = x.sub(ONE36).mul(ONE36).div(x.add(ONE36)); - const zSquared = z.mul(z).div(ONE36); + if ( + !( + guessSquared.lte(input.add(mulUp(guess, tolerance))) && + guessSquared.gte(input.sub(mulUp(guess, tolerance))) + ) + ) + throw new Error('Gyro2Pool: _sqrt failed'); - let num = z; - let seriesSum = num; - - for (let i = 3; i <= 15; i = i + 2) { - num = num.mul(zSquared).div(ONE36); - seriesSum = seriesSum.add(num.div(i)); - } - - return seriesSum.mul(2); + return guess; } -function _ln(a: BigNumber): [BigNumber, boolean] { - if (a.lt(ONE)) { - return [_ln(ONE.mul(ONE).div(a))[0], false]; - } - - let sum = BigNumber.from(0); - - if (a.gte(a0.mul(ONE))) { - a = a.div(a0); - sum = sum.add(x0); - } - - if (a.gte(a1.mul(ONE))) { - a = a.div(a1); - sum = sum.add(x1); - } - - sum = sum.mul(100); - a = a.mul(100); - const ONE20 = ONE.mul(100); - - [ - [a2, x2], - [a3, x3], - [a4, x4], - [a5, x5], - [a6, x6], - [a7, x7], - [a8, x8], - [a9, x9], - [a10, x10], - [a11, x11], - ].forEach(([aNum, xNum]) => { - if (a.gte(aNum)) { - a = a.mul(ONE20).div(aNum); - sum = sum.add(xNum); - } - }); - - const z = a.sub(ONE20).mul(ONE20).div(a.add(ONE20)); - const zSquared = z.mul(z).div(ONE20); - - let num = z; - let seriesSum = num; - - for (let i = 3; i <= 11; i = i + 2) { - num = num.mul(zSquared).div(ONE20); - seriesSum = seriesSum.add(num.div(i)); +function _makeInitialGuess(input: BigNumber) { + if (input.gte(ONE)) { + return BigNumber.from(2) + .pow(_intLog2Halved(input.div(ONE))) + .mul(ONE); + } else { + if (input.lte('10')) { + return SQRT_1E_NEG_17; + } + if (input.lte('100')) { + return BigNumber.from('10000000000'); + } + if (input.lte('1000')) { + return SQRT_1E_NEG_15; + } + if (input.lte('10000')) { + return BigNumber.from('100000000000'); + } + if (input.lte('100000')) { + return SQRT_1E_NEG_13; + } + if (input.lte('1000000')) { + return BigNumber.from('1000000000000'); + } + if (input.lte('10000000')) { + return SQRT_1E_NEG_11; + } + if (input.lte('100000000')) { + return BigNumber.from('10000000000000'); + } + if (input.lte('1000000000')) { + return SQRT_1E_NEG_9; + } + if (input.lte('10000000000')) { + return BigNumber.from('100000000000000'); + } + if (input.lte('100000000000')) { + return SQRT_1E_NEG_7; + } + if (input.lte('1000000000000')) { + return BigNumber.from('1000000000000000'); + } + if (input.lte('10000000000000')) { + return SQRT_1E_NEG_5; + } + if (input.lte('100000000000000')) { + return BigNumber.from('10000000000000000'); + } + if (input.lte('1000000000000000')) { + return SQRT_1E_NEG_3; + } + if (input.lte('10000000000000000')) { + return BigNumber.from('100000000000000000'); + } + if (input.lte('100000000000000000')) { + return SQRT_1E_NEG_1; + } + return input; } - - seriesSum = seriesSum.mul(2); - - return [sum.add(seriesSum).div(100), true]; } -function exp(x: BigNumber, isPos: boolean) { - if (!isPos) { - return ONE.mul(ONE).div(exp(x, true)); - } +function _intLog2Halved(x: BigNumber) { + let n = 0; - let firstAN = BigNumber.from(0); - if (x.gte(x0)) { - x = x.sub(x0); - firstAN = a0; - } else if (x.gte(x1)) { - x = x.sub(x1); - firstAN = a1; - } else { - firstAN = BigNumber.from(1); - } - - x = x.mul(100); - const ONE20 = ONE.mul(100); - - let product = ONE20; - - [ - [a2, x2], - [a3, x3], - [a4, x4], - [a5, x5], - [a6, x6], - [a7, x7], - [a8, x8], - [a9, x9], - ].forEach(([aNum, xNum]) => { - if (x.gte(xNum)) { - x = x.sub(xNum); - product = product.mul(aNum).div(ONE20); - } - }); - - let seriesSum = ONE20; - let term = x; - seriesSum = seriesSum.add(term); - - for (let i = 2; i <= 12; i++) { - term = term.mul(x).div(ONE20).div(i); - seriesSum = seriesSum.add(term); + for (let i = 128; i >= 2; i = i / 2) { + const factor = BigNumber.from(2).pow(i); + if (x.gte(factor)) { + x = x.div(factor); + n += i / 2; + } } - return product.mul(seriesSum).div(ONE20).mul(firstAN).div(100); + return n; } export function mulUp(a: BigNumber, b: BigNumber) { @@ -213,6 +125,16 @@ export function divUp(a: BigNumber, b: BigNumber) { return aInflated.sub(1).div(b).add(1); } +export function mulDown(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + return product.div(ONE); +} + +export function divDown(a: BigNumber, b: BigNumber) { + const aInflated = a.mul(ONE); + return aInflated.div(b); +} + export function _normalizeBalances( balances: BigNumber[], decimalsIn: number, diff --git a/src/pools/gyro3Pool/gyro3Math.ts b/src/pools/gyro3Pool/gyro3Math.ts index a58ee6d7..c7b288b1 100644 --- a/src/pools/gyro3Pool/gyro3Math.ts +++ b/src/pools/gyro3Pool/gyro3Math.ts @@ -7,7 +7,14 @@ import { _INVARIANT_MIN_ITERATIONS, _INVARIANT_SHRINKING_FACTOR_PER_STEP, } from './constants'; -import { mulUp, divUp, newtonSqrt, _safeLargePow3ADown } from './helpers'; +import { + mulUp, + divUp, + mulDown, + divDown, + newtonSqrt, + _safeLargePow3ADown, +} from './helpers'; ///////// /// Invariant Calculation @@ -43,18 +50,16 @@ export function _calculateCubicTerms( balances: BigNumber[], root3Alpha: BigNumber ): [BigNumber, BigNumber, BigNumber, BigNumber] { - const alpha23: BigNumber = root3Alpha.mul(root3Alpha).div(ONE); // alpha to the power of (2/3) - const alpha = alpha23.mul(root3Alpha).div(ONE); + const alpha23: BigNumber = mulDown(root3Alpha, root3Alpha); // alpha to the power of (2/3) + const alpha = mulDown(alpha23, root3Alpha); const a = ONE.sub(alpha); const bterm = balances[0].add(balances[1]).add(balances[2]); - const mb = bterm.mul(root3Alpha).div(ONE).mul(root3Alpha).div(ONE); - const cterm = balances[0] - .mul(balances[1]) - .div(ONE) - .add(balances[1].mul(balances[2]).div(ONE)) - .add(balances[2].mul(balances[0]).div(ONE)); - const mc = cterm.mul(root3Alpha).div(ONE); - const md = balances[0].mul(balances[1]).div(ONE).mul(balances[2]).div(ONE); + const mb = mulDown(mulDown(bterm, root3Alpha), root3Alpha); + const cterm = mulDown(balances[0], balances[1]) + .add(mulDown(balances[1], balances[2])) + .add(mulDown(balances[2], balances[0])); + const mc = mulDown(cterm, root3Alpha); + const md = mulDown(mulDown(balances[0], balances[1]), balances[2]); return [a, mb, mc, md]; } @@ -159,26 +164,20 @@ export function _calcNewtonDelta( // Subtraction does not underflow since rootEst is chosen so that it's always above the (only) local minimum. let dfRootEst = BigNumber.from(0); - const rootEst2 = rootEst.mul(rootEst).div(ONE); + const rootEst2 = mulDown(rootEst, rootEst); dfRootEst = rootEst2.mul(3); dfRootEst = dfRootEst.sub( - dfRootEst - .mul(root3Alpha) - .div(ONE) - .mul(root3Alpha) - .div(ONE) - .mul(root3Alpha) - .div(ONE) + mulDown(mulDown(mulDown(dfRootEst, root3Alpha), root3Alpha), root3Alpha) ); - dfRootEst = dfRootEst.sub(rootEst.mul(mb).div(ONE).mul(2)).sub(mc); + dfRootEst = dfRootEst.sub(mulDown(rootEst, mb).mul(2)).sub(mc); const deltaMinus = _safeLargePow3ADown(rootEst, root3Alpha, dfRootEst); // NB: We could order the operations here in much the same way we did above to reduce errors. But tests show // that this has no significant effect, and it would lead to more complex code. - let deltaPlus = rootEst.mul(rootEst).div(ONE).mul(mb).div(ONE); - deltaPlus = deltaPlus.add(rootEst.mul(mc).div(ONE)).mul(ONE).div(dfRootEst); - deltaPlus = deltaPlus.add(md.mul(ONE).div(dfRootEst)); + let deltaPlus = mulDown(mulDown(rootEst, rootEst), mb); + deltaPlus = divDown(deltaPlus.add(mulDown(rootEst, mc)), dfRootEst); + deltaPlus = deltaPlus.add(divDown(md, dfRootEst)); const deltaIsPos = deltaPlus.gte(deltaMinus); const deltaAbs = deltaIsPos @@ -215,7 +214,7 @@ export function _calcOutGivenIn( // Note that -dz > 0 is what the trader receives. // // We exploit the fact that this formula is symmetric up to virtualParam{X,Y,Z}. // **********************************************************************************************/ - if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) + if (amountIn.gt(mulDown(balanceIn, _MAX_IN_RATIO))) throw new Error('Swap Amount In Too Large'); // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets @@ -223,13 +222,13 @@ export function _calcOutGivenIn( // computation. const virtInOver = balanceIn.add(mulUp(virtualOffset, ONE.add(2))); - const virtOutUnder = balanceOut.add(virtualOffset.mul(ONE.sub(1)).div(ONE)); + const virtOutUnder = balanceOut.add(mulDown(virtualOffset, ONE.sub(1))); const amountOut = virtOutUnder.mul(amountIn).div(virtInOver.add(amountIn)); // Note that this in particular reverts if amountOut > balanceOut, i.e., if the out-amount would be more than // the balance. - if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) + if (amountOut.gt(mulDown(balanceOut, _MAX_OUT_RATIO))) throw new Error('Resultant Swap Amount Out Too Large'); return amountOut; @@ -263,21 +262,21 @@ export function _calcInGivenOut( // Note that this in particular reverts if amountOut > balanceOut, i.e., if the trader tries to take more out of // the pool than is in it. - if (amountOut.gt(balanceOut.mul(_MAX_OUT_RATIO).div(ONE))) + if (amountOut.gt(mulDown(balanceOut, _MAX_OUT_RATIO))) throw new Error('Swap Amount Out Too Large'); // The factors in total lead to a multiplicative "safety margin" between the employed virtual offsets // very slightly larger than 3e-18, compensating for the maximum multiplicative error in the invariant // computation. const virtInOver = balanceIn.add(mulUp(virtualOffset, ONE.add(2))); - const virtOutUnder = balanceOut.add(virtualOffset.mul(ONE.sub(1)).div(ONE)); + const virtOutUnder = balanceOut.add(mulDown(virtualOffset, ONE.sub(1))); const amountIn = divUp( mulUp(virtInOver, amountOut), virtOutUnder.sub(amountOut) ); - if (amountIn.gt(balanceIn.mul(_MAX_IN_RATIO).div(ONE))) + if (amountIn.gt(mulDown(balanceIn, _MAX_IN_RATIO))) throw new Error('Resultant Swap Amount In Too Large'); return amountIn; @@ -309,12 +308,12 @@ export function _calculateNewSpotPrice( const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualOffsetInOut); // x + virtualOffsetInOut = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const numerator = virtIn.add(mulDown(afterFeeMultiplier, inAmount)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualOffsetInOut); // z + virtualOffsetInOut = y' - const denominator = afterFeeMultiplier.mul(virtOut.sub(outAmount)).div(ONE); // (1 - s) * (z' + dz) + const denominator = mulDown(afterFeeMultiplier, virtOut.sub(outAmount)); // (1 - s) * (z' + dz) - const newSpotPrice = numerator.mul(ONE).div(denominator); + const newSpotPrice = divDown(numerator, denominator); return newSpotPrice; } @@ -345,7 +344,7 @@ export function _derivativeSpotPriceAfterSwapExactTokenInForTokenOut( const virtOut = balances[1].add(virtualOffsetInOut); // z' = z + virtualOffsetInOut const denominator = virtOut.sub(outAmount); // z' + dz - const derivative = TWO.mul(ONE).div(denominator); + const derivative = divDown(TWO, denominator); return derivative; } @@ -374,15 +373,13 @@ export function _derivativeSpotPriceAfterSwapTokenInForExactTokenOut( const TWO = BigNumber.from(2).mul(ONE); const afterFeeMultiplier = ONE.sub(swapFee); // 1 - s const virtIn = balances[0].add(virtualOffsetInOut); // x + virtualOffsetInOut = x' - const numerator = virtIn.add(afterFeeMultiplier.mul(inAmount).div(ONE)); // x' + (1 - s) * dx + const numerator = virtIn.add(mulDown(afterFeeMultiplier, inAmount)); // x' + (1 - s) * dx const virtOut = balances[1].add(virtualOffsetInOut); // z + virtualOffsetInOut = z' - const denominator = virtOut - .sub(outAmount) - .mul(virtOut.sub(outAmount)) - .div(ONE); // (z' + dz)^2 - const factor = TWO.mul(ONE).div(afterFeeMultiplier); // 2 / (1 - s) + const denominator = mulDown(virtOut.sub(outAmount), virtOut.sub(outAmount)); + // (z' + dz)^2 + const factor = divDown(TWO, afterFeeMultiplier); // 2 / (1 - s) - const derivative = factor.mul(numerator.mul(ONE).div(denominator)).div(ONE); + const derivative = mulDown(factor, divDown(numerator, denominator)); return derivative; } @@ -407,7 +404,7 @@ export function _getNormalizedLiquidity( const virtIn = balances[0].add(virtualParamIn); const afterFeeMultiplier = ONE.sub(swapFee); - const normalizedLiquidity = virtIn.mul(ONE).div(afterFeeMultiplier); + const normalizedLiquidity = divDown(virtIn, afterFeeMultiplier); return normalizedLiquidity; } diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index 0f238e43..d6f2bfff 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -22,7 +22,13 @@ import { _getNormalizedLiquidity, } from './gyro3Math'; -import { _normalizeBalances, _reduceFee, _addFee } from './helpers'; +import { + _normalizeBalances, + _reduceFee, + _addFee, + mulDown, + divDown, +} from './helpers'; export type Gyro3PoolPairData = PoolPairBase & { balanceTertiary: BigNumber; // Balance of the unchanged asset @@ -162,7 +168,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const normalisedLiquidity = _getNormalizedLiquidity( normalizedBalances, @@ -180,14 +186,14 @@ export class Gyro3Pool implements PoolBase { if (swapType === SwapTypes.SwapExactIn) { return bnum( formatFixed( - poolPairData.balanceIn.mul(this.MAX_IN_RATIO).div(ONE), + mulDown(poolPairData.balanceIn, this.MAX_IN_RATIO), poolPairData.decimalsIn ) ); } else { return bnum( formatFixed( - poolPairData.balanceOut.mul(this.MAX_OUT_RATIO).div(ONE), + mulDown(poolPairData.balanceOut, this.MAX_OUT_RATIO), poolPairData.decimalsOut ) ); @@ -228,7 +234,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); @@ -268,7 +274,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmountLessFee = _calcInGivenOut( normalizedBalances[0], @@ -302,7 +308,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); @@ -346,7 +352,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmountLessFee = _calcInGivenOut( normalizedBalances[0], @@ -388,7 +394,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmount = parseFixed(amount.toString(), 18); const inAmountLessFee = _reduceFee(inAmount, poolPairData.swapFee); @@ -430,7 +436,7 @@ export class Gyro3Pool implements PoolBase { this.root3Alpha ); - const virtualOffsetInOut = invariant.mul(this.root3Alpha).div(ONE); + const virtualOffsetInOut = mulDown(invariant, this.root3Alpha); const inAmountLessFee = _calcInGivenOut( normalizedBalances[0], diff --git a/src/pools/gyro3Pool/helpers.ts b/src/pools/gyro3Pool/helpers.ts index 9819a036..9fd1be6d 100644 --- a/src/pools/gyro3Pool/helpers.ts +++ b/src/pools/gyro3Pool/helpers.ts @@ -34,6 +34,16 @@ export function divUp(a: BigNumber, b: BigNumber) { return aInflated.sub(1).div(b).add(1); } +export function mulDown(a: BigNumber, b: BigNumber) { + const product = a.mul(b); + return product.div(ONE); +} + +export function divDown(a: BigNumber, b: BigNumber) { + const aInflated = a.mul(ONE); + return aInflated.div(b); +} + export function newtonSqrt(input: BigNumber, tolerance: BigNumber) { if (input.isZero()) { return BigNumber.from(0); diff --git a/test/testScripts/swapExample.ts b/test/testScripts/swapExample.ts index 0be3585e..b93c2cfa 100644 --- a/test/testScripts/swapExample.ts +++ b/test/testScripts/swapExample.ts @@ -752,10 +752,10 @@ export async function simpleSwap() { const networkId = Network.KOVAN; // Pools source can be Subgraph URL or pools data set passed directly // Update pools list with most recent onchain balances - const tokenIn = ADDRESSES[networkId].GYRO2_TEST1; - const tokenOut = ADDRESSES[networkId].GYRO2_TEST2; + const tokenIn = ADDRESSES[networkId].GYRO3_TEST1; + const tokenOut = ADDRESSES[networkId].GYRO3_TEST2; const swapType = SwapTypes.SwapExactIn; - const swapAmount = parseFixed('100', 18); + const swapAmount = parseFixed('1', 18); const executeTrade = true; const provider = new JsonRpcProvider(PROVIDER_URLS[networkId]); From 5ec86fee493a15a618c3ffdbdbd1fcdf81131e44 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 5 Jul 2022 10:23:16 +0100 Subject: [PATCH 08/11] Fix imports. --- test/gyro2Math.spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/gyro2Math.spec.ts b/test/gyro2Math.spec.ts index 64cd66e9..004740a2 100644 --- a/test/gyro2Math.spec.ts +++ b/test/gyro2Math.spec.ts @@ -7,14 +7,15 @@ import { Gyro2Pool } from '../src/pools/gyro2Pool/gyro2Pool'; // Add new pool test data in Subgraph Schema format import testPools from './testData/gyro2Pools/gyro2TestPool.json'; import { - _addFee, - _calculateInvariant, - _reduceFee, _calculateQuadratic, _calculateQuadraticTerms, _findVirtualParams, - _normalizeBalances, } from '../src/pools/gyro2Pool/gyro2Math'; +import { + _addFee, + _reduceFee, + _normalizeBalances, +} from '../src/pools/gyro2Pool/helpers'; describe('gyro2Math tests', () => { const testPool: any = cloneDeep(testPools).pools[0]; From f7062dcc846e936297168ff58e43d5954b18094f Mon Sep 17 00:00:00 2001 From: Josh Guha Date: Tue, 5 Jul 2022 11:34:22 +0100 Subject: [PATCH 09/11] Update tests --- test/gyro2Math.spec.ts | 8 ++++++-- test/gyro2Pool.spec.ts | 4 ++-- test/gyro3Math.spec.ts | 30 +++++++++++++++++++++--------- test/gyro3Pool.spec.ts | 6 +++--- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/test/gyro2Math.spec.ts b/test/gyro2Math.spec.ts index 004740a2..c4ae4f70 100644 --- a/test/gyro2Math.spec.ts +++ b/test/gyro2Math.spec.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import cloneDeep from 'lodash.clonedeep'; import { formatFixed, parseFixed, BigNumber } from '@ethersproject/bignumber'; +import { WeiPerEther as ONE } from '@ethersproject/constants'; import { USDC, DAI } from './lib/constants'; // Add new PoolType import { Gyro2Pool } from '../src/pools/gyro2Pool/gyro2Pool'; @@ -45,7 +46,7 @@ describe('gyro2Math tests', () => { poolPairData.decimalsIn, poolPairData.decimalsOut ); - const [a, mb, mc] = _calculateQuadraticTerms( + const [a, mb, bSquare, mc] = _calculateQuadraticTerms( normalizedBalances, poolPairData.sqrtAlpha, poolPairData.sqrtBeta @@ -53,9 +54,12 @@ describe('gyro2Math tests', () => { expect(formatFixed(a, 18)).to.equal('0.00099950047470021'); expect(formatFixed(mb, 18)).to.equal('2230.884220626971757449'); + expect(formatFixed(bSquare, 18)).to.equal( + '4976844.405842411200429555' + ); expect(formatFixed(mc, 18)).to.equal('1232000.0'); - const L = _calculateQuadratic(a, mb, mc); + const L = _calculateQuadratic(a, mb, mb.mul(mb).div(ONE), mc); expect(formatFixed(L, 18)).to.equal('2232551.271501112084098627'); }); diff --git a/test/gyro2Pool.spec.ts b/test/gyro2Pool.spec.ts index 30470a5b..0198a22c 100644 --- a/test/gyro2Pool.spec.ts +++ b/test/gyro2Pool.spec.ts @@ -101,7 +101,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(amountOut.toString()).to.eq('13.379816831223949756'); + expect(amountOut.toString()).to.eq('13.379816831223414577'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = @@ -129,7 +129,7 @@ describe('Gyro2Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(amountIn.toString()).to.eq('45.977973896503961218'); + expect(amountIn.toString()).to.eq('45.977973896504501314'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = diff --git a/test/gyro3Math.spec.ts b/test/gyro3Math.spec.ts index 2347875c..d0b74198 100644 --- a/test/gyro3Math.spec.ts +++ b/test/gyro3Math.spec.ts @@ -11,8 +11,8 @@ import { _calculateCubicStartingPoint, _calculateCubicTerms, _runNewtonIteration, - _normalizeBalances, } from '../src/pools/gyro3Pool/gyro3Math'; +import { _normalizeBalances } from '../src/pools/gyro3Pool/helpers'; describe('gyro3Math tests', () => { const testPool: any = cloneDeep(testPools).pools[0]; @@ -53,7 +53,7 @@ describe('gyro3Math tests', () => { const l0 = _calculateCubicStartingPoint(a, mb, mc); - expect(formatFixed(l0, 18)).to.equal('18937948.911434007702525325'); + expect(formatFixed(l0, 18)).to.equal('18937948.911434007702525329'); }); it(`should correctly calculate deltas for Newton method`, async () => { @@ -69,11 +69,12 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst0 ); expect(formatFixed(deltaAbs1, 18)).to.equal( - '20725.034790223169767955' + '20724.666415828607336826' ); expect(deltaIsPos1).to.equal(true); @@ -84,11 +85,12 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst1 ); expect(formatFixed(deltaAbs2, 18)).to.equal( - '45.163851832290322917' + '45.530618963506481754' ); expect(deltaIsPos2).to.equal(false); @@ -99,10 +101,11 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst2 ); - expect(formatFixed(deltaAbs3, 18)).to.equal('0.000214713810115934'); + expect(formatFixed(deltaAbs3, 18)).to.equal('0.366985332320115631'); expect(deltaIsPos3).to.equal(false); const rootEst3 = parseFixed('18958628.782157684771854429', 18); @@ -112,10 +115,11 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst3 ); - expect(formatFixed(deltaAbs4, 18)).to.equal('0.000000000004454138'); + expect(formatFixed(deltaAbs4, 18)).to.equal('0.366770618526583671'); expect(deltaIsPos4).to.equal(false); const rootEst4 = parseFixed('18958628.782157684767400291', 18); @@ -125,16 +129,24 @@ describe('gyro3Math tests', () => { mb, mc, md, + pool.root3Alpha, rootEst4 ); - expect(formatFixed(deltaAbs5, 18)).to.equal('0.000000000004453998'); + expect(formatFixed(deltaAbs5, 18)).to.equal('0.366770618522129533'); expect(deltaIsPos5).to.equal(false); - const finalRootEst = _runNewtonIteration(a, mb, mc, md, rootEst0); + const finalRootEst = _runNewtonIteration( + a, + mb, + mc, + md, + pool.root3Alpha, + rootEst0 + ); expect(formatFixed(finalRootEst, 18)).to.equal( - '18958628.782157684762946293' + '18958628.415387052085178162' ); }); }); diff --git a/test/gyro3Pool.spec.ts b/test/gyro3Pool.spec.ts index 8430bb0c..b13a1cf4 100644 --- a/test/gyro3Pool.spec.ts +++ b/test/gyro3Pool.spec.ts @@ -55,7 +55,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { pool.getNormalizedLiquidity(poolPairData); expect(normalizedLiquidity.toString()).to.equal( - '19016283.610415153991329405' + '19016283.61041515459756377' ); }); }); @@ -69,7 +69,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountIn ); - expect(amountOut.toString()).to.eq('233.628220683475858449'); + expect(amountOut.toString()).to.eq('233.628220683475857751'); }); it('should correctly calculate newSpotPrice', async () => { const newSpotPrice = @@ -97,7 +97,7 @@ describe('Gyro3Pool tests USDC > DAI', () => { poolPairData, amountOut ); - expect(amountIn.toString()).to.eq('4538.618912854584506276'); + expect(amountIn.toString()).to.eq('4538.618912854584519788'); }); it('should correctly calculate newSpotPrice', async () => { From a7c52cfc845fed1b891f3268065e262bed853175 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Tue, 26 Jul 2022 16:54:50 +0100 Subject: [PATCH 10/11] Add gyro2/3 integration tests. --- src/pools/gyro3Pool/gyro3Pool.ts | 5 - test/gyro2.integration.spec.ts | 177 +++++++++++++++++++++++++++++ test/gyro3.integration.spec.ts | 184 +++++++++++++++++++++++++++++++ test/lib/onchainData.ts | 27 ++++- 4 files changed, 387 insertions(+), 6 deletions(-) create mode 100644 test/gyro2.integration.spec.ts create mode 100644 test/gyro3.integration.spec.ts diff --git a/src/pools/gyro3Pool/gyro3Pool.ts b/src/pools/gyro3Pool/gyro3Pool.ts index d6f2bfff..af28828f 100644 --- a/src/pools/gyro3Pool/gyro3Pool.ts +++ b/src/pools/gyro3Pool/gyro3Pool.ts @@ -244,11 +244,6 @@ export class Gyro3Pool implements PoolBase { inAmountLessFee, virtualOffsetInOut ); - console.log( - `!!!!!!! ${amount.toString()} ${bnum( - formatFixed(outAmount, 18) - ).toString()}` - ); return bnum(formatFixed(outAmount, 18)); } diff --git a/test/gyro2.integration.spec.ts b/test/gyro2.integration.spec.ts new file mode 100644 index 00000000..228ee6b9 --- /dev/null +++ b/test/gyro2.integration.spec.ts @@ -0,0 +1,177 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyro2.integration.spec.ts +import dotenv from 'dotenv'; +import { parseFixed } from '@ethersproject/bignumber'; +import { AddressZero } from '@ethersproject/constants'; +import { expect } from 'chai'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { Vault__factory } from '@balancer-labs/typechain'; +import { vaultAddr } from './lib/constants'; +import { + SOR, + SubgraphPoolBase, + SwapTypes, + TokenPriceService, +} from '../src/index'; +import { Network, MULTIADDR, SOR_CONFIG } from './testScripts/constants'; +import { OnChainPoolDataService } from './lib/onchainData'; + +dotenv.config(); + +/* + * Testing on KOVAN + * - Update hardhat.config.js with chainId = 42 + * - Update ALCHEMY_URL on .env with a kovan api key + * - Run kovan node on terminal: yarn run node + * TO DO - Change this test to mainnet once deployed. + */ +const { ALCHEMY_URL: jsonRpcUrl } = process.env; +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new JsonRpcProvider(rpcUrl, 42); +const vault = Vault__factory.connect(vaultAddr, provider); + +const gyro2Pool: SubgraphPoolBase = { + id: '0xd9e058f39c11313103229bd481a8453bddd20d81000200000000000000000999', + address: '0xd9e058f39c11313103229bd481a8453bddd20d81', + poolType: 'Gyro2', + swapFee: '0.09', + totalShares: '20950.113635776677649258', + tokens: [ + { + address: '0x2b7c320d7b915d9d10aeb2f93f94720d4f3fff91', + balance: '1000', + decimals: 18, + weight: null, + priceRate: '1', + }, + { + address: '0x6a7ddccff3141a337f8819fa9d0922e33c405d6f', + balance: '1000', + decimals: 18, + weight: null, + priceRate: '1', + }, + ], + tokensList: [ + '0x2b7c320d7b915d9d10aeb2f93f94720d4f3fff91', + '0x6a7ddccff3141a337f8819fa9d0922e33c405d6f', + ], + totalWeight: '0', + swapEnabled: true, + wrappedIndex: 0, + mainIndex: 0, + sqrtAlpha: '0.9', + sqrtBeta: '1.1', +}; + +// Setup SOR with data services +function setUp(networkId: Network, provider: JsonRpcProvider): SOR { + // The SOR needs to fetch pool data from an external source. This provider fetches from Subgraph and onchain calls. + const subgraphPoolDataService = new OnChainPoolDataService({ + vaultAddress: vaultAddr, + multiAddress: MULTIADDR[networkId], + provider, + pools: [gyro2Pool], + }); + + class CoingeckoTokenPriceService implements TokenPriceService { + constructor(private readonly chainId: number) {} + async getNativeAssetPriceInToken( + tokenAddress: string + ): Promise { + return '0'; + } + } + + // Use coingecko to fetch token price information. Used to calculate cost of additonal swaps/hops. + const coingeckoTokenPriceService = new CoingeckoTokenPriceService( + networkId + ); + + return new SOR( + provider, + SOR_CONFIG[networkId], + subgraphPoolDataService, + coingeckoTokenPriceService + ); +} + +let sor: SOR; + +describe('gyro2 integration tests', () => { + context('test swaps vs queryBatchSwap', () => { + const tokenIn = '0x2b7c320d7b915d9d10aeb2f93f94720d4f3fff91'; + const tokenOut = '0x6a7ddccff3141a337f8819fa9d0922e33c405d6f'; + const swapAmount = parseFixed('17.789', 18); + const funds = { + sender: AddressZero, + recipient: AddressZero, + fromInternalBalance: false, + toInternalBalance: false, + }; + // Setup chain + before(async function () { + this.timeout(20000); + + await provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber: 32941193, + }, + }, + ]); + + const networkId = Network.KOVAN; + sor = setUp(networkId, provider); + await sor.fetchPools(); + }); + + it('ExactIn', async () => { + const swapType = SwapTypes.SwapExactIn; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(queryResult[0].toString()).to.eq( + swapInfo.swapAmount.toString() + ); + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.returnAmount.toString() + ); + }).timeout(10000); + + it('ExactOut', async () => { + const swapType = SwapTypes.SwapExactOut; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + // Amount out should be exact + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.swapAmount.toString() + ); + const deltaIn = queryResult[0].sub(swapInfo.returnAmount); + expect(deltaIn.toNumber()).to.be.lessThan(2); + }).timeout(10000); + }); +}); diff --git a/test/gyro3.integration.spec.ts b/test/gyro3.integration.spec.ts new file mode 100644 index 00000000..735cd9fe --- /dev/null +++ b/test/gyro3.integration.spec.ts @@ -0,0 +1,184 @@ +// TS_NODE_PROJECT='tsconfig.testing.json' npx mocha -r ts-node/register test/gyro3.integration.spec.ts +import dotenv from 'dotenv'; +import { parseFixed } from '@ethersproject/bignumber'; +import { AddressZero } from '@ethersproject/constants'; +import { expect } from 'chai'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { Vault__factory } from '@balancer-labs/typechain'; +import { vaultAddr } from './lib/constants'; +import { + SOR, + SubgraphPoolBase, + SwapTypes, + TokenPriceService, +} from '../src/index'; +import { Network, MULTIADDR, SOR_CONFIG } from './testScripts/constants'; +import { OnChainPoolDataService } from './lib/onchainData'; + +dotenv.config(); + +/* + * Testing on KOVAN + * - Update hardhat.config.js with chainId = 42 + * - Update ALCHEMY_URL on .env with a kovan api key + * - Run kovan node on terminal: yarn run node + * TO DO - Change this test to mainnet once deployed. + */ +const { ALCHEMY_URL: jsonRpcUrl } = process.env; +const rpcUrl = 'http://127.0.0.1:8545'; +const provider = new JsonRpcProvider(rpcUrl, 42); +const vault = Vault__factory.connect(vaultAddr, provider); + +const gyro3Pool: SubgraphPoolBase = { + id: '0x24eabc806e3e9b63f41114d1e323a8aa08472ca7000100000000000000000891', + address: '0x24eabc806e3e9b63f41114d1e323a8aa08472ca7', + poolType: 'Gyro3', + swapFee: '0.09', + totalShares: '6583.623489368247136191', + tokens: [ + { + address: '0x11fb9071e69628d804bf0b197cc61eeacd4aaecf', + balance: '9.999481158411540472', + decimals: 18, + weight: null, + priceRate: '1', + }, + { + address: '0x4ea2110a3e277b10c9b098f61d72f58efa8655db', + balance: '9.997571529493261602', + decimals: 18, + weight: null, + priceRate: '1', + }, + { + address: '0x5663082e6d6addf940a38ea312b899a5ec86c2dc', + balance: '9.99848131028051242', + decimals: 18, + weight: null, + priceRate: '1', + }, + ], + tokensList: [ + '0x11fb9071e69628d804bf0b197cc61eeacd4aaecf', + '0x4ea2110a3e277b10c9b098f61d72f58efa8655db', + '0x5663082e6d6addf940a38ea312b899a5ec86c2dc', + ], + totalWeight: '0', + swapEnabled: true, + wrappedIndex: 0, + mainIndex: 0, + root3Alpha: '0.000099997499906244', +}; + +// Setup SOR with data services +function setUp(networkId: Network, provider: JsonRpcProvider): SOR { + // The SOR needs to fetch pool data from an external source. This provider fetches from Subgraph and onchain calls. + const subgraphPoolDataService = new OnChainPoolDataService({ + vaultAddress: vaultAddr, + multiAddress: MULTIADDR[networkId], + provider, + pools: [gyro3Pool], + }); + + class CoingeckoTokenPriceService implements TokenPriceService { + constructor(private readonly chainId: number) {} + async getNativeAssetPriceInToken( + tokenAddress: string + ): Promise { + return '0'; + } + } + + // Use coingecko to fetch token price information. Used to calculate cost of additonal swaps/hops. + const coingeckoTokenPriceService = new CoingeckoTokenPriceService( + networkId + ); + + return new SOR( + provider, + SOR_CONFIG[networkId], + subgraphPoolDataService, + coingeckoTokenPriceService + ); +} + +let sor: SOR; + +describe('gyro3 integration tests', () => { + context('test swaps vs queryBatchSwap', () => { + const tokenIn = '0x11fb9071e69628d804bf0b197cc61eeacd4aaecf'; + const tokenOut = '0x4ea2110a3e277b10c9b098f61d72f58efa8655db'; + const swapAmount = parseFixed('1.7', 18); + const funds = { + sender: AddressZero, + recipient: AddressZero, + fromInternalBalance: false, + toInternalBalance: false, + }; + // Setup chain + before(async function () { + this.timeout(20000); + + await provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber: 32937471, + }, + }, + ]); + + const networkId = Network.KOVAN; + sor = setUp(networkId, provider); + await sor.fetchPools(); + }); + + it('ExactIn', async () => { + const swapType = SwapTypes.SwapExactIn; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + expect(queryResult[0].toString()).to.eq( + swapInfo.swapAmount.toString() + ); + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.returnAmount.toString() + ); + }).timeout(10000); + + it('ExactOut', async () => { + const swapType = SwapTypes.SwapExactOut; + + const swapInfo = await sor.getSwaps( + tokenIn, + tokenOut, + swapType, + swapAmount + ); + + const queryResult = await vault.callStatic.queryBatchSwap( + swapType, + swapInfo.swaps, + swapInfo.tokenAddresses, + funds + ); + // Amount out should be exact + expect(queryResult[1].abs().toString()).to.eq( + swapInfo.swapAmount.toString() + ); + const deltaIn = queryResult[0].sub(swapInfo.returnAmount); + expect(deltaIn.toNumber()).to.be.lessThan(2); + }).timeout(10000); + }); +}); diff --git a/test/lib/onchainData.ts b/test/lib/onchainData.ts index 6899857f..a3c4d5b0 100644 --- a/test/lib/onchainData.ts +++ b/test/lib/onchainData.ts @@ -1,3 +1,4 @@ +import { JsonRpcProvider } from '@ethersproject/providers'; import { formatFixed } from '@ethersproject/bignumber'; import { Provider } from '@ethersproject/providers'; import { isSameAddress } from '../../src/utils'; @@ -9,7 +10,7 @@ import weightedPoolAbi from '../../src/pools/weightedPool/weightedPoolAbi.json'; import stablePoolAbi from '../../src/pools/stablePool/stablePoolAbi.json'; import elementPoolAbi from '../../src/pools/elementPool/ConvergentCurvePool.json'; import linearPoolAbi from '../../src/pools/linearPool/linearPoolAbi.json'; -import { PoolFilter, SubgraphPoolBase } from '../../src'; +import { PoolFilter, SubgraphPoolBase, PoolDataService } from '../../src'; import { Multicaller } from './multicaller'; export async function getOnChainBalances( @@ -215,3 +216,27 @@ export async function getOnChainBalances( return onChainPools; } + +/* +PoolDataService to fetch onchain balances of Gyro3 pool. +(Fetching all pools off a fork is too intensive) +*/ +export class OnChainPoolDataService implements PoolDataService { + constructor( + private readonly config: { + multiAddress: string; + vaultAddress: string; + provider: JsonRpcProvider; + pools: SubgraphPoolBase[]; + } + ) {} + + public async getPools(): Promise { + return getOnChainBalances( + this.config.pools, + this.config.multiAddress, + this.config.vaultAddress, + this.config.provider + ); + } +} From d606be4339e41ede386d8a6c8a3495399339744f Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 27 Jul 2022 09:31:17 +0100 Subject: [PATCH 11/11] Update version to 4.0.1-beta.2. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4a658e0..47ce6f7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sor", - "version": "4.0.1-beta.1", + "version": "4.0.1-beta.2", "license": "GPL-3.0-only", "main": "dist/index.js", "module": "dist/index.esm.js",