Skip to content

Commit

Permalink
feat: Initial Exit functionality and Weighted Pool implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
johngrantuk committed Sep 13, 2023
1 parent 3cafa27 commit 294c082
Show file tree
Hide file tree
Showing 12 changed files with 703 additions and 23 deletions.
2 changes: 2 additions & 0 deletions src/entities/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types';
export * from './replaceWrapped';
12 changes: 12 additions & 0 deletions src/entities/common/replaceWrapped.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Token } from '../token';
import { NATIVE_ASSETS, ZERO_ADDRESS } from '../../utils';

export function replaceWrapped(tokens: Token[], chainId: number): Token[] {
return tokens.map((token) => {
if (token.isUnderlyingEqual(NATIVE_ASSETS[chainId])) {
return new Token(chainId, ZERO_ADDRESS, 18);
} else {
return token;
}
});
}
12 changes: 12 additions & 0 deletions src/entities/common/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Address } from '../../types';

// Returned from API and used as input
export type PoolState = {
id: Address;
address: Address;
type: string;
tokens: {
address: Address;
decimals: number;
}[]; // already properly sorted in case different versions sort them differently
};
1 change: 1 addition & 0 deletions src/entities/exit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types';
21 changes: 21 additions & 0 deletions src/entities/exit/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { BaseExit, ExitConfig } from './types';
import { WeightedExit } from './weighted/weightedExit';

/*********************** Basic Helper to get exit class from pool type *************/

export class ExitParser {
private readonly poolExits: Record<string, BaseExit> = {};

constructor(config?: ExitConfig) {
const { customPoolExits } = config || {};
this.poolExits = {
Weighted: new WeightedExit(),
// custom pool Exits take precedence over base Exits
...customPoolExits,
};
}

public getExit(poolType: string): BaseExit {
return this.poolExits[poolType];
}
}
77 changes: 77 additions & 0 deletions src/entities/exit/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { TokenAmount } from '../tokenAmount';
import { Slippage } from '../slippage';
import { Address } from '../../types';
import { PoolState } from '../common';

export enum ExitKind {
UNBALANCED = 'UNBALANCED', // exitExactOut
SINGLE_ASSET = 'SINGLE_ASSET', // exitExactInSingleAsset
PROPORTIONAL = 'PROPORTIONAL', // exitExactInProportional
}

// This will be extended for each pools specific output requirements
export type BaseExitInput = {
chainId: number;
rpcUrl?: string;
exitWithNativeAsset?: boolean;
};

export type UnbalancedExitInput = BaseExitInput & {
amountsOut: TokenAmount[];
kind: ExitKind.UNBALANCED;
};

export type SingleAssetExitInput = BaseExitInput & {
bptIn: TokenAmount;
tokenOut: Address;
kind: ExitKind.SINGLE_ASSET;
};

export type ProportionalExitInput = BaseExitInput & {
bptIn: TokenAmount;
kind: ExitKind.PROPORTIONAL;
};

export type ExitInput =
| UnbalancedExitInput
| SingleAssetExitInput
| ProportionalExitInput;

// Returned from a exit query
export type ExitQueryResult = {
id: Address;
exitKind: ExitKind;
bptIn: TokenAmount;
amountsOut: TokenAmount[];
tokenOutIndex?: number;
};

export type ExitCallInput = ExitQueryResult & {
slippage: Slippage;
sender: Address;
recipient: Address;
};

export type BuildOutput = {
call: Address;
to: Address;
value: bigint | undefined;
maxBptIn: bigint;
minAmountsOut: bigint[];
};

export interface BaseExit {
query(input: ExitInput, poolState: PoolState): Promise<ExitQueryResult>;
buildCall(input: ExitCallInput): BuildOutput;
}

export type ExitConfig = {
customPoolExits: Record<string, BaseExit>;
};

export type ExitPoolRequest = {
assets: Address[];
minAmountsOut: bigint[];
userData: Address;
toInternalBalance: boolean;
};
29 changes: 29 additions & 0 deletions src/entities/exit/weighted/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Address } from '../../../types';
import { ExitPoolRequest } from '../types';

export function getExitParameters({
poolId,
assets,
sender,
recipient,
minAmountsOut,
userData,
toInternalBalance,
}: {
poolId: Address;
assets: Address[];
sender: Address;
recipient: Address;
minAmountsOut: bigint[];
userData: Address;
toInternalBalance: boolean;
}) {
const exitPoolRequest: ExitPoolRequest = {
assets, // with BPT
minAmountsOut, // with BPT
userData, // wihtout BPT
toInternalBalance,
};

return [poolId, sender, recipient, exitPoolRequest] as const;
}
180 changes: 180 additions & 0 deletions src/entities/exit/weighted/weightedExit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { createPublicClient, encodeFunctionData, http } from 'viem';
import { Token, TokenAmount, WeightedEncoder } from '../../..';
import { Address } from '../../../types';
import {
BALANCER_HELPERS,
BALANCER_VAULT,
CHAINS,
MAX_UINT256,
ZERO_ADDRESS,
} from '../../../utils';
import { balancerHelpersAbi, vaultAbi } from '../../../abi';
import { getExitParameters } from './helpers';
import {
BaseExit,
BuildOutput,
ExitCallInput,
ExitInput,
ExitKind,
ExitQueryResult,
} from '../types';
import { PoolState, replaceWrapped } from '../../common';

