-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #93 from balancer/exit
Exit Pool
- Loading branch information
Showing
17 changed files
with
979 additions
and
297 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`); | ||
} | ||
} | ||
} |
Oops, something went wrong.