@guru-fund/sdk 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,5 +37,12 @@ export interface QuoteDepositResult {
37
37
  txData: TransactionRequest;
38
38
  decodeLogs: (logs: SimulationLog[]) => QuoteDepositLogs | null;
39
39
  }
40
+ type DepositAllocationQuote = {
41
+ asset: Fund.Asset;
42
+ amountIn: bigint;
43
+ amountOut: bigint;
44
+ };
40
45
  export declare function allocateDepositInputAmounts(assets: Fund.Asset[], totalAmount: bigint, totalValueLocked: bigint): Map<string, bigint>;
46
+ export declare function rebalanceDepositInputAmounts(quotes: DepositAllocationQuote[], totalAmount: bigint): Map<string, bigint>;
41
47
  export default function quoteDeposit(params: QuoteDepositParams, ctx: QuoteDepositContext): Promise<QuoteDepositResult>;
48
+ export {};
@@ -8,28 +8,30 @@ import { quoteDepositSchema } from '../schemas/quoteDeposit.js';
8
8
  import buildDepositTx from '../txBuilders/buildDepositTx.js';
9
9
  import { FundLedger__factory } from '../typechain/index.js';
10
10
  const MAX_UINT256 = (1n << 256n) - 1n;
11
+ const MIN_ROUTE_INPUT_AMOUNT = 2n;
12
+ const ROUTE_AWARE_ALLOCATION_PASSES = 1;
11
13
  const minBigint = (a, b) => (a < b ? a : b);
12
14
  const slippageE2For = (settings, token) => {
13
15
  const slippage = settings?.[token.toLowerCase()];
14
16
  return slippage == null ? undefined : Number(BigInt(slippage) / 10n);
15
17
  };
16
18
  const assetUsd1e18Value = (asset) => (asset.usd1e18Price * asset.balance) / Token.unitFor(asset.token.decimals);