export class WeightedExit implements BaseExit {
public async query(
input: ExitInput,
poolState: PoolState,
): Promise<ExitQueryResult> {
// TODO - This would need extended to work with relayer

const poolTokens = poolState.tokens.map(
(t) => new Token(input.chainId, t.address, t.decimals),
);
let minAmountsOut = Array(poolTokens.length).fill(0n);
let userData: Address;

switch (input.kind) {
case ExitKind.UNBALANCED:
minAmountsOut = poolTokens.map(
(t) =>
input.amountsOut.find((a) => a.token.isEqual(t))
?.amount ?? 0n,
);
userData = WeightedEncoder.exitExactOut(
minAmountsOut,
MAX_UINT256,
);
break;
case ExitKind.SINGLE_ASSET:
userData = WeightedEncoder.exitExactInSingleAsset(
input.bptIn.amount,
poolTokens.findIndex(
(t) => t.address === input.tokenOut.toLowerCase(),
),
);
break;
case ExitKind.PROPORTIONAL:
userData = WeightedEncoder.exitExactInProportional(
input.bptIn.amount,
);
break;
}

let tokensOut = [...poolTokens];
// replace wrapped token with native asset if needed
if (input.exitWithNativeAsset)
tokensOut = replaceWrapped(poolTokens, input.chainId);

const queryArgs = getExitParameters({
poolId: poolState.id,
assets: tokensOut.map((t) => t.address),
sender: ZERO_ADDRESS,
recipient: ZERO_ADDRESS,
minAmountsOut,
userData,
toInternalBalance: false, // TODO - Should we make this part of input?
});

const client = createPublicClient({
transport: http(input.rpcUrl),
chain: CHAINS[input.chainId],
});

const {
result: [queryBptIn, queryAmountsOut],
} = await client.simulateContract({
address: BALANCER_HELPERS[input.chainId],
abi: balancerHelpersAbi,
functionName: 'queryExit',
args: queryArgs,
});

const bpt = new Token(input.chainId, poolState.address, 18);
const bptIn = TokenAmount.fromRawAmount(bpt, queryBptIn);

const amountsOut = queryAmountsOut.map((a, i) =>
TokenAmount.fromRawAmount(tokensOut[i], a),
);

const tokenOutIndex =
input.kind === ExitKind.SINGLE_ASSET
? poolTokens.findIndex(
(t) => t.address === input.tokenOut.toLowerCase(),
)
: undefined;

return {
exitKind: input.kind,
id: poolState.id,
bptIn,
amountsOut,
tokenOutIndex,
};
}

public buildCall(input: ExitCallInput): BuildOutput {
let minAmountsOut: bigint[];
let maxBptIn: bigint;
let userData: Address;

switch (input.exitKind) {
case ExitKind.UNBALANCED:
minAmountsOut = input.amountsOut.map((a) => a.amount);
maxBptIn = input.slippage.applyTo(input.bptIn.amount);
userData = WeightedEncoder.exitExactOut(
minAmountsOut,
maxBptIn,
);
break;
case ExitKind.SINGLE_ASSET:
if (input.tokenOutIndex === undefined) {
throw new Error(
'tokenOutIndex must be defined for SINGLE_ASSET exit',
);
}
minAmountsOut = input.amountsOut.map((a) =>
input.slippage.removeFrom(a.amount),
);
maxBptIn = input.bptIn.amount;
userData = WeightedEncoder.exitExactInSingleAsset(
maxBptIn,
input.tokenOutIndex,
);
break;
case ExitKind.PROPORTIONAL:
minAmountsOut = input.amountsOut.map((a) =>
input.slippage.removeFrom(a.amount),
);
maxBptIn = input.bptIn.amount;
userData = WeightedEncoder.exitExactInProportional(
input.bptIn.amount,
);
break;
}

const queryArgs = getExitParameters({
poolId: input.id,
assets: input.amountsOut.map((a) => a.token.address),
sender: input.sender,
recipient: input.recipient,
minAmountsOut,
userData,
toInternalBalance: false, // TODO - Should we make this part of input?
});

const call = encodeFunctionData({
abi: vaultAbi,
functionName: 'exitPool',
args: queryArgs,
});

// Encode data
return {
call,
to: BALANCER_VAULT,
value: 0n,
maxBptIn,
minAmountsOut,
};
}
}
4 changes: 3 additions & 1 deletion src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from './encoders';
export * from './join/';
export * from './join';
export * from './exit';
export * from './path';
export * from './swap';
export * from './slippage';
export * from './token';
export * from './tokenAmount';
export * from './pools/';
export * from './common';
12 changes: 1 addition & 11 deletions src/entities/join/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TokenAmount } from '../tokenAmount';
import { Slippage } from '../slippage';
import { Address } from '../../types';
import { PoolState } from '../common';

export enum JoinKind {
Init = 'Init',
Expand All @@ -9,17 +10,6 @@ export enum JoinKind {
ExactOutProportional = 'ExactOutProportional',
}

// Returned from API and used as input
export type PoolState = {
id: Address;
address: Address;
type: string;
tokens: {
address: Address;
decimals: number;
}[]; // already properly sorted in case different versions sort them differently
};

// This will be extended for each pools specific input requirements
export type BaseJoinInput = {
chainId: number;
Expand Down
Loading

0 comments on commit 294c082

Please sign in to comment.