From c95d21ea52439367d91ba395cc037fa0535a6ad6 Mon Sep 17 00:00:00 2001 From: Bruno Eidam Guerios Date: Fri, 8 Sep 2023 16:04:25 -0300 Subject: [PATCH] Refactor join types to match contract interface --- src/entities/encoders/weighted.ts | 2 + src/entities/join/index.ts | 35 +++--- src/entities/join/weighted/helpers.ts | 37 +----- src/entities/join/weighted/weightedJoin.ts | 49 ++++---- test/weightedJoin.integration.test.ts | 128 ++++++++++----------- 5 files changed, 110 insertions(+), 141 deletions(-) diff --git a/src/entities/encoders/weighted.ts b/src/entities/encoders/weighted.ts index ccaa2227..bc1dec28 100644 --- a/src/entities/encoders/weighted.ts +++ b/src/entities/encoders/weighted.ts @@ -53,6 +53,7 @@ export class WeightedEncoder { * @param bptAmountOut - the amount of BPT to be minted * @param enterTokenIndex - the index of the token to be provided as liquidity */ + // TODO: refactor this into 2 separate functions static joinGivenOut = ( bptAmountOut: bigint, enterTokenIndex?: number, @@ -86,6 +87,7 @@ export class WeightedEncoder { * @param bptAmountIn - the amount of BPT to be burned * @param enterTokenIndex - the index of the token to removed from the pool */ + // TODO: refactor this into 2 separate functions static exitGivenIn = ( bptAmountIn: bigint, exitTokenIndex?: number, diff --git a/src/entities/join/index.ts b/src/entities/join/index.ts index 00c97dec..000f6637 100644 --- a/src/entities/join/index.ts +++ b/src/entities/join/index.ts @@ -5,10 +5,9 @@ import { Token } from '../token'; export enum JoinKind { Init = 'Init', - Proportional = 'Proportional', - Unbalanced = 'Unbalanced', - SingleAsset = 'SingleAsset', - ExactOut = 'ExactOut', + ExactIn = 'ExactIn', + ExactOutSingleAsset = 'ExactOutSingleAsset', + ExactOutProportional = 'ExactOutProportional', } // Returned from API and used as input @@ -32,32 +31,27 @@ export type InitJoinInput = BaseJoinInput & { kind: JoinKind.Init; }; -export type ProportionalJoinInput = BaseJoinInput & { - refAmountIn: TokenAmount; - kind: JoinKind.Proportional; -}; - -export type UnbalancedJoinInput = BaseJoinInput & { +export type ExactInJoinInput = BaseJoinInput & { amountsIn: TokenAmount[]; - kind: JoinKind.Unbalanced; + kind: JoinKind.ExactIn; }; -export type SingleAssetJoinInput = BaseJoinInput & { - amountIn: TokenAmount; - kind: JoinKind.SingleAsset; +export type ExactOutSingleAssetJoinInput = BaseJoinInput & { + bptOut: TokenAmount; + tokenIn: Address; + kind: JoinKind.ExactOutSingleAsset; }; -export type ExactOutJoinInput = BaseJoinInput & { +export type ExactOutProportionalJoinInput = BaseJoinInput & { bptOut: TokenAmount; - kind: JoinKind.ExactOut; + kind: JoinKind.ExactOutProportional; }; export type JoinInput = | InitJoinInput - | ProportionalJoinInput - | UnbalancedJoinInput - | SingleAssetJoinInput - | ExactOutJoinInput; + | ExactInJoinInput + | ExactOutSingleAssetJoinInput + | ExactOutProportionalJoinInput; // Returned from a join query export type JoinQueryResult = { @@ -65,6 +59,7 @@ export type JoinQueryResult = { joinKind: JoinKind; bptOut: TokenAmount; amountsIn: TokenAmount[]; + tokenInIndex?: number; }; export type JoinCallInput = JoinQueryResult & { diff --git a/src/entities/join/weighted/helpers.ts b/src/entities/join/weighted/helpers.ts index 24ae1529..cc455852 100644 --- a/src/entities/join/weighted/helpers.ts +++ b/src/entities/join/weighted/helpers.ts @@ -1,8 +1,7 @@ import { Address } from '../../../types'; -import { ZERO_ADDRESS } from '../../../utils'; import { WeightedEncoder } from '../../encoders'; import { TokenAmount } from '../../tokenAmount'; -import { JoinInput, JoinKind, PoolState } from '..'; +import { JoinInput, JoinKind } from '..'; import { Token } from '../../token'; export function getJoinParameters({ @@ -30,21 +29,6 @@ export function getJoinParameters({ return [poolId, sender, recipient, joinPoolRequest] as const; } -export function checkInputs(input: JoinInput, poolState: PoolState) { - const poolAssets = poolState.tokens.map((t) => t.address); - switch (input.kind) { - case JoinKind.Proportional: { - if (!poolAssets.includes(input.refAmountIn.token.address)) { - throw new Error('Reference token not in pool'); - } - } - break; - // TODO: think about a way to consolidate checks so this doesn't become uneccessarily hard to maintain - default: - break; - } -} - // TODO: amounts for native asset join relies on the fact that the user provided wrapped address for input tokens - is that a fair assumption? export function getAmountsIn(input: JoinInput, poolTokens: Token[]): bigint[] { return poolTokens.map((token) => { @@ -55,18 +39,9 @@ export function getAmountsIn(input: JoinInput, poolTokens: Token[]): bigint[] { t.token.isEqual(token), ); break; - case JoinKind.Proportional: - if (input.refAmountIn.token.isEqual(token)) - tokenIn = input.refAmountIn; - // TODO: calculate proportional amounts based on reference token - break; - case JoinKind.Unbalanced: + case JoinKind.ExactIn: tokenIn = input.amountsIn.find((t) => t.token.isEqual(token)); break; - case JoinKind.SingleAsset: - if (input.amountIn.token.isEqual(token)) - tokenIn = input.amountIn; - break; } return tokenIn?.amount ?? 0n; }); @@ -79,13 +54,13 @@ export function getUserData(input: JoinInput, amountsIn: bigint[]): Address { userData = WeightedEncoder.joinInit(amountsIn); break; } - case JoinKind.Proportional: - case JoinKind.Unbalanced: - case JoinKind.SingleAsset: { + case JoinKind.ExactIn: { userData = WeightedEncoder.joinGivenIn(amountsIn, 0n); break; } - case JoinKind.ExactOut: { + case JoinKind.ExactOutSingleAsset: + // TODO: add tokenInIndex after refactoring into switch/case into separate functions + case JoinKind.ExactOutProportional: { userData = WeightedEncoder.joinGivenOut(input.bptOut.amount); break; } diff --git a/src/entities/join/weighted/weightedJoin.ts b/src/entities/join/weighted/weightedJoin.ts index 0dae70dc..a403de1a 100644 --- a/src/entities/join/weighted/weightedJoin.ts +++ b/src/entities/join/weighted/weightedJoin.ts @@ -9,12 +9,7 @@ import { ZERO_ADDRESS, } from '../../../utils'; import { balancerHelpersAbi, vaultAbi } from '../../../abi'; -import { - checkInputs, - getAmountsIn, - getJoinParameters, - getUserData, -} from './helpers'; +import { getAmountsIn, getJoinParameters, getUserData } from './helpers'; import { BaseJoin, JoinCallInput, @@ -36,22 +31,22 @@ export class WeightedJoin implements BaseJoin { ): Promise { // TODO - This would need extended to work with relayer - checkInputs(input, poolState); + // TODO: check inputs after refactoring switch/case into separate functions const maxAmountsIn = getAmountsIn(input, poolState.tokens); const userData = getUserData(input, maxAmountsIn); + let tokensIn = poolState.tokens; // replace wrapped token with native asset if needed - const tokensIn = poolState.tokens.map((token) => { - if ( - input.joinWithNativeAsset && - token.isUnderlyingEqual(NATIVE_ASSETS[input.chainId]) - ) { - return new Token(input.chainId, ZERO_ADDRESS, 18); - } else { - return token; - } - }); + if (input.joinWithNativeAsset) { + tokensIn = poolState.tokens.map((token) => { + if (token.isUnderlyingEqual(NATIVE_ASSETS[input.chainId])) { + return new Token(input.chainId, ZERO_ADDRESS, 18); + } else { + return token; + } + }); + } const queryArgs = getJoinParameters({ poolId: poolState.id, @@ -83,11 +78,17 @@ export class WeightedJoin implements BaseJoin { TokenAmount.fromRawAmount(tokensIn[i], a), ); + const tokenInIndex = + input.kind === JoinKind.ExactOutSingleAsset + ? poolState.tokens.findIndex((t) => t.address === input.tokenIn) + : undefined; + return { joinKind: input.kind, id: poolState.id, bptOut, amountsIn, + tokenInIndex, }; } @@ -96,6 +97,7 @@ export class WeightedJoin implements BaseJoin { to: Address; value: bigint | undefined; minBptOut: bigint; + // TODO: add maxAmountsIn after creating test scenario for ExactOut joins } { let maxAmountsIn: bigint[]; let userData: Address; @@ -107,19 +109,22 @@ export class WeightedJoin implements BaseJoin { userData = WeightedEncoder.joinInit(maxAmountsIn); break; } - case JoinKind.Proportional: - case JoinKind.Unbalanced: - case JoinKind.SingleAsset: { + case JoinKind.ExactIn: { maxAmountsIn = input.amountsIn.map((a) => a.amount); minBptOut = input.slippage.removeFrom(input.bptOut.amount); userData = WeightedEncoder.joinGivenIn(maxAmountsIn, minBptOut); break; } - case JoinKind.ExactOut: { + case JoinKind.ExactOutSingleAsset: + case JoinKind.ExactOutProportional: { + // TODO: refactor this after splitting encoder into 2 methods maxAmountsIn = input.amountsIn.map((a) => input.slippage.applyTo(a.amount), ); - userData = WeightedEncoder.joinGivenOut(input.bptOut.amount); + userData = WeightedEncoder.joinGivenOut( + input.bptOut.amount, + input.tokenInIndex, + ); break; } default: diff --git a/test/weightedJoin.integration.test.ts b/test/weightedJoin.integration.test.ts index 1747658c..b309a7e6 100644 --- a/test/weightedJoin.integration.test.ts +++ b/test/weightedJoin.integration.test.ts @@ -16,16 +16,16 @@ import { import { BaseJoin, + ExactInJoinInput, JoinKind, PoolState, - SingleAssetJoinInput, Slippage, Token, TokenAmount, } from '../src/entities'; import { JoinParser } from '../src/entities/join/parser'; import { Address } from '../src/types'; -import { CHAINS, ChainId, ZERO_ADDRESS, getPoolAddress } from '../src/utils'; +import { CHAINS, ChainId, getPoolAddress } from '../src/utils'; import { approveToken, sendTransactionGetBalances } from './lib/utils/helper'; @@ -68,7 +68,6 @@ describe('weighted join test', () => { // prepare test client with balance and token approvals await client.impersonateAccount({ address: testAddress }); - await approveToken(client, testAddress, tokenIn.address); // get pool state from api poolFromApi = await api.getPool(poolId); @@ -78,27 +77,29 @@ describe('weighted join test', () => { weightedJoin = joinParser.getJoin(poolFromApi.type); }); - describe('single asset join', async () => { + describe('exact in', async () => { beforeAll(() => { poolId = '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014'; // 80BAL-20WETH + }); + + test('single asset join', async () => { tokenIn = new Token( chainId, '0xba100000625a3754423978a60c9317c58a424e3D', 18, 'BAL', ); - }); - - test('should join', async () => { const amountIn = TokenAmount.fromHumanAmount(tokenIn, '1'); + await approveToken(client, testAddress, tokenIn.address); + // perform join query to get expected bpt out - const joinInput: SingleAssetJoinInput = { - amountIn, + const joinInput: ExactInJoinInput = { + amountsIn: [amountIn], chainId, rpcUrl, - kind: JoinKind.SingleAsset, + kind: JoinKind.ExactIn, }; const queryResult = await weightedJoin.query( joinInput, @@ -142,68 +143,59 @@ describe('weighted join test', () => { }); }); - describe('native asset join', async () => { - beforeAll(() => { - poolId = - '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014'; // 80BAL-20WETH - tokenIn = new Token( - chainId, - '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', - 18, - 'WETH', - ); - }); - - test('should join', async () => { - const amountIn = TokenAmount.fromHumanAmount(tokenIn, '1'); - // perform join query to get expected bpt out - const joinInput: SingleAssetJoinInput = { - amountIn, - chainId, - rpcUrl, - kind: JoinKind.SingleAsset, - joinWithNativeAsset: true, - }; - const queryResult = await weightedJoin.query( - joinInput, - poolFromApi, - ); + test('native asset join', async () => { + tokenIn = new Token( + chainId, + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + 18, + 'WETH', + ); + const amountIn = TokenAmount.fromHumanAmount(tokenIn, '1'); - // build join call with expected minBpOut based on slippage - const slippage = Slippage.fromPercentage('1'); // 1% - const { call, to, value, minBptOut } = weightedJoin.buildCall({ - ...queryResult, - slippage, - sender: testAddress, - recipient: testAddress, - }); + await approveToken(client, testAddress, tokenIn.address); - // send join transaction and check balance changes - const { transactionReceipt, balanceDeltas } = - await sendTransactionGetBalances( - [ - ...queryResult.amountsIn.map((a) => a.token.address), - queryResult.bptOut.token.address, - ], - client, - testAddress, - to, - call, - value, - ); + // perform join query to get expected bpt out + const joinInput: ExactInJoinInput = { + amountsIn: [amountIn], + chainId, + rpcUrl, + kind: JoinKind.ExactIn, + joinWithNativeAsset: true, + }; + const queryResult = await weightedJoin.query(joinInput, poolFromApi); + + // build join call with expected minBpOut based on slippage + const slippage = Slippage.fromPercentage('1'); // 1% + const { call, to, value, minBptOut } = weightedJoin.buildCall({ + ...queryResult, + slippage, + sender: testAddress, + recipient: testAddress, + }); - expect(transactionReceipt.status).to.eq('success'); - expect(queryResult.bptOut.amount > 0n).to.be.true; - const expectedDeltas = [ - ...queryResult.amountsIn.map((a) => a.amount), - queryResult.bptOut.amount, - ]; - expect(expectedDeltas).to.deep.eq(balanceDeltas); - const expectedMinBpt = slippage.removeFrom( - queryResult.bptOut.amount, + // send join transaction and check balance changes + const { transactionReceipt, balanceDeltas } = + await sendTransactionGetBalances( + [ + ...queryResult.amountsIn.map((a) => a.token.address), + queryResult.bptOut.token.address, + ], + client, + testAddress, + to, + call, + value, ); - expect(expectedMinBpt).to.deep.eq(minBptOut); - }); + + expect(transactionReceipt.status).to.eq('success'); + expect(queryResult.bptOut.amount > 0n).to.be.true; + const expectedDeltas = [ + ...queryResult.amountsIn.map((a) => a.amount), + queryResult.bptOut.amount, + ]; + expect(expectedDeltas).to.deep.eq(balanceDeltas); + const expectedMinBpt = slippage.removeFrom(queryResult.bptOut.amount); + expect(expectedMinBpt).to.deep.eq(minBptOut); }); });