17
- export function allocateDepositInputAmounts(assets, totalAmount, totalValueLocked) {
19
+ function allocateWeightedInputAmounts(items, totalAmount) {
18
20
  const allocations = new Map();
19
- if (totalAmount === 0n || totalValueLocked === 0n) {
20
- for (const asset of assets) {
21
- allocations.set(asset.token.address.toLowerCase(), 0n);
22
- }
21
+ if (totalAmount === 0n) {
22
+ for (const item of items)
23
+ allocations.set(item.key, 0n);
23
24
  return allocations;
24
25
  }
25
- const weighted = assets.map((asset) => {
26
- const usdValue = assetUsd1e18Value(asset);
27
- const weightedAmount = totalAmount * usdValue;
28
- const amount = weightedAmount / totalValueLocked;
26
+ const weighted = items.map((item) => {
27
+ const weightedAmount = totalAmount * item.weight;
28
+ const amount = item.denominator === 0n ? 0n : weightedAmount / item.denominator;
29
29
  return {
30
- asset,
30
+ ...item,
31
31
  amount,
32
- remainder: weightedAmount % totalValueLocked,
32
+ remainder: item.denominator === 0n
33
+ ? 0n
34
+ : weightedAmount % item.denominator,
33
35
  };
34
36
  });
35
37
  const allocated = weighted.reduce((sum, item) => sum + item.amount, 0n);
@@ -38,11 +40,116 @@ export function allocateDepositInputAmounts(assets, totalAmount, totalValueLocke
38
40
  .sort((a, b) => a.remainder === b.remainder ? 0 : a.remainder > b.remainder ? -1 : 1)
39
41
  .forEach((item) => {
40
42
  const extra = remainder > 0n ? 1n : 0n;
41
- allocations.set(item.asset.token.address.toLowerCase(), item.amount + extra);
43
+ allocations.set(item.key, item.amount + extra);
42
44
  remainder -= extra;
43
45
  });
46
+ const zeroRoundedAssets = weighted
47
+ .filter((item) => item.balance > 0n && (allocations.get(item.key) ?? 0n) === 0n)
48
+ .sort((a, b) => a.remainder === b.remainder ? 0 : a.remainder > b.remainder ? -1 : 1);
49
+ for (const item of zeroRoundedAssets) {
50
+ const donor = weighted
51
+ .filter((candidate) => candidate.key !== item.key)
52
+ .sort((a, b) => {
53
+ const aAmount = allocations.get(a.key) ?? 0n;
54
+ const bAmount = allocations.get(b.key) ?? 0n;
55
+ return aAmount === bAmount ? 0 : aAmount > bAmount ? -1 : 1;
56
+ })
57
+ .find((candidate) => (allocations.get(candidate.key) ?? 0n) >
58
+ MIN_ROUTE_INPUT_AMOUNT);
59
+ if (!donor)
60
+ break;
61
+ allocations.set(donor.key, (allocations.get(donor.key) ?? 0n) - MIN_ROUTE_INPUT_AMOUNT);
62
+ allocations.set(item.key, MIN_ROUTE_INPUT_AMOUNT);
63
+ }
44
64
  return allocations;
45
65
  }
66
+ export function allocateDepositInputAmounts(assets, totalAmount, totalValueLocked) {
67
+ if (totalAmount === 0n || totalValueLocked === 0n) {
68
+ const allocations = new Map();
69
+ for (const asset of assets) {
70
+ allocations.set(asset.token.address.toLowerCase(), 0n);
71
+ }
72
+ return allocations;
73
+ }
74
+ return allocateWeightedInputAmounts(assets.map((asset) => ({
75
+ key: asset.token.address.toLowerCase(),
76
+ weight: assetUsd1e18Value(asset),
77
+ denominator: totalValueLocked,
78
+ balance: asset.balance,
79
+ })), totalAmount);
80
+ }
81
+ export function rebalanceDepositInputAmounts(quotes, totalAmount) {
82
+ let denominator = 0n;
83
+ const weights = quotes.map(({ asset, amountIn, amountOut }) => {
84
+ let weight = 0n;
85
+ if (asset.balance > 0n && amountIn > 0n && amountOut > 0n) {
86
+ const inputRatio = (amountOut * UNIT) / asset.balance;
87
+ if (inputRatio > 0n) {
88
+ weight = (amountIn * UNIT) / inputRatio;
89
+ }
90
+ }
91
+ denominator += weight;
92
+ return {
93
+ key: asset.token.address.toLowerCase(),
94
+ weight,
95
+ denominator: 0n,
96
+ balance: asset.balance,
97
+ };
98
+ });
99
+ if (denominator === 0n) {
100
+ return new Map(quotes.map(({ asset, amountIn }) => [
101
+ asset.token.address.toLowerCase(),
102
+ amountIn,
103
+ ]));
104
+ }
105
+ return allocateWeightedInputAmounts(weights.map((item) => ({ ...item, denominator })), totalAmount);
106
+ }
107
+ function allocationsEqual(a, b) {
108
+ if (a.size !== b.size)
109
+ return false;
110
+ for (const [key, value] of a) {
111
+ if (b.get(key) !== value)
112
+ return false;
113
+ }
114
+ return true;
115
+ }
116
+ async function quoteDepositAllocations({ inputAllocations, coinData, fundData, parsed, ctx, vault, }) {
117
+ const allocationQuotes = [];
118
+ if (coinData) {
119
+ const amountIn = inputAllocations.get(coinData.token.address.toLowerCase()) ?? 0n;
120
+ if (coinData.balance > 0n && amountIn === 0n) {
121
+ throw new Error(`Deposit amount is too small to allocate input for asset ${coinData.token.address}`);
122
+ }
123
+ allocationQuotes.push({
124
+ asset: coinData,
125
+ amountIn,
126
+ amountOut: amountIn,
127
+ });
128
+ }
129
+ const assetsToSwap = fundData.assets.filter((asset) => !compareAddresses(asset.token.address, parsed.coin));
130
+ const routeResults = await Promise.all(assetsToSwap.map(async (asset) => {
131
+ const amountIn = inputAllocations.get(asset.token.address.toLowerCase()) ?? 0n;
132
+ if (asset.balance > 0n && amountIn === 0n) {
133
+ throw new Error(`Deposit amount is too small to allocate input for asset ${asset.token.address}`);
134
+ }
135
+ const route = await getRouteIn({
136
+ chainId: ctx.chainId,
137
+ tokenIn: parsed.coin,
138
+ tokenOut: asset.token.address,
139
+ amountIn,
140
+ account: parsed.account,
141
+ vault,
142
+ slippageE2: slippageE2For(parsed.slippageSettings, asset.token.address),
143
+ }, ctx);
144
+ allocationQuotes.push({
145
+ asset,
146
+ amountIn,
147
+ amountOut: BigInt(route.data.amountToReceive),
148
+ });
149
+ return { asset, amountIn, route };
150
+ }));
151
+ return { allocationQuotes, routeResults };
152
+ }
46
153
  export default async function quoteDeposit(params, ctx) {
47
154
  const parsed = quoteDepositSchema.parse(params);
48
155
  if (parsed.referrerFeeBps < 0n || parsed.referrerFeeBps > MAX_BPS) {
@@ -65,32 +172,36 @@ export default async function quoteDeposit(params, ctx) {
65
172
  let fees = (originalAmount * referrerFeeBps) / MAX_BPS;
66
173
  const adjustedAmount = originalAmount - fees;
67
174
  const coinData = fundData.assets.find((asset) => compareAddresses(asset.token.address, parsed.coin));
68
- const inputAllocations = allocateDepositInputAmounts(fundData.assets, adjustedAmount, fundData.totalValueLocked);
69
- const coinInputAmount = coinData
70
- ? (inputAllocations.get(coinData.token.address.toLowerCase()) ?? 0n)
71
- : 0n;
72
- if (coinData && coinData.balance > 0n && coinInputAmount === 0n) {
73
- throw new Error(`Deposit amount is too small to allocate input for asset ${coinData.token.address}`);
74
- }
75
- let lowestInputRatio = coinData && coinData.balance > 0n
76
- ? (coinInputAmount * UNIT) / coinData.balance
77
- : MAX_UINT256;
78
- const assetsToSwap = fundData.assets.filter((asset) => !compareAddresses(asset.token.address, parsed.coin));
79
- const routeResults = await Promise.all(assetsToSwap.map(async (asset) => {
80
- const amountIn = inputAllocations.get(asset.token.address.toLowerCase()) ?? 0n;
81
- if (asset.balance > 0n && amountIn === 0n) {
82
- throw new Error(`Deposit amount is too small to allocate input for asset ${asset.token.address}`);
83
- }
84
- const route = await getRouteIn({
85
- chainId: ctx.chainId,
86
- tokenIn: parsed.coin,
87
- tokenOut: asset.token.address,
88
- amountIn,
89
- account: parsed.account,
175
+ let inputAllocations = allocateDepositInputAmounts(fundData.assets, adjustedAmount, fundData.totalValueLocked);
176
+ let quoted = await quoteDepositAllocations({
177
+ inputAllocations,
178
+ coinData,
179
+ fundData,
180
+ parsed,
181
+ ctx,
182
+ vault,
183
+ });
184
+ for (let i = 0; i < ROUTE_AWARE_ALLOCATION_PASSES; i++) {
185
+ const rebalanced = rebalanceDepositInputAmounts(quoted.allocationQuotes, adjustedAmount);
186
+ if (allocationsEqual(inputAllocations, rebalanced))
187
+ break;
188
+ inputAllocations = rebalanced;
189
+ quoted = await quoteDepositAllocations({
190
+ inputAllocations,
191
+ coinData,
192
+ fundData,
193
+ parsed,
194
+ ctx,
90
195
  vault,
91
- }, ctx);
92
- return { asset, route };
93
- }));
196
+ });
197
+ }
198
+ let lowestInputRatio = MAX_UINT256;
199
+ for (const { asset, amountOut } of quoted.allocationQuotes) {
200
+ if (asset.balance === 0n)
201
+ continue;
202
+ lowestInputRatio = minBigint(lowestInputRatio, (amountOut * UNIT) / asset.balance);
203
+ }
204
+ const routeResults = quoted.routeResults;
94
205
  const coinToken = await new Token(parsed.coin, provider).metadata();
95
206
  const coinUsd1e18Price = await ctx.getPriceUsd1e18(parsed.coin);
96
207
  const externalTokenMetadata = new Map();
@@ -142,7 +253,6 @@ export default async function quoteDeposit(params, ctx) {
142
253
  callData: route.callData,
143
254
  });
144
255
  fees += await tollToCoinUnits(route.toll.amount, route.toll.currency);
145
- lowestInputRatio = minBigint(lowestInputRatio, (BigInt(route.data.amountToReceive) * UNIT) / asset.balance);
146
256
  const bps = route.effectiveSlippageBps;
147
257
  if (bps != null) {
148
258
  const bpsN = BigInt(bps);
@@ -1,5 +1,5 @@
1
1
  export declare const VELORA_ENDPOINT = "https://api.velora.xyz/swap";
2
- export declare const SUPPORTED_DEXS: readonly ["UniswapV2", "UniswapV3", "UniswapV4", "AerodromeV2", "PancakeSwapV2", "PancakeSwapV3"];
2
+ export declare const SUPPORTED_DEXS: readonly ["UniswapV2", "UniswapV3", "UniswapV4", "AerodromeV3", "AerodromeV2", "PancakeSwapV2", "PancakeSwapV3"];
3
3
  export declare const VELORA_VERSION_BY_DEX: Partial<Record<SupportedDex, string>>;
4
4
  export type SupportedDex = (typeof SUPPORTED_DEXS)[number];
5
5
  export declare const INITIAL_SLIPPAGE_E3 = 500n;
@@ -3,6 +3,7 @@ export const SUPPORTED_DEXS = [
3
3
  'UniswapV2',
4
4
  'UniswapV3',
5
5
  'UniswapV4',
6
+ 'AerodromeV3',
6
7
  'AerodromeV2',
7
8
  'PancakeSwapV2',
8
9
  'PancakeSwapV3',
@@ -15,6 +15,7 @@ export declare function connectV4Quoter(address: string, provider: Provider): V4
15
15
  export declare function toAdapterPathKeys(path: V4Path): SwapV4Struct['path'];
16
16
  type V4QuoteSource = 'quoter' | 'adapter-preview';
17
17
  export interface ApplyV4TollParams {
18
+ tollIn: boolean;
18
19
  baseToll: {
19
20
  currency: string;
20
21
  amount: bigint;
@@ -32,7 +33,7 @@ export interface ApplyV4TollResult {
32
33
  amountToSend: bigint;
33
34
  initialTollAmount: bigint;
34
35
  }
35
- export declare function applyV4Toll({ baseToll, quotedAmountToReceive, quoteSource, swapAmountIn, }: ApplyV4TollParams): ApplyV4TollResult;
36
+ export declare function applyV4Toll({ tollIn, baseToll, quotedAmountToReceive, quoteSource, swapAmountIn, }: ApplyV4TollParams): ApplyV4TollResult;
36
37
  export declare function toHookAdapterPathKeys(path: V4Path): V4HookPathKey[];
37
38
  export interface GetUniswapV4RouteContext {
38
39
  provider: Provider;
@@ -45,15 +45,17 @@ function isV4HookLockedError(reason) {
45
45
  }
46
46
  return false;
47
47
  }
48
- export function applyV4Toll({ baseToll, quotedAmountToReceive, quoteSource, swapAmountIn, }) {
48
+ export function applyV4Toll({ tollIn, baseToll, quotedAmountToReceive, quoteSource, swapAmountIn, }) {
49
49
  const toll = { ...baseToll };
50
50
  let amountQuoted = quotedAmountToReceive;
51
51
  let amountToSend = swapAmountIn;
52
52
  let initialTollAmount = toll.amount;
53
53
  if (toll.amount === 0n) {
54
54
  if (quoteSource === 'quoter') {
55
- toll.amount = quotedAmountToReceive / TOLL_DIVISOR_20BPS;
56
- amountQuoted -= toll.amount;
55
+ if (!tollIn) {
56
+ toll.amount = quotedAmountToReceive / TOLL_DIVISOR_20BPS;
57
+ amountQuoted -= toll.amount;
58
+ }
57
59
  }
58
60
  initialTollAmount = 0n;
59
61
  }
@@ -194,7 +196,7 @@ export default async function getUniswapV4Route(params, ctx) {
194
196
  throw new Error(`UniswapV4 not supported on chainId ${chainId}`);
195
197
  }
196
198
  const maxSlippageE3 = slippageE2 != null ? BigInt(slippageE2) * 10n : undefined;
197
- const { baseToll, swapAmountIn } = resolveBaseToll(addresses, tokenIn, tokenOut, amountIn);
199
+ const { tollIn, baseToll, swapAmountIn } = resolveBaseToll(addresses, tokenIn, tokenOut, amountIn);
198
200
  const candidates = await discoverV4Paths(chainId, tokenIn, tokenOut, provider);
199
201
  if (candidates.length === 0) {
200
202
  throw new Error('NO_UNISWAP_V4_ROUTES_FOUND');
@@ -254,6 +256,7 @@ export default async function getUniswapV4Route(params, ctx) {
254
256
  const selectedAdapter = bestQuote.adapter ?? adapter;
255
257
  const bestPath = bestQuote.path;
256
258
  const { toll, amountQuoted, amountToSend, initialTollAmount } = applyV4Toll({
259
+ tollIn,
257
260
  baseToll,
258
261
  quotedAmountToReceive: bestQuote.amountToReceive,
259
262
  quoteSource: bestQuote.source,
@@ -296,6 +299,7 @@ export default async function getUniswapV4Route(params, ctx) {
296
299
  amountToSend,
297
300
  amountQuoted,
298
301
  initialTollAmount,
302
+ outputTollE3: tollIn ? 0n : undefined,
299
303
  maxSlippageE3,
300
304
  });
301
305
  return {
@@ -20,7 +20,7 @@ export default async function getVeloraRoute(params, ctx) {
20
20
  tokenOutDecimals ?? fetchDecimals(tokenOut, provider),
21
21
  FundVault__factory.connect(vault, provider).controller(),
22
22
  ]);
23
- const { baseToll, swapAmountIn } = resolveBaseToll(addresses, tokenIn, tokenOut, amountIn);
23
+ const { tollIn, baseToll, swapAmountIn } = resolveBaseToll(addresses, tokenIn, tokenOut, amountIn);
24
24
  const routes = (await Promise.all(SUPPORTED_DEXS.map(async (dex) => {
25
25
  try {
26
26
  if (!isDexConfigured(addresses, dex))
@@ -47,6 +47,7 @@ export default async function getVeloraRoute(params, ctx) {
47
47
  dex,
48
48
  cachedPath,
49
49
  amountIn: swapAmountIn,
50
+ tollIn,
50
51
  toll,
51
52
  vault,
52
53
  controller,
@@ -1,4 +1,4 @@
1
- import { parseEther } from 'ethers';
1
+ import { Contract, parseEther, ZeroAddress } from 'ethers';
2
2
  import { getGuruProtocolAddresses, } from '../addresses.js';
3
3
  import compareAddresses from '../helpers/compareAddresses.js';
4
4
  import { Token } from '../helpers/Token.js';
@@ -7,6 +7,33 @@ import getUniswapV4Route, { connectV4Quoter, toAdapterPathKeys, } from './getUni
7
7
  import getVeloraRoute from './getVeloraRoute.js';
8
8
  import PoolHelper from './poolHelper.js';
9
9
  import { discoverV4Paths } from './v4PoolDiscovery.js';
10
+ const AERO_V3_FACTORY_ABI = [
11
+ 'function getPool(address tokenA, address tokenB, int24 tickSpacing) view returns (address pool)',
12
+ ];
13
+ const V2_FACTORY_ABI = [
14
+ 'function getPair(address tokenA, address tokenB) view returns (address pair)',
15
+ ];
16
+ const V2_ROUTER_ABI = [
17
+ 'function getAmountsOut(uint256 amountIn, address[] memory path) view returns (uint256[] memory amounts)',
18
+ ];
19
+ const AERO_V2_FACTORY_ABI = [
20
+ 'function getPool(address tokenA, address tokenB, bool stable) view returns (address)',
21
+ ];
22
+ const AERO_V2_ROUTER_ABI = [
23
+ 'function getAmountsOut(uint256 amountIn, (address from, address to, bool stable, address factory)[] routes) view returns (uint256[] amounts)',
24
+ ];
25
+ const V3_FACTORY_ABI = [
26
+ 'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
27
+ ];
28
+ const V3_QUOTER_ABI = [
29
+ 'function quoteExactInputSingle((address tokenIn,address tokenOut,uint256 amountIn,uint24 fee,uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut,uint160 sqrtPriceX96After,uint32 initializedTicksCrossed,uint256 gasEstimate)',
30
+ ];
31
+ const AERO_V3_QUOTER_ABI = [
32
+ 'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, int24 tickSpacing, uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
33
+ ];
34
+ const UNISWAP_V3_FEES = [100, 500, 3000, 10000];
35
+ const PANCAKE_V3_FEES = [100, 500, 2500, 10000];
36
+ const AERODROME_V3_TICK_SPACINGS = [1, 50, 100, 200];
10
37
  export async function veloraThenV4Discovery(velora, v4Discovery) {
11
38
  try {
12
39
  return await velora();
@@ -97,9 +124,7 @@ export async function getPriceUsd1e18(token, ctx) {
97
124
  }
98
125
  const { decimals } = await new Token(token, ctx.provider).metadata();
99
126
  const oneToken = 10n ** BigInt(decimals);
100
- const stableUsd = await _priceViaDirectStablePool(token, oneToken, poolHelper, stable, stableDecimals);
101
- if (stableUsd !== null)
102
- return stableUsd;
127
+ const stableUsd = await _priceViaDirectStablePool(token, oneToken, poolHelper, stable, stableDecimals, ctx.provider);
103
128
  const WETH_UNIT = 10n ** 18n;
104
129
  try {
105
130
  const wethUsd = await getWethUsd();
@@ -108,33 +133,157 @@ export async function getPriceUsd1e18(token, ctx) {
108
133
  path: [token, addresses.tokens.WETH],
109
134
  requireSwappable: false,
110
135
  });
111
- return (oneTokenInWeth * wethUsd) / WETH_UNIT;
136
+ const wethRouteUsd = (oneTokenInWeth * wethUsd) / WETH_UNIT;
137
+ return stableUsd !== null && stableUsd > wethRouteUsd
138
+ ? stableUsd
139
+ : wethRouteUsd;
112
140
  }
113
141
  catch (error) {
114
142
  const v4Usd = await _priceViaV4Pools(token, oneToken, getWethUsd, ctx);
143
+ if (stableUsd !== null && v4Usd !== null) {
144
+ return stableUsd > v4Usd ? stableUsd : v4Usd;
145
+ }
146
+ if (stableUsd !== null)
147
+ return stableUsd;
115
148
  if (v4Usd !== null)
116
149
  return v4Usd;
117
150
  throw error;
118
151
  }
119
152
  }
120
- async function _priceViaDirectStablePool(token, oneToken, poolHelper, stable, stableDecimals) {
121
- const factory = poolHelper.addresses.factories.aerodromeV2;
122
- if (!factory)
123
- return null;
124
- const route = await poolHelper
125
- .getTokensForStableQuote({
126
- path: [token, stable],
127
- inputAmount: oneToken,
128
- slippage: 0n,
129
- exchangeFactory: factory,
130
- })
131
- .catch(() => null);
132
- if (!route)
133
- return null;
134
- const amountToReceive = BigInt(route.data.amountToReceive);
135
- if (amountToReceive === 0n)
153
+ async function _priceViaDirectStablePool(token, oneToken, poolHelper, stable, stableDecimals, provider) {
154
+ const stableUnit = Token.unitFor(stableDecimals);
155
+ const toUsd1e18 = (amount) => (amount * 10n ** 18n) / stableUnit;
156
+ const quotes = [];
157
+ const v2Amount = await _quoteDirectV2StablePools(token, stable, oneToken, poolHelper, provider);
158
+ if (v2Amount > 0n)
159
+ quotes.push(v2Amount);
160
+ const v3Amount = await _quoteDirectV3StablePools(token, stable, oneToken, poolHelper, provider);
161
+ if (v3Amount > 0n)
162
+ quotes.push(v3Amount);
163
+ const aeroV3Amount = await _quoteDirectAerodromeV3StablePool(token, stable, oneToken, poolHelper, provider);
164
+ if (aeroV3Amount > 0n)
165
+ quotes.push(aeroV3Amount);
166
+ if (quotes.length === 0)
136
167
  return null;
137
- return (amountToReceive * 10n ** 18n) / Token.unitFor(stableDecimals);
168
+ const best = quotes.reduce((currentBest, amount) => amount > currentBest ? amount : currentBest);
169
+ return toUsd1e18(best);
170
+ }
171
+ async function _quoteDirectV2StablePools(token, stable, oneToken, poolHelper, provider) {
172
+ let best = 0n;
173
+ const quoteStandardV2 = async (factoryAddress, routerAddress) => {
174
+ if (!factoryAddress || !routerAddress)
175
+ return;
176
+ const factory = new Contract(factoryAddress, V2_FACTORY_ABI, provider);
177
+ const pairAddress = await factory
178
+ .getPair(token, stable)
179
+ .catch(() => ZeroAddress);
180
+ if (pairAddress === ZeroAddress)
181
+ return;
182
+ const router = new Contract(routerAddress, V2_ROUTER_ABI, provider);
183
+ const amounts = await router
184
+ .getAmountsOut(oneToken, [token, stable])
185
+ .catch(() => null);
186
+ const amountOut = amounts?.at(-1) ?? 0n;
187
+ if (amountOut > best)
188
+ best = amountOut;
189
+ };
190
+ const quoteAerodromeV2 = async (factoryAddress, routerAddress) => {
191
+ if (!factoryAddress || !routerAddress)
192
+ return;
193
+ const factory = new Contract(factoryAddress, AERO_V2_FACTORY_ABI, provider);
194
+ const router = new Contract(routerAddress, AERO_V2_ROUTER_ABI, provider);
195
+ await Promise.all([false, true].map(async (stablePool) => {
196
+ const poolAddress = await factory
197
+ .getPool(token, stable, stablePool)
198
+ .catch(() => ZeroAddress);
199
+ if (poolAddress === ZeroAddress)
200
+ return;
201
+ const amounts = await router
202
+ .getAmountsOut(oneToken, [
203
+ {
204
+ from: token,
205
+ to: stable,
206
+ stable: stablePool,
207
+ factory: factoryAddress,
208
+ },
209
+ ])
210
+ .catch(() => null);
211
+ const amountOut = amounts?.at(-1) ?? 0n;
212
+ if (amountOut > best)
213
+ best = amountOut;
214
+ }));
215
+ };
216
+ await Promise.all([
217
+ quoteStandardV2(poolHelper.addresses.factories.uniswapV2, poolHelper.addresses.routers.uniswapV2),
218
+ quoteStandardV2(poolHelper.addresses.factories.pancakeV2, poolHelper.addresses.routers.pancakeV2),
219
+ quoteAerodromeV2(poolHelper.addresses.factories.aerodromeV2, poolHelper.addresses.routers.aerodromeV2),
220
+ ]);
221
+ return best;
222
+ }
223
+ async function _quoteDirectV3StablePools(token, stable, oneToken, poolHelper, provider) {
224
+ let best = 0n;
225
+ const quoteFactory = async (factoryAddress, quoterAddress, fees) => {
226
+ if (!factoryAddress || !quoterAddress)
227
+ return;
228
+ const factory = new Contract(factoryAddress, V3_FACTORY_ABI, provider);
229
+ const quoter = new Contract(quoterAddress, V3_QUOTER_ABI, provider);
230
+ await Promise.all(fees.map(async (fee) => {
231
+ const poolAddress = await factory
232
+ .getPool(token, stable, fee)
233
+ .catch(() => ZeroAddress);
234
+ if (poolAddress === ZeroAddress)
235
+ return;
236
+ const quote = await quoter.quoteExactInputSingle.staticCall({
237
+ tokenIn: token,
238
+ tokenOut: stable,
239
+ amountIn: oneToken,
240
+ fee: BigInt(fee),
241
+ sqrtPriceLimitX96: 0n,
242
+ }).catch(() => null);
243
+ const amountOut = quote?.amountOut ?? 0n;
244
+ if (amountOut > best)
245
+ best = amountOut;
246
+ }));
247
+ };
248
+ await Promise.all([
249
+ quoteFactory(poolHelper.addresses.factories.uniswapV3, poolHelper.addresses.quoters.uniswapV3, UNISWAP_V3_FEES),
250
+ quoteFactory(poolHelper.addresses.factories.pancakeV3, poolHelper.addresses.quoters.pancakeV3, PANCAKE_V3_FEES),
251
+ ]);
252
+ return best;
253
+ }
254
+ async function _quoteDirectAerodromeV3StablePool(token, stable, oneToken, poolHelper, provider) {
255
+ const quoterAddress = poolHelper.addresses.quoters.aerodromeV3;
256
+ if (!quoterAddress)
257
+ return 0n;
258
+ const factories = [
259
+ poolHelper.addresses.factories.aerodromeV3,
260
+ poolHelper.addresses.factories.aerodromeV3Bis,
261
+ ].filter((factory) => Boolean(factory));
262
+ if (factories.length === 0)
263
+ return 0n;
264
+ const quoter = new Contract(quoterAddress, AERO_V3_QUOTER_ABI, provider);
265
+ let best = 0n;
266
+ await Promise.all(factories.flatMap((factoryAddress) => {
267
+ const factory = new Contract(factoryAddress, AERO_V3_FACTORY_ABI, provider);
268
+ return AERODROME_V3_TICK_SPACINGS.map(async (tickSpacing) => {
269
+ const poolAddress = await factory
270
+ .getPool(token, stable, tickSpacing)
271
+ .catch(() => ZeroAddress);
272
+ if (poolAddress === ZeroAddress)
273
+ return;
274
+ const quote = await quoter.quoteExactInputSingle.staticCall({
275
+ tokenIn: token,
276
+ tokenOut: stable,
277
+ amountIn: oneToken,
278
+ tickSpacing: BigInt(tickSpacing),
279
+ sqrtPriceLimitX96: 0n,
280
+ }).catch(() => null);
281
+ const amountOut = quote?.amountOut ?? 0n;
282
+ if (amountOut > best)
283
+ best = amountOut;
284
+ });
285
+ }));
286
+ return best;
138
287
  }
139
288
  async function _priceViaV4Pools(token, oneToken, getWethUsd, ctx) {
140
289
  const addresses = getGuruProtocolAddresses(ctx.chainId);
@@ -32,10 +32,18 @@ export function extractPathFromResponse(dex, response) {
32
32
  const swapPath = swap.swapExchanges[0].data.path;
33
33
  if (!Array.isArray(swapPath) ||
34
34
  typeof swapPath[0]?.tokenIn !== 'string' ||
35
- typeof swapPath[0]?.fee !== 'string') {
35
+ swapPath[0]?.fee == null) {
36
36
  throw new Error('Unexpected V3 path structure');
37
37
  }
38
- combinedPath.push(...swapPath);
38
+ combinedPath.push(...swapPath.map((hop) => ({
39
+ tokenIn: hop.tokenIn,
40
+ tokenOut: hop.tokenOut,
41
+ fee: String(dex === 'AerodromeV3'
42
+ ? (hop.tickSpacing ?? hop.fee)
43
+ : hop.fee),
44
+ currentFee: hop.currentFee,
45
+ tickSpacing: hop.tickSpacing,
46
+ })));
39
47
  }
40
48
  return { type: 'v3', path: combinedPath, hops: combinedPath.length };
41
49
  }
@@ -21,6 +21,9 @@ const V2_PAIR_ABI = [
21
21
  const V3_FACTORY_ABI = [
22
22
  'function getPool(address tokenA, address tokenB, uint24 fee) view returns (address pool)',
23
23
  ];
24
+ const AERO_V3_FACTORY_ABI = [
25
+ 'function getPool(address tokenA, address tokenB, int24 tickSpacing) view returns (address pool)',
26
+ ];
24
27
  const V3_POOL_ABI = ['function fee() view returns (uint24)'];
25
28
  const AERO_FACTORY_ABI = [
26
29
  'function getPool(address tokenA, address tokenB, bool stable) view returns (address)',
@@ -260,7 +263,9 @@ export default class PoolHelper {
260
263
  }
261
264
  }
262
265
  else {
263
- const factory = connect(factoryAddress, V3_FACTORY_ABI, this.provider);
266
+ const factory = dex.kind === 'aerodrome'
267
+ ? connect(factoryAddress, AERO_V3_FACTORY_ABI, this.provider)
268
+ : connect(factoryAddress, V3_FACTORY_ABI, this.provider);
264
269
  await Promise.all(dex.feeTiers.map(async (feeTier) => {
265
270
  try {
266
271
  const poolAddress = await factory.getPool(token, weth, feeTier);
@@ -491,6 +496,7 @@ export default class PoolHelper {
491
496
  amountToSend: inputAmount,
492
497
  amountQuoted: lastAmount(amounts, exchangeFactory),
493
498
  initialTollAmount: swapFee,
499
+ outputTollE3: 0n,
494
500
  slippage,
495
501
  buildSwap: (amountToReceive) => {
496
502
  const data = {
@@ -522,6 +528,7 @@ export default class PoolHelper {
522
528
  amountToSend: inputAmount,
523
529
  amountQuoted: lastAmount(amounts, exchangeFactory),
524
530
  initialTollAmount: swapFee,
531
+ outputTollE3: 0n,
525
532
  slippage,
526
533
  buildSwap: (amountToReceive) => {
527
534
  const data = {
@@ -585,6 +592,7 @@ export default class PoolHelper {
585
592
  amountToSend: inputAmount,
586
593
  amountQuoted: amountOut,
587
594
  initialTollAmount: swapFee,
595
+ outputTollE3: 0n,
588
596
  slippage,
589
597
  buildSwap: (amountToReceive) => {
590
598
  const data = {
@@ -40,6 +40,8 @@ export type V3PathHop = {
40
40
  tokenIn: string;
41
41
  tokenOut: string;
42
42
  fee: string;
43
+ currentFee?: string | number;
44
+ tickSpacing?: string | number;
43
45
  };
44
46
  export type V3Path = V3PathHop[];
45
47
  export type CachedPath = {
@@ -14,6 +14,7 @@ interface V3DexEntry {
14
14
  interface VeloraDexConfig {
15
15
  UniswapV2?: V2DexEntry;
16
16
  AerodromeV2?: V2DexEntry;
17
+ AerodromeV3?: V3DexEntry;
17
18
  PancakeSwapV2?: V2DexEntry;
18
19
  UniswapV3?: V3DexEntry;
19
20
  PancakeSwapV3?: V3DexEntry;
@@ -29,6 +30,7 @@ export interface GetRouteFromPathParams {
29
30
  dex: SupportedDex;
30
31
  cachedPath: CachedPath;
31
32
  amountIn: bigint;
33
+ tollIn: boolean;
32
34
  toll: {
33
35
  currency: string;
34
36
  amount: bigint;
@@ -39,5 +41,5 @@ export interface GetRouteFromPathParams {
39
41
  maxSlippageE3?: bigint;
40
42
  prefixTxs?: PrefixTx[];
41
43
  }
42
- export declare function getRouteFromPath({ chainId, addresses, provider, simulator, dex, cachedPath, amountIn, toll, vault, controller, account, maxSlippageE3, prefixTxs, }: GetRouteFromPathParams): Promise<Route>;
44
+ export declare function getRouteFromPath({ chainId, addresses, provider, simulator, dex, cachedPath, amountIn, tollIn, toll, vault, controller, account, maxSlippageE3, prefixTxs, }: GetRouteFromPathParams): Promise<Route>;
43
45
  export {};
@@ -1,6 +1,6 @@
1
1
  import { Contract, solidityPacked } from 'ethers';
2
2
  import { TOLL_DIVISOR_20BPS } from '../constants.js';
3
- import { AerodromeV2Adapter__factory, IPancakeQuoterV2__factory, UniswapV2Adapter__factory, UniswapV3Adapter__factory, UniswapV4Adapter__factory, } from '../typechain/index.js';
3
+ import { AerodromeV3Adapter__factory, AerodromeV2Adapter__factory, IPancakeQuoterV2__factory, UniswapV2Adapter__factory, UniswapV3Adapter__factory, UniswapV4Adapter__factory, } from '../typechain/index.js';
4
4
  import { connectV4Quoter, toAdapterPathKeys } from './getUniswapV4Route.js';
5
5
  import { SWAP_DEADLINE_SECONDS } from './constants.js';
6
6
  import { finalizeRouteQuote } from './finalizeRoute.js';
@@ -10,12 +10,18 @@ const V2_ROUTER_ABI = [
10
10
  const AERO_V2_ROUTER_ABI = [
11
11
  'function getAmountsOut(uint256 amountIn, (address from, address to, bool stable, address factory)[] routes) external view returns (uint256[] amounts)',
12
12
  ];
13
+ const AERO_V3_QUOTER_ABI = [
14
+ 'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, int24 tickSpacing, uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
15
+ ];
13
16
  function connectV2Router(address, provider) {
14
17
  return new Contract(address, V2_ROUTER_ABI, provider);
15
18
  }
16
19
  function connectAeroV2Router(address, provider) {
17
20
  return new Contract(address, AERO_V2_ROUTER_ABI, provider);
18
21
  }
22
+ function connectAeroV3Quoter(address, provider) {
23
+ return new Contract(address, AERO_V3_QUOTER_ABI, provider);
24
+ }
19
25
  function connectV3Quoter(address, provider) {
20
26
  return IPancakeQuoterV2__factory.connect(address, provider);
21
27
  }
@@ -33,6 +39,12 @@ export function buildVeloraDexConfig(addresses) {
33
39
  routerAddress: addresses.routers.aerodromeV2,
34
40
  };
35
41
  }
42
+ if (addresses.adapters.aerodromeV3 && addresses.quoters.aerodromeV3) {
43
+ cfg.AerodromeV3 = {
44
+ adapter: addresses.adapters.aerodromeV3,
45
+ quoterAddress: addresses.quoters.aerodromeV3,
46
+ };
47
+ }
36
48
  if (addresses.adapters.pancakeV2 && addresses.routers.pancakeV2) {
37
49
  cfg.PancakeSwapV2 = {
38
50
  adapter: addresses.adapters.pancakeV2,
@@ -71,7 +83,7 @@ function encodeV3Path(path) {
71
83
  }
72
84
  return solidityPacked(pathEncoding, pathToEncode);
73
85
  }
74
- export async function getRouteFromPath({ chainId, addresses, provider, simulator, dex, cachedPath, amountIn, toll, vault, controller, account, maxSlippageE3, prefixTxs, }) {
86
+ export async function getRouteFromPath({ chainId, addresses, provider, simulator, dex, cachedPath, amountIn, tollIn, toll, vault, controller, account, maxSlippageE3, prefixTxs, }) {
75
87
  if (!cachedPath) {
76
88
  throw new Error('No cached path');
77
89
  }
@@ -95,7 +107,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
95
107
  ]);
96
108
  let amountQuoted = grossAmountToReceive;
97
109
  let amountToSend = amountIn;
98
- if (toll.amount === 0n) {
110
+ if (!tollIn && toll.amount === 0n) {
99
111
  toll.amount = grossAmountToReceive / TOLL_DIVISOR_20BPS;
100
112
  amountQuoted -= toll.amount;
101
113
  }
@@ -128,6 +140,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
128
140
  amountToSend,
129
141
  amountQuoted,
130
142
  initialTollAmount: toll.amount,
143
+ outputTollE3: tollIn ? 0n : undefined,
131
144
  maxSlippageE3,
132
145
  });
133
146
  return {
@@ -163,7 +176,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
163
176
  ]);
164
177
  let amountQuoted = grossAmountToReceive;
165
178
  let amountToSend = amountIn;
166
- if (toll.amount === 0n) {
179
+ if (!tollIn && toll.amount === 0n) {
167
180
  toll.amount = grossAmountToReceive / TOLL_DIVISOR_20BPS;
168
181
  amountQuoted -= toll.amount;
169
182
  }
@@ -196,6 +209,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
196
209
  amountToSend,
197
210
  amountQuoted,
198
211
  initialTollAmount: toll.amount,
212
+ outputTollE3: tollIn ? 0n : undefined,
199
213
  maxSlippageE3,
200
214
  });
201
215
  return {
@@ -212,6 +226,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
212
226
  effectiveSlippageBps,
213
227
  };
214
228
  }
229
+ case 'AerodromeV3':
215
230
  case 'PancakeSwapV3':
216
231
  case 'UniswapV3': {
217
232
  if (cachedPath.type !== 'v3') {
@@ -222,15 +237,43 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
222
237
  throw new Error(`DEX ${dex} not supported on chainId ${chainId}`);
223
238
  }
224
239
  const adapter = entry.adapter;
225
- const quoter = connectV3Quoter(entry.quoterAddress, provider);
226
- const encodedPath = encodeV3Path(cachedPath.path);
227
- const [{ amountOut: grossAmountToReceive }, blockNumber] = await Promise.all([
228
- quoter.quoteExactInput.staticCall(encodedPath, amountIn),
229
- provider.getBlockNumber(),
230
- ]);
240
+ let encodedPath;
241
+ let grossAmountToReceive;
242
+ let blockNumber;
243
+ if (dex === 'AerodromeV3') {
244
+ if (cachedPath.path.length !== 1) {
245
+ throw new Error('Aerodrome V3 multi-hop not supported');
246
+ }
247
+ const [hop] = cachedPath.path;
248
+ const tickSpacing = BigInt(hop.fee);
249
+ encodedPath = solidityPacked(['address', 'uint24', 'address'], [hop.tokenIn, Number(tickSpacing), hop.tokenOut]);
250
+ const quoter = connectAeroV3Quoter(entry.quoterAddress, provider);
251
+ const [quote, currentBlockNumber] = await Promise.all([
252
+ quoter.quoteExactInputSingle.staticCall({
253
+ tokenIn: hop.tokenIn,
254
+ tokenOut: hop.tokenOut,
255
+ amountIn,
256
+ tickSpacing,
257
+ sqrtPriceLimitX96: 0n,
258
+ }),
259
+ provider.getBlockNumber(),
260
+ ]);
261
+ grossAmountToReceive = quote.amountOut;
262
+ blockNumber = currentBlockNumber;
263
+ }
264
+ else {
265
+ const quoter = connectV3Quoter(entry.quoterAddress, provider);
266
+ encodedPath = encodeV3Path(cachedPath.path);
267
+ const [quote, currentBlockNumber] = await Promise.all([
268
+ quoter.quoteExactInput.staticCall(encodedPath, amountIn),
269
+ provider.getBlockNumber(),
270
+ ]);
271
+ grossAmountToReceive = quote.amountOut;
272
+ blockNumber = currentBlockNumber;
273
+ }
231
274
  let amountQuoted = grossAmountToReceive;
232
275
  let amountToSend = amountIn;
233
- if (toll.amount === 0n) {
276
+ if (!tollIn && toll.amount === 0n) {
234
277
  toll.amount = grossAmountToReceive / TOLL_DIVISOR_20BPS;
235
278
  amountQuoted -= toll.amount;
236
279
  }
@@ -238,7 +281,10 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
238
281
  amountToSend += toll.amount;
239
282
  }
240
283
  const deadline = Math.floor(Date.now() / 1000) + SWAP_DEADLINE_SECONDS;
241
- const buildCallDataForAmount = (amountToReceive) => UniswapV3Adapter__factory.createInterface().encodeFunctionData('executeSwap', [
284
+ const swapInterface = dex === 'AerodromeV3'
285
+ ? AerodromeV3Adapter__factory.createInterface()
286
+ : UniswapV3Adapter__factory.createInterface();
287
+ const buildCallDataForAmount = (amountToReceive) => swapInterface.encodeFunctionData('executeSwap', [
242
288
  {
243
289
  amountToSend,
244
290
  amountToReceive,
@@ -263,6 +309,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
263
309
  amountToSend,
264
310
  amountQuoted,
265
311
  initialTollAmount: toll.amount,
312
+ outputTollE3: tollIn ? 0n : undefined,
266
313
  maxSlippageE3,
267
314
  });
268
315
  return {
@@ -301,7 +348,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
301
348
  ]);
302
349
  let amountQuoted = grossAmountToReceive;
303
350
  let amountToSend = amountIn;
304
- if (toll.amount === 0n) {
351
+ if (!tollIn && toll.amount === 0n) {
305
352
  toll.amount = grossAmountToReceive / TOLL_DIVISOR_20BPS;
306
353
  amountQuoted -= toll.amount;
307
354
  }
@@ -335,6 +382,7 @@ export async function getRouteFromPath({ chainId, addresses, provider, simulator
335
382
  amountToSend,
336
383
  amountQuoted,
337
384
  initialTollAmount: toll.amount,
385
+ outputTollE3: tollIn ? 0n : undefined,
338
386
  maxSlippageE3,
339
387
  });
340
388
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guru-fund/sdk",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Guru Protocol SDK — quote + transaction builders for on-chain managed funds.",
5
5
  "license": "MIT",
6
6
  "type": "module",