Skip to content

Commit

Permalink
Merge pull request #93 from balancer/exit
Browse files Browse the repository at this point in the history
Exit Pool
  • Loading branch information
johngrantuk authored Sep 21, 2023
2 parents b801f74 + 346cb9d commit f8ede7c
Show file tree
Hide file tree
Showing 17 changed files with 979 additions and 297 deletions.
1 change: 0 additions & 1 deletion src/entities/encoders/weighted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export enum WeightedPoolExitKind {
MANAGEMENT_FEE_TOKENS_OUT = 3,
}

// TODO: rename functions after deciding on the naming convention
export class WeightedEncoder {
/**
* Cannot be constructed.
Expand Down
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];
}
}
79 changes: 79 additions & 0 deletions src/entities/exit/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { TokenAmount } from '../tokenAmount';
import { Slippage } from '../slippage';
import { Address } from '../../types';
import { PoolState } from '../types';

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;
toInternalBalance?: 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;
toInternalBalance?: boolean;
};

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;
};
23 changes: 23 additions & 0 deletions src/entities/exit/weighted/validateInputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ExitInput, ExitKind } from '..';
import { PoolState } from '../../types';
import { areTokensInArray } from '../../utils/areTokensInArray';

export function validateInputs(input: ExitInput, poolState: PoolState) {
switch (input.kind) {
case ExitKind.UNBALANCED:
areTokensInArray(
input.amountsOut.map((a) => a.token.address),
poolState.tokens.map((t) => t.address),
);
break;
case ExitKind.SINGLE_ASSET:
areTokensInArray(
[input.tokenOut],
poolState.tokens.map((t) => t.address),
);
case ExitKind.PROPORTIONAL:
areTokensInArray([input.bptIn.token.address], [poolState.address]);
default:
break;
}
}
182 changes: 182 additions & 0 deletions src/entities/exit/weighted/weightedExit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { encodeFunctionData } from 'viem';
import { Token, TokenAmount, WeightedEncoder } from '../../..';
import { Address } from '../../../types';
import { BALANCER_VAULT, MAX_UINT256, ZERO_ADDRESS } from '../../../utils';
import { vaultAbi } from '../../../abi';
import { parseExitArgs } from '../../utils/parseExitArgs';
import {
BaseExit,
BuildOutput,
ExitCallInput,
ExitInput,
ExitKind,
ExitQueryResult,
} from '../types';
import { getSortedTokens } from '../../utils';
import { PoolState, AmountsExit } from '../../types';
import { doQueryExit } from '../../utils/doQueryExit';
import { validateInputs } from './validateInputs';

export class WeightedExit implements BaseExit {
public async query(
input: ExitInput,
poolState: PoolState,
): Promise<ExitQueryResult> {
validateInputs(input, poolState);

const sortedTokens = getSortedTokens(poolState.tokens, input.chainId);

const amounts = this.getAmountsQuery(sortedTokens, input);

const userData = this.encodeUserData(input.kind, amounts);

// tokensOut will have zero address if exit with native asset
const { args, tokensOut } = parseExitArgs({
chainId: input.chainId,
exitWithNativeAsset: !!input.exitWithNativeAsset,
poolId: poolState.id,
sortedTokens,
sender: ZERO_ADDRESS,
recipient: ZERO_ADDRESS,
minAmountsOut: amounts.minAmountsOut,
userData,
toInternalBalance: !!input.toInternalBalance,
});

const queryResult = await doQueryExit(
input.rpcUrl,
input.chainId,
args,
);

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

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

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

private getAmountsQuery(tokens: Token[], input: ExitInput): AmountsExit {
switch (input.kind) {
case ExitKind.UNBALANCED:
return {
minAmountsOut: tokens.map(
(t) =>
input.amountsOut.find((a) => a.token.isEqual(t))
?.amount ?? 0n,
),
tokenOutIndex: undefined,
maxBptAmountIn: MAX_UINT256,
};
case ExitKind.SINGLE_ASSET:
return {
minAmountsOut: Array(tokens.length).fill(0n),
tokenOutIndex: tokens.findIndex((t) =>
t.isSameAddress(input.tokenOut),
),
maxBptAmountIn: input.bptIn.amount,
};
case ExitKind.PROPORTIONAL:
return {
minAmountsOut: Array(tokens.length).fill(0n),
tokenOutIndex: undefined,
maxBptAmountIn: input.bptIn.amount,
};
}
}

public buildCall(input: ExitCallInput): BuildOutput {
const amounts = this.getAmountsCall(input);

const userData = this.encodeUserData(input.exitKind, amounts);

const { args } = parseExitArgs({
poolId: input.id,
sortedTokens: input.amountsOut.map((a) => a.token),
sender: input.sender,
recipient: input.recipient,
minAmountsOut: amounts.minAmountsOut,
userData,
toInternalBalance: !!input.toInternalBalance,
});

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

return {
call,
to: BALANCER_VAULT,
value: 0n,
maxBptIn: amounts.maxBptAmountIn,
minAmountsOut: amounts.minAmountsOut,
};
}

private getAmountsCall(input: ExitCallInput): AmountsExit {
switch (input.exitKind) {
case ExitKind.UNBALANCED:
return {
minAmountsOut: input.amountsOut.map((a) => a.amount),
tokenOutIndex: input.tokenOutIndex,
maxBptAmountIn: input.slippage.applyTo(input.bptIn.amount),
};
case ExitKind.SINGLE_ASSET:
if (input.tokenOutIndex === undefined) {
throw new Error(
'tokenOutIndex must be defined for SINGLE_ASSET exit',
);
}
return {
minAmountsOut: input.amountsOut.map((a) =>
input.slippage.removeFrom(a.amount),
),
tokenOutIndex: input.tokenOutIndex,
maxBptAmountIn: input.bptIn.amount,
};
case ExitKind.PROPORTIONAL:
return {
minAmountsOut: input.amountsOut.map((a) =>
input.slippage.removeFrom(a.amount),
),
tokenOutIndex: input.tokenOutIndex,
maxBptAmountIn: input.bptIn.amount,
};
default:
throw Error('Unsupported Exit Type');
}
}

private encodeUserData(kind: ExitKind, amounts: AmountsExit): Address {
switch (kind) {
case ExitKind.UNBALANCED:
return WeightedEncoder.exitUnbalanced(
amounts.minAmountsOut,
amounts.maxBptAmountIn,
);
case ExitKind.SINGLE_ASSET:
if (amounts.tokenOutIndex === undefined)
throw Error('No Index');

return WeightedEncoder.exitSingleAsset(
amounts.maxBptAmountIn,
amounts.tokenOutIndex,
);
case ExitKind.PROPORTIONAL:
return WeightedEncoder.exitProportional(amounts.maxBptAmountIn);
default:
throw Error('Unsupported Exit Type');
}
}
}
5 changes: 4 additions & 1 deletion src/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
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 './utils';
export * from './types';
21 changes: 4 additions & 17 deletions src/entities/join/weighted/validateInputs.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,24 @@
import { JoinInput, JoinKind } from '..';
import { PoolState } from '../../types';
import { Address } from '../../../types';
import { areTokensInArray } from '../../utils/areTokensInArray';

export function validateInputs(input: JoinInput, poolState: PoolState) {
switch (input.kind) {
case JoinKind.Init:
case JoinKind.Unbalanced:
checkTokenMismatch(
areTokensInArray(
input.amountsIn.map((a) => a.token.address),
poolState.tokens.map((t) => t.address),
);
break;
case JoinKind.SingleAsset:
checkTokenMismatch(
areTokensInArray(
[input.tokenIn],
poolState.tokens.map((t) => t.address),
);
case JoinKind.Proportional:
checkTokenMismatch(
[input.bptOut.token.address],
[poolState.address],
);
areTokensInArray([input.bptOut.token.address], [poolState.address]);
default:
break;
}
}

function checkTokenMismatch(tokensIn: Address[], poolTokens: Address[]) {
const sanitisedTokensIn = tokensIn.map((t) => t.toLowerCase() as Address);
const sanitisedPoolTokens = poolTokens.map((t) => t.toLowerCase());
for (const tokenIn of sanitisedTokensIn) {
if (!sanitisedPoolTokens.includes(tokenIn)) {
throw new Error(`Token ${tokenIn} not found in pool`);
}
}
}
Loading

0 comments on commit f8ede7c

Please sign in to comment.