-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Initial Exit functionality and Weighted Pool implementation.
- Loading branch information
1 parent
3cafa27
commit 294c082
Showing
12 changed files
with
703 additions
and
23 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './types'; | ||
export * from './replaceWrapped'; |
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,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; | ||
} | ||
}); | ||
} |
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,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 | ||
}; |
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,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; | ||
}; |
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,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; | ||
} |
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,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, | ||
}; | ||
} | ||
} |
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,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'; |
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
Oops, something went wrong.