Skip to content

Commit

Permalink
Iterate if initial ratio does not match optimal (Uniswap#6)
Browse files Browse the repository at this point in the history
Co-authored-by: Will Pote <[email protected]>
  • Loading branch information
ewilz and willpote authored Oct 4, 2021
1 parent 06ac449 commit 4af41b9
Show file tree
Hide file tree
Showing 7 changed files with 850 additions and 376 deletions.
21 changes: 16 additions & 5 deletions cli/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,10 @@ export abstract class BaseCommand extends Command {

private _log: Logger | null = null;
private _router: IRouter<any> | null = null;
private _swapToRatioRouter: ISwapToRatio<any> | null = null;
private _swapToRatioRouter: ISwapToRatio<any, any> | null = null;
private _tokenProvider: ITokenProvider | null = null;
private _poolProvider: IPoolProvider | null = null;
private _blockNumber: number | null = null;

get logger() {
return this._log
Expand Down Expand Up @@ -147,6 +148,14 @@ export abstract class BaseCommand extends Command {
}
}

get blockNumber() {
if (this._blockNumber) {
return this._blockNumber;
} else {
throw 'blockNumber not initialized';
}
}

async init() {
const query: ParserOutput<any, any> = this.parse();
const {
Expand Down Expand Up @@ -195,7 +204,9 @@ export abstract class BaseCommand extends Command {
chainId == ChainId.MAINNET ? process.env.JSON_RPC_PROVIDER! : process.env.JSON_RPC_PROVIDER_RINKEBY!,
chainName
);


this._blockNumber = await provider.getBlockNumber()

const tokenCache = new NodeJSCache<Token>(new NodeCache({ stdTTL: 3600, useClones: false }));

let tokenListProvider: CachingTokenListProvider;
Expand Down Expand Up @@ -235,13 +246,13 @@ export abstract class BaseCommand extends Command {
});
} else {
const subgraphCache = new NodeJSCache<SubgraphPool[]>(new NodeCache({ stdTTL: 900, useClones: true }));
const poolCache = new NodeJSCache<Pool>(new NodeCache({ stdTTL: 900, useClones: true }));
const poolCache = new NodeJSCache<Pool>(new NodeCache({ stdTTL: 900, useClones: false }));
const gasPriceCache = new NodeJSCache<GasPrice>(new NodeCache({ stdTTL: 15, useClones: true }));

const router = new AlphaRouter({
provider,
chainId,
subgraphProvider: new CachingSubgraphProvider(chainId,
subgraphProvider: new CachingSubgraphProvider(chainId,
new URISubgraphProvider(chainId, 'https://ipfs.io/ipfs/QmfArMYESGVJpPALh4eQXnjF8HProSF1ky3v8RmuYLJZT4'),
subgraphCache
),
Expand Down
21 changes: 14 additions & 7 deletions cli/commands/quote-to-ratio.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { flags } from '@oclif/command';
import { Currency, Ether, Percent } from '@uniswap/sdk-core';
import { Currency, Ether, Fraction, Percent } from '@uniswap/sdk-core';
import { Position } from '@uniswap/v3-sdk';
import dotenv from 'dotenv';
import { ethers } from 'ethers';
Expand Down Expand Up @@ -48,6 +48,7 @@ export class QuoteToRatio extends BaseCommand {
topNWithBaseToken,
topNWithBaseTokenInSet,
maxSwapsPerPath,
minSplits,
maxSplits,
distributionPercent,
} = flags;
Expand All @@ -74,11 +75,12 @@ export class QuoteToRatio extends BaseCommand {
const tokenInBalance = parseAmount(token0BalanceStr, tokenIn);
const tokenOutBalance = parseAmount(token1BalanceStr, tokenOut);

const poolAccessor = await this.poolProvider.getPools([
[tokenIn.wrapped, tokenOut.wrapped, 3000],
]);
const poolAccessor = await this.poolProvider.getPools(
[[tokenIn.wrapped, tokenOut.wrapped, feeAmount]],
{ blockNumber: this.blockNumber }
);

const pool = poolAccessor?.getPool(
const pool = poolAccessor.getPool(
tokenIn.wrapped,
tokenOut.wrapped,
feeAmount
Expand All @@ -100,24 +102,29 @@ export class QuoteToRatio extends BaseCommand {
});

let swapRoutes: SwapRoute<any> | null;
swapRoutes = await router?.routeToRatio(
swapRoutes = await router.routeToRatio(
tokenInBalance,
tokenOutBalance,
position,
{
errorTolerance: new Fraction(1, 100),
maxIterations: 6
},
{
deadline: 100,
recipient,
slippageTolerance: new Percent(5, 10_000),
},
{
blockNumber: this.blockNumber,
topN,
topNDirectSwaps: 2,
topNTokenInOut,
topNSecondHop,
topNWithEachBaseToken,
topNWithBaseToken,
topNWithBaseTokenInSet,
maxSwapsPerPath,
minSplits,
maxSplits,
distributionPercent,
}
Expand Down
170 changes: 134 additions & 36 deletions src/routers/alpha-router/alpha-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ export const DEFAULT_CONFIG: AlphaRouterConfig = {
distributionPercent: 5,
};

export class AlphaRouter implements IRouter<AlphaRouterConfig>, ISwapToRatio<AlphaRouterConfig> {
export type SwapAndAddConfig = {
errorTolerance: Fraction;
maxIterations: number;
}

export class AlphaRouter implements IRouter<AlphaRouterConfig>, ISwapToRatio<AlphaRouterConfig, SwapAndAddConfig> {
protected chainId: ChainId;
protected provider: providers.BaseProvider;
protected multicall2Provider: IMulticallProvider;
Expand Down Expand Up @@ -119,51 +124,108 @@ export class AlphaRouter implements IRouter<AlphaRouterConfig>, ISwapToRatio<Alp
token0Balance: CurrencyAmount,
token1Balance: CurrencyAmount,
position: Position,
swapAndAddConfig: SwapAndAddConfig,
swapConfig?: SwapConfig,
routingConfig = DEFAULT_CONFIG
): Promise<SwapRoute<TradeType.EXACT_INPUT> | null> {
if (
token0Balance.currency.wrapped.address.toLowerCase()
> token1Balance.currency.wrapped.address.toLowerCase()
) {
if (token1Balance.currency.wrapped.sortsBefore(token0Balance.currency.wrapped)) {
[token0Balance, token1Balance] = [token1Balance, token0Balance]
}

const precision = JSBI.BigInt('1' + '0'.repeat(18))

const sqrtPriceX96 = position.pool.sqrtRatioX96
const sqrtPriceX96Lower = TickMath.getSqrtRatioAtTick(position.tickLower)
const sqrtPriceX96Upper = TickMath.getSqrtRatioAtTick(position.tickUpper)

const token0Proportion = SqrtPriceMath.getAmount0Delta(
sqrtPriceX96,
sqrtPriceX96Upper,
precision,
true
)
const token1Proportion = SqrtPriceMath.getAmount1Delta(
sqrtPriceX96,
sqrtPriceX96Lower,
precision,
let preSwapOptimalRatio = this.calculateOptimalRatio(
position,
position.pool.sqrtRatioX96,
true
)

const amountToSwap = calculateRatioAmountIn(
new Fraction(token0Proportion, token1Proportion),
position.pool.token0Price,
token0Balance,
token1Balance,
)
// set up parameters according to which token will be swapped
let zeroForOne: boolean
if (position.pool.tickCurrent > position.tickUpper) {
zeroForOne = true
} else if (position.pool.tickCurrent < position.tickLower) {
zeroForOne = false
} else {
zeroForOne = new Fraction(token0Balance.quotient, token1Balance.quotient).greaterThan(preSwapOptimalRatio)
if (!zeroForOne) preSwapOptimalRatio = preSwapOptimalRatio.invert()
}

return this.routeExactIn(
amountToSwap.currency,
amountToSwap.currency == token0Balance.currency
? token1Balance.currency
: token0Balance.currency,
amountToSwap,
swapConfig,
routingConfig
)
const [inputBalance, outputBalance] = zeroForOne
? [token0Balance, token1Balance]
: [token1Balance, token0Balance]

let optimalRatio = preSwapOptimalRatio
let exchangeRate: Fraction = zeroForOne
? position.pool.token0Price
: position.pool.token1Price
let swap: SwapRoute<TradeType.EXACT_INPUT> | null = null
let ratioAchieved = false
let n = 0

// iterate until we find a swap with a sufficient ratio or return null
while (!ratioAchieved) {
n++
if (n > swapAndAddConfig.maxIterations) {
return null;
}

let amountToSwap = calculateRatioAmountIn(
optimalRatio,
exchangeRate,
inputBalance,
outputBalance,
)

swap = await this.routeExactIn(
inputBalance.currency,
outputBalance.currency,
amountToSwap,
swapConfig,
routingConfig
)
if (!swap) {
return null;
}

let inputBalanceUpdated = inputBalance.subtract(swap.trade.inputAmount)
let outputBalanceUpdated = outputBalance.add(swap.trade.outputAmount)
let newRatio = inputBalanceUpdated.divide(outputBalanceUpdated)

let targetPoolHit = false
swap.route.forEach(route => {
route.route.pools.forEach((pool, i) => {
if(
pool.token0.equals(position.pool.token0) &&
pool.token1.equals(position.pool.token1) &&
pool.fee == position.pool.fee
) {
targetPoolHit = true
optimalRatio = this.calculateOptimalRatio(
position,
JSBI.BigInt(route.sqrtPriceX96AfterList[i]!.toString()),
zeroForOne,
)
}
})
})
if (!targetPoolHit) {
optimalRatio = preSwapOptimalRatio
}

ratioAchieved = (
newRatio.equalTo(optimalRatio) ||
this.absoluteValue(newRatio.asFraction.divide(optimalRatio).subtract(1)).lessThan(swapAndAddConfig.errorTolerance)
)
exchangeRate = swap.trade.outputAmount.divide(swap.trade.inputAmount)

log.info({
optimalRatio: optimalRatio.asFraction.toFixed(18),
newRatio: newRatio.asFraction.toFixed(18),
errorTolerance: swapAndAddConfig.errorTolerance.toFixed(18),
iterationN: n.toString()
})
}

return swap
}

public async routeExactIn(
Expand Down Expand Up @@ -542,4 +604,40 @@ export class AlphaRouter implements IRouter<AlphaRouterConfig>, ISwapToRatio<Alp
'Log for gas model'
);
}

private calculateOptimalRatio(position: Position, sqrtRatioX96: JSBI, zeroForOne: boolean): Fraction {
const upperSqrtRatioX96 = TickMath.getSqrtRatioAtTick(position.tickUpper);
const lowerSqrtRatioX96 = TickMath.getSqrtRatioAtTick(position.tickLower);

// returns Fraction(0, 1) for any out of range position regardless of zeroForOne. Implication: function
// cannot be used to determine the trading direction of out of range positions.
if (JSBI.greaterThan(sqrtRatioX96, upperSqrtRatioX96) || JSBI.lessThan(sqrtRatioX96, lowerSqrtRatioX96)) {
return new Fraction(0,1)
}

const precision = JSBI.BigInt('1' + '0'.repeat(18))
let optimalRatio = new Fraction(
SqrtPriceMath.getAmount0Delta(
sqrtRatioX96,
upperSqrtRatioX96,
precision,
true
),
SqrtPriceMath.getAmount1Delta(
sqrtRatioX96,
lowerSqrtRatioX96,
precision,
true
)
)
if (!zeroForOne) optimalRatio = optimalRatio.invert()
return optimalRatio
}

private absoluteValue(fraction: Fraction): Fraction {
if (fraction.lessThan(0)) {
return fraction.multiply(-1)
}
return fraction
}
}
24 changes: 12 additions & 12 deletions src/routers/alpha-router/functions/calculate-ratio-amount-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { CurrencyAmount } from '../../../util/amounts';

export function calculateRatioAmountIn(
optimalRatio: Fraction,
token0Price: Fraction,
token0Balance: CurrencyAmount,
token1Balance: CurrencyAmount
inputTokenPrice: Fraction,
inputBalance: CurrencyAmount,
outputBalance: CurrencyAmount
): CurrencyAmount {
// formula: amountToSwap = (token0Balance - (optimalRatio * token1Balance)) / ((optimalRatio * token0Price) + 1))
const amountToSwapRaw = new Fraction(token0Balance.quotient)
.subtract(optimalRatio.multiply(token1Balance.quotient))
.divide(optimalRatio.multiply(token0Price).add(1));
// formula: amountToSwap = (inputBalance - (optimalRatio * outputBalance)) / ((optimalRatio * inputTokenPrice) + 1))
const amountToSwapRaw = new Fraction(inputBalance.quotient)
.subtract(optimalRatio.multiply(outputBalance.quotient))
.divide(optimalRatio.multiply(inputTokenPrice).add(1));

const zeroForOne = !amountToSwapRaw.lessThan(0)
if (amountToSwapRaw.lessThan(0)) {
// should never happen since we do checks before calling in
throw new Error('routeToRatio: insufficient input token amount')
}

return CurrencyAmount.fromRawAmount(
zeroForOne ? token0Balance.currency : token1Balance.currency,
zeroForOne ? amountToSwapRaw.quotient : amountToSwapRaw.multiply(-1).multiply(token0Price).quotient
);
return CurrencyAmount.fromRawAmount(inputBalance.currency, amountToSwapRaw.quotient)
}
3 changes: 2 additions & 1 deletion src/routers/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ export abstract class IRouter<RoutingConfig> {
): Promise<SwapRoute<TradeType.EXACT_OUTPUT> | null>;
}

export abstract class ISwapToRatio<RoutingConfig> {
export abstract class ISwapToRatio<RoutingConfig, SwapAndAddConfig> {
abstract routeToRatio(
token0Balance: CurrencyAmount,
token1Balance: CurrencyAmount,
position: Position,
swapAndAddConfig: SwapAndAddConfig,
swapConfig?: SwapConfig,
routingConfig?: RoutingConfig
): Promise<SwapRoute<TradeType.EXACT_INPUT> | null>;
Expand Down
Loading

0 comments on commit 4af41b9

Please sign in to comment.