From 8206d1c38e6559e1e17a6f0f5f1696e3f5000b25 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Wed, 5 Apr 2023 19:19:46 -0300 Subject: [PATCH 1/7] Bugfixing Composable Stable and Stable to accept join with native asset; Bugfixing insert function to accept inserting in the last position (push) Adding error throw to replace function; Accepting undefined value for slots on setUpExample function; --- balancer-js/examples/pools/helper.ts | 2 +- balancer-js/src/lib/utils/index.ts | 7 ++-- .../join.concern.integration.spec.ts | 36 +++++++++++++++---- .../concerns/composableStable/join.concern.ts | 3 +- .../concerns/stable/join.concern.ts | 3 +- balancer-js/src/test/lib/joinHelper.ts | 11 +++--- balancer-js/src/test/lib/utils.ts | 4 +-- 7 files changed, 45 insertions(+), 21 deletions(-) diff --git a/balancer-js/examples/pools/helper.ts b/balancer-js/examples/pools/helper.ts index 91fe9f526..9d09b0e20 100644 --- a/balancer-js/examples/pools/helper.ts +++ b/balancer-js/examples/pools/helper.ts @@ -21,7 +21,7 @@ export async function setUpExample( rpcUrlLocal: string, network: Network, tokens: string[], - slots: number[], + slots: number[] | undefined, balances: string[], poolId: string, blockNo: number diff --git a/balancer-js/src/lib/utils/index.ts b/balancer-js/src/lib/utils/index.ts index 9751eba4a..03849f4f8 100644 --- a/balancer-js/src/lib/utils/index.ts +++ b/balancer-js/src/lib/utils/index.ts @@ -18,8 +18,8 @@ export const isSameAddress = (address1: string, address2: string): boolean => getAddress(address1) === getAddress(address2); export function insert(arr: T[], index: number, newItem: T): T[] { - if (index < 0 || index >= arr.length) { - return arr; + if (index < 0 || index > arr.length) { + throw new Error("Index out of bounds. Can't insert item."); } return [ // part of the array before the specified index @@ -38,6 +38,9 @@ export function insert(arr: T[], index: number, newItem: T): T[] { * @param newItem */ export function replace(arr: T[], index: number, newItem: T): T[] { + if (index < 0 || index >= arr.length) { + throw new Error("Index out of bounds. Can't replace item."); + } return [ // part of the array before the specified index ...arr.slice(0, index), diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts index 9b319f9f8..8878cfbbc 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts @@ -3,24 +3,31 @@ import dotenv from 'dotenv'; import { ethers } from 'hardhat'; import { parseFixed } from '@ethersproject/bignumber'; -import { removeItem, PoolWithMethods, Network } from '@/.'; +import { + removeItem, + PoolWithMethods, + Network, + replace, + BALANCER_NETWORK_CONFIG, +} from '@/.'; import { forkSetup, TestPoolHelper } from '@/test/lib/utils'; import { testExactTokensIn, testAttributes, testSortingInputs, } from '@/test/lib/joinHelper'; +import { AddressZero } from '@ethersproject/constants'; dotenv.config(); -const network = Network.MAINNET; -const { ALCHEMY_URL: jsonRpcUrl } = process.env; -const rpcUrl = 'http://127.0.0.1:8545'; +const network = Network.POLYGON; +const { ALCHEMY_URL_POLYGON: jsonRpcUrl } = process.env; +const rpcUrl = 'http://127.0.0.1:8137'; const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network); const signer = provider.getSigner(); -const blockNumber = 16350000; +const blockNumber = 40767042; const testPoolId = - '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d'; + '0x02d2e2d7a89d6c5cb3681cfcb6f7dac02a55eda400000000000000000000088f'; describe('ComposableStable Pool - Join Functions', async () => { let signerAddress: string; @@ -48,7 +55,7 @@ describe('ComposableStable Pool - Join Functions', async () => { await forkSetup( signer, pool.tokensList, - Array(pool.tokensList.length).fill(0), + [0, 3, 0], Array(pool.tokensList.length).fill(parseFixed('100000', 18).toString()), jsonRpcUrl as string, blockNumber // holds the same state as the static repository @@ -72,6 +79,21 @@ describe('ComposableStable Pool - Join Functions', async () => { amountsIn[0] = parseFixed('202', 18).toString(); await testExactTokensIn(pool, signer, signerAddress, tokensIn, amountsIn); }); + + it('should join - native asset', async () => { + const wrappedNativeAssetIndex = pool.tokensList.indexOf( + BALANCER_NETWORK_CONFIG[ + network + ].addresses.tokens.wrappedNativeAsset.toLowerCase() + ); + const tokensIn = removeItem( + replace(pool.tokensList, wrappedNativeAssetIndex, AddressZero), + pool.bptIndex + ); + const amountsIn = Array(tokensIn.length).fill('0'); + amountsIn[wrappedNativeAssetIndex] = parseFixed('202', 18).toString(); + await testExactTokensIn(pool, signer, signerAddress, tokensIn, amountsIn); + }); }); context('Unit Tests', () => { diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.ts index 1de5e5f3d..4b49ca39b 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.ts @@ -23,6 +23,7 @@ import { JoinPoolAttributes, JoinPool, } from '../types'; +import { AddressZero } from '@ethersproject/constants'; interface SortedValues { sortedAmountsIn: string[]; @@ -244,7 +245,7 @@ export class ComposableStablePoolJoin implements JoinConcern { bptIndex, scalingFactorsWithoutBpt, upScaledBalancesWithoutBpt, - } = parsePoolInfo(pool, wrappedNativeAsset); + } = parsePoolInfo(pool, wrappedNativeAsset, tokensIn.includes(AddressZero)); return { sortedAmountsIn, scalingFactorsWithoutBpt, diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.ts index d2bcb715d..fa930b2ad 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.ts @@ -17,6 +17,7 @@ import { JoinPoolAttributes, JoinPoolParameters, } from '../types'; +import { AddressZero } from '@ethersproject/constants'; type SortedValues = { poolTokens: string[]; @@ -119,7 +120,7 @@ export class StablePoolJoin implements JoinConcern { swapFeeEvm, scalingFactors, upScaledBalances, - } = parsePoolInfo(pool, wrappedNativeAsset); + } = parsePoolInfo(pool, wrappedNativeAsset, tokensIn.includes(AddressZero)); const assetHelpers = new AssetHelpers(wrappedNativeAsset); // Sorts amounts in into ascending order (referenced to token addresses) to match the format expected by the Vault. diff --git a/balancer-js/src/test/lib/joinHelper.ts b/balancer-js/src/test/lib/joinHelper.ts index 19cfc4ddf..f9f98391b 100644 --- a/balancer-js/src/test/lib/joinHelper.ts +++ b/balancer-js/src/test/lib/joinHelper.ts @@ -16,12 +16,8 @@ export const testExactTokensIn = async ( ): Promise => { const slippage = '6'; // 6 bps = 0.06% - const { to, data, minBPTOut, expectedBPTOut, priceImpact } = pool.buildJoin( - signerAddress, - tokensIn, - amountsIn, - slippage - ); + const { to, data, minBPTOut, expectedBPTOut, priceImpact, value } = + pool.buildJoin(signerAddress, tokensIn, amountsIn, slippage); const { transactionReceipt, balanceDeltas } = await sendTransactionGetBalances( @@ -29,7 +25,8 @@ export const testExactTokensIn = async ( signer, signerAddress, to, - data + data, + value ); expect(transactionReceipt.status).to.eq(1); diff --git a/balancer-js/src/test/lib/utils.ts b/balancer-js/src/test/lib/utils.ts index 1c17df083..2b655d515 100644 --- a/balancer-js/src/test/lib/utils.ts +++ b/balancer-js/src/test/lib/utils.ts @@ -75,6 +75,7 @@ export const forkSetup = async ( slots = await Promise.all( tokens.map(async (token) => findTokenBalanceSlot(signer, token)) ); + console.log('slots: ' + slots); } for (let i = 0; i < tokens.length; i++) { // Set initial account balance for each token that will be used to join pool @@ -374,7 +375,6 @@ export async function sendTransactionGetBalances( signer, signerAddress ); - const balanceDeltas = balancesAfter.map((balAfter, i) => { // ignore ETH delta from gas cost if (tokensForBalanceCheck[i] === AddressZero) { @@ -431,5 +431,5 @@ export async function findTokenBalanceSlot( ]); if (balance.eq(BigNumber.from(probe))) return i; } - throw 'Balances slot not found!'; + throw new Error('Balance slot not found!'); } From 7547c7adcef73453d8e16476a77a0fb9f89f2c0e Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Wed, 5 Apr 2023 19:35:51 -0300 Subject: [PATCH 2/7] returning original array in helper functions if index is out of bonds; --- balancer-js/src/lib/utils/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/balancer-js/src/lib/utils/index.ts b/balancer-js/src/lib/utils/index.ts index 03849f4f8..3c197170e 100644 --- a/balancer-js/src/lib/utils/index.ts +++ b/balancer-js/src/lib/utils/index.ts @@ -19,7 +19,7 @@ export const isSameAddress = (address1: string, address2: string): boolean => export function insert(arr: T[], index: number, newItem: T): T[] { if (index < 0 || index > arr.length) { - throw new Error("Index out of bounds. Can't insert item."); + return arr; } return [ // part of the array before the specified index @@ -39,7 +39,7 @@ export function insert(arr: T[], index: number, newItem: T): T[] { */ export function replace(arr: T[], index: number, newItem: T): T[] { if (index < 0 || index >= arr.length) { - throw new Error("Index out of bounds. Can't replace item."); + return arr; } return [ // part of the array before the specified index From b5048e05326e9ee5309484ac8f45ee13b3977cd0 Mon Sep 17 00:00:00 2001 From: bronco Date: Mon, 3 Apr 2023 17:20:46 +0200 Subject: [PATCH 3/7] new: migrations --- balancer-js/examples/helpers/print-logs.ts | 25 +- .../liquidity-managment/migrations.ts | 40 +++ balancer-js/src/index.ts | 1 + .../migrations.integrations.spec.ts | 303 ++++++++++++++++++ .../modules/liquidity-managment/migrations.ts | 176 ++++++++++ .../migrations/builder.spec-helpers.ts | 111 +++++++ .../migrations/builder.spec.ts | 49 +++ .../liquidity-managment/migrations/builder.ts | 166 ++++++++++ .../liquidity-managment/migrations/helpers.ts | 174 ++++++++++ .../src/modules/relayer/actions.spec.ts | 101 ++++++ balancer-js/src/modules/relayer/actions.ts | 242 ++++++++++++++ balancer-js/src/modules/relayer/types.ts | 7 + balancer-js/src/modules/sdk.module.ts | 11 +- balancer-js/src/test/factories/data.ts | 14 +- .../src/test/factories/named-tokens.ts | 42 ++- balancer-js/src/test/lib/utils.ts | 16 +- 16 files changed, 1464 insertions(+), 14 deletions(-) create mode 100644 balancer-js/examples/liquidity-managment/migrations.ts create mode 100644 balancer-js/src/modules/liquidity-managment/migrations.integrations.spec.ts create mode 100644 balancer-js/src/modules/liquidity-managment/migrations.ts create mode 100644 balancer-js/src/modules/liquidity-managment/migrations/builder.spec-helpers.ts create mode 100644 balancer-js/src/modules/liquidity-managment/migrations/builder.spec.ts create mode 100644 balancer-js/src/modules/liquidity-managment/migrations/builder.ts create mode 100644 balancer-js/src/modules/liquidity-managment/migrations/helpers.ts create mode 100644 balancer-js/src/modules/relayer/actions.spec.ts create mode 100644 balancer-js/src/modules/relayer/actions.ts diff --git a/balancer-js/examples/helpers/print-logs.ts b/balancer-js/examples/helpers/print-logs.ts index 3d47f35e2..1bdc636e0 100644 --- a/balancer-js/examples/helpers/print-logs.ts +++ b/balancer-js/examples/helpers/print-logs.ts @@ -28,8 +28,8 @@ const decodeLog = async (log: any, abi: any) => { }; export const decodeLogs = async (logs: any[]) => { - const decodedLogs = []; - let abi; + const decodedLogs: any[] = []; + let abi: any; for (const log of logs) { abi = abis.get(log.address); @@ -69,21 +69,30 @@ export const printLogs = async (logs: any[]) => { }); }; + const printInternalBalanceChanged = (log: any) => { + const { user, token, delta } = log.args + console.log('\x1b[32m%s\x1b[0m', 'User: ', user) + console.log('\x1b[32m%s\x1b[0m', 'Token:', token) + console.log('\x1b[32m%s\x1b[0m', 'Delta:', formatEther(delta)) + } + const printTransfer = (log: any) => { console.log(log.address); - const { from, to, value, src, dst, wad } = log.args; - console.log('\x1b[32m%s\x1b[0m', 'From: ', from || src); - console.log('\x1b[32m%s\x1b[0m', 'To: ', to || dst); - console.log('\x1b[32m%s\x1b[0m', 'Value:', formatEther(value || wad)); - }; + const { from, to, value, src, dst, wad, _to, _from, _value } = log.args + console.log('\x1b[32m%s\x1b[0m', 'From: ', from || _from || src) + console.log('\x1b[32m%s\x1b[0m', 'To: ', to || _to || dst) + console.log('\x1b[32m%s\x1b[0m', 'Value:', formatEther(value || _value || wad)) + } - decodedLogs.map((log) => { + decodedLogs.map((log: any) => { console.log('-'.repeat(80)); console.log(log.name); if (log.name === 'Swap') { printSwap(log); } else if (log.name === 'PoolBalanceChanged') { printPoolBalanceChanged(log); + } else if (log.name === 'InternalBalanceChanged') { + printInternalBalanceChanged(log); } else if (log.name === 'Transfer') { printTransfer(log); } diff --git a/balancer-js/examples/liquidity-managment/migrations.ts b/balancer-js/examples/liquidity-managment/migrations.ts new file mode 100644 index 000000000..f00b58cf4 --- /dev/null +++ b/balancer-js/examples/liquidity-managment/migrations.ts @@ -0,0 +1,40 @@ +/** + * Migrations module contains methods to migrate liquidity between pools + * Run command: yarn examples:run ./examples/liquidity-managment/migrations.ts + */ +import { BalancerSDK } from '@/.' + +const sdk = new BalancerSDK({ + network: 1, + rpcUrl: 'http://127.0.0.1:8545', // Using a forked mainnet to be able to approve the relayer +}) + +const { provider, migrationService } = sdk + +if (!migrationService) { + throw new Error('No migrationService present') +} + +const main = async () => { + const user = '0x783596B9504Ef2752EFB2d4Aed248fDCb0d9FDab' + const from = '0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080' + const to = '0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080' + const balance = await sdk.contracts.ERC20('0x32296969ef14eb0c6d29669c550d4a0449130230', provider).balanceOf(user) + + // To be able to perform a migration and a static call, user needs to approve the relayer first + await provider.send('hardhat_impersonateAccount', [user]) + const signer = provider.getSigner(user) + await sdk.contracts.vault.connect(signer).setRelayerApproval(user, migrationService.relayerAddress, true) + + // Query for the minimum amount of BPT to receive + const peek = await migrationService.pool2pool(user, from, to, balance) + const peekResult = await provider.call({ ...peek, from: user, gasLimit: 8e6 }); + const expectedBptOut = migrationService.getMinBptOut(peekResult); + console.log('expectedBptOut', expectedBptOut.toString(), 'BPT') + + // Build the migration with the minimum amount of BPT to receive + const txParams = await migrationService.pool2pool(user, from, to, balance, expectedBptOut) + console.log(txParams.data) +} + +main() diff --git a/balancer-js/src/index.ts b/balancer-js/src/index.ts index d201e7a9c..df7942a6d 100644 --- a/balancer-js/src/index.ts +++ b/balancer-js/src/index.ts @@ -38,3 +38,4 @@ export { } from '@balancer-labs/sor'; export { SimulationType } from './modules/simulation/simulation.module'; export { BALANCER_NETWORK_CONFIG } from './lib/constants/config'; +export { Migrations } from './modules/liquidity-managment/migrations'; diff --git a/balancer-js/src/modules/liquidity-managment/migrations.integrations.spec.ts b/balancer-js/src/modules/liquidity-managment/migrations.integrations.spec.ts new file mode 100644 index 000000000..63e34d4b0 --- /dev/null +++ b/balancer-js/src/modules/liquidity-managment/migrations.integrations.spec.ts @@ -0,0 +1,303 @@ +import { impersonateAccount, reset } from '@/test/lib/utils'; +import { expect } from 'chai'; +import { Vault__factory } from '@/contracts'; +import { BALANCER_NETWORK_CONFIG } from '@/lib/constants/config'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; +import { ERC20 } from '@/modules/contracts/implementations/ERC20'; +import { + vitaDao1, + vitaDao2, + metaStable, + composableStable, + poolRepository, + gaugesRepository, + polygonComposableStable, + polygonPoolRepository, +} from './migrations/builder.spec-helpers'; +import { Migrations } from './migrations'; + +describe('Migrations', () => { + context('mainnet', () => { + const { + addresses: { contracts }, + } = BALANCER_NETWORK_CONFIG[1]; + const relayerAddress = contracts.relayerV5 as string; + const provider = new JsonRpcProvider('http://127.0.0.1:8545'); + const vault = Vault__factory.connect(contracts.vault, provider); + let signer: JsonRpcSigner; + let address: string; + + const migrations = new Migrations( + relayerAddress, + poolRepository, + gaugesRepository, + provider + ); + + beforeEach(async () => { + await reset('https://rpc.ankr.com/eth', provider, 16950000); + signer = await impersonateAccount(address, provider); + + // approve relayer + await vault + .connect(signer) + .setRelayerApproval(address, relayerAddress, true); + }); + + context('Metastable to Metastable', () => { + const from = metaStable; + const to = from; + + describe('bptHodler', () => { + before(() => { + address = '0x21ac89788d52070D23B8EaCEcBD3Dc544178DC60'; + }); + + it('joins a new pool with an limit', async () => { + const balance = await ERC20(from.address, signer).balanceOf(address); + const peek = await migrations.pool2pool( + address, + from.id, + to.id, + balance + ); + const peekResult = await signer.call({ ...peek, gasLimit: 8e6 }); + const expectedBptOut = Migrations.getMinBptOut(peekResult); + + const txParams = await migrations.pool2pool( + address, + from.id, + to.id, + balance, + expectedBptOut + ); + + await (await signer.sendTransaction(txParams)).wait(); + + const balanceAfter = await ERC20(to.address, signer).balanceOf( + address + ); + + expect(String(balanceAfter)).to.be.eq(expectedBptOut); + }); + }); + + describe('staked bpt', () => { + before(() => { + address = '0xe8343fd029561289CF7359175EE84DA121817C71'; + }); + + it('should build a migration using exit / join and stake tokens in the gauge', async () => { + const gauge = (await gaugesRepository.findBy('poolId', from.id)) as { + id: string; + }; + const balance = await ERC20(gauge.id, signer).balanceOf(address); + + const peek = await migrations.pool2poolWithGauges( + address, + from.id, + to.id, + balance + ); + const peekResult = await signer.call({ ...peek, gasLimit: 8e6 }); + const expectedBptOut = Migrations.getMinBptOut(peekResult); + + const txParams = await migrations.pool2poolWithGauges( + address, + from.id, + to.id, + balance, + expectedBptOut + ); + + await (await signer.sendTransaction(txParams)).wait(); + + const balanceAfter = await ERC20(gauge.id, signer).balanceOf(address); + + expect(String(balanceAfter)).to.be.eq(expectedBptOut); + }); + }); + }); + + context('ComposableStable to ComposableStable', () => { + before(() => { + address = '0x74C3646ADad7e196102D1fE35267aDFD401A568b'; + }); + + it('should build a migration using exit / join', async () => { + const pool = composableStable; + const balance = await ERC20(pool.address, signer).balanceOf(address); + + const peek = await migrations.pool2pool( + address, + pool.id, + pool.id, + balance + ); + const peekResult = await signer.call({ ...peek, gasLimit: 8e6 }); + const expectedBptOut = Migrations.getMinBptOut(peekResult); + + // NOTICE: When swapping from Linear Pools, the swap will query for the current wrapped token rate. + // It is possible that the rate changes between the static call checking for the BPT out + // and the actual swap, causing it to fail with BAL#208. + // To avoid this, we can add a small buffer to the min BPT out amount. eg. 0.0000001% of the BPT amount. + const buffer = BigInt(expectedBptOut) / BigInt(1e9); + const minBptOut = String(BigInt(expectedBptOut) - buffer); + + const txParams = await migrations.pool2pool( + address, + pool.id, + pool.id, + balance, + minBptOut + ); + + await (await signer.sendTransaction(txParams)).wait(); + + const balanceAfter = await ERC20(pool.address, signer).balanceOf( + address + ); + + // NOTICE: We don't know the exact amount of BPT that will be minted, + // because swaps from the linear pool are not deterministic due to external rates + expect(BigInt(balanceAfter)).to.satisfy( + (v: bigint) => v > v - v / buffer && v < v + v / buffer + ); + }); + }); + + context('Weighted to Weighted between different pools', () => { + before(() => { + address = '0x673CA7d2faEB3c02c4cDB9383344ae5c9738945e'; + }); + + it('should build a migration using exit / join', async () => { + const from = vitaDao1; + const to = vitaDao2; + const balance = await ERC20(from.address, signer).balanceOf(address); + const peek = await migrations.pool2pool( + address, + from.id, + to.id, + balance + ); + const peekResult = await signer.call({ ...peek, gasLimit: 8e6 }); + const expectedBptOut = Migrations.getMinBptOut(peekResult); + + const txParams = await migrations.pool2pool( + address, + from.id, + to.id, + balance, + expectedBptOut + ); + + await (await signer.sendTransaction(txParams)).wait(); + + const balanceAfter = await ERC20(to.address, signer).balanceOf(address); + + expect(String(balanceAfter)).to.be.eq(expectedBptOut); + }); + }); + + context('gauge to gauge', () => { + before(() => { + address = '0xaF297deC752c909092A117A932A8cA4AaaFF9795'; + }); + + it('should build a migration using exit / join and stake tokens in the gauge', async () => { + const from = '0xa6468eca7633246dcb24e5599681767d27d1f978'; + const to = '0x57ab3b673878c3feab7f8ff434c40ab004408c4c'; + const balance = await ERC20(from, provider).balanceOf(address); + + const txParams = await migrations.gauge2gauge( + address, + from, + to, + balance + ); + + await (await signer.sendTransaction(txParams)).wait(); + + const balanceAfter = await ERC20(to, provider).balanceOf(address); + + expect(balanceAfter).to.be.eql(balance); + }); + }); + }); + + context('polygon', () => { + const { + addresses: { contracts }, + } = BALANCER_NETWORK_CONFIG[137]; + const relayerAddress = contracts.relayerV5 as string; + const provider = new JsonRpcProvider('http://127.0.0.1:8137'); + const vault = Vault__factory.connect(contracts.vault, provider); + let signer: JsonRpcSigner; + let address: string; + + const migrations = new Migrations( + relayerAddress, + polygonPoolRepository, + gaugesRepository, + provider + ); + + beforeEach(async () => { + await reset('https://rpc.ankr.com/polygon', provider, 41098000); + signer = await impersonateAccount(address, provider); + + // approve relayer + await vault + .connect(signer) + .setRelayerApproval(address, relayerAddress, true); + }); + + context('ComposableStable to ComposableStable', () => { + before(() => { + address = '0x92a0b2c089733bef43ac367d2ce7783526aea590'; + }); + + it('should build a migration using exit / join', async () => { + const pool = polygonComposableStable; + const balance = await ERC20(pool.address, signer).balanceOf(address); + + const peek = await migrations.pool2pool( + address, + pool.id, + pool.id, + balance + ); + const peekResult = await signer.call({ ...peek, gasLimit: 8e6 }); + const expectedBptOut = Migrations.getMinBptOut(peekResult); + + // NOTICE: When swapping from Linear Pools, the swap will query for the current wrapped token rate. + // It is possible that the rate changes between the static call checking for the BPT out + // and the actual swap, causing it to fail with BAL#208. + // To avoid this, we can add a small buffer to the min BPT out amount. eg. 0.0000001% of the BPT amount. + const buffer = BigInt(expectedBptOut) / BigInt(1e14); // 0.0000001% + const minBptOut = String(BigInt(expectedBptOut) - buffer); + + const txParams = await migrations.pool2pool( + address, + pool.id, + pool.id, + balance, + minBptOut + ); + + await (await signer.sendTransaction(txParams)).wait(); + + const balanceAfter = await ERC20(pool.address, signer).balanceOf( + address + ); + + // NOTICE: We don't know the exact amount of BPT that will be minted, + // because swaps from the linear pool are not deterministic due to external rates + expect(BigInt(balanceAfter)).to.satisfy( + (v: bigint) => v > v - buffer && v < v + buffer + ); + }); + }); + }); +}); diff --git a/balancer-js/src/modules/liquidity-managment/migrations.ts b/balancer-js/src/modules/liquidity-managment/migrations.ts new file mode 100644 index 000000000..989654b34 --- /dev/null +++ b/balancer-js/src/modules/liquidity-managment/migrations.ts @@ -0,0 +1,176 @@ +import { Findable, Pool, PoolAttribute } from '@/types'; +import { JsonRpcProvider } from '@ethersproject/providers'; +import { SubgraphLiquidityGauge } from '../subgraph/subgraph'; +import { migrationBuilder } from './migrations/builder'; +import { + balancerRelayerInterface, + buildMigrationPool, + getMinBptOut, +} from './migrations/helpers'; +import * as actions from '@/modules/relayer/actions'; + +/** + * Class responsible for building liquidity migration transactions. + */ +export class Migrations { + /** + * Instance of a class responsible for building liquidity migration transactions. + * + * @param relayerAddress Address of the relayer contract. + * @param poolsRepository Repository of pools. + * @param liquidityGaugesRepository Repository of liquidity gauges. + * @param provider Provider to use for RPC data fetching. + * + * Available methods: + * - `pool2pool` - Migrates liquidity from one pool to another. + * - `pool2poolWithGauges` - Migrates liquidity from a pool's gauge to another gauge. + * - `gauge2gauge` - Migrates liquidity from one gauge to another of the same pool. + * + * @example + * ```typescript + * const sdk = new BalancerSDK({ + * network: 1, + * rpcUrl: 'https://rpc.ankr.com/eth', + * }) + * + * const migrations = new Migrations( + * sdk.networkConfig.addresses.contracts.relayerV4 as string, + * sdk.data.pools, + * sdk.data.liquidityGauges.subgraph, + * sdk.provider + * ) + * + * const user = '0xfacec29Ae158B26e234B1a81Db2431F6Bd8F8cE8' + * const from = '0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080' + * const to = '0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080' + * const balance = '1000000000000000000' + * const { to, data } = await migrations.pool2pool(user, from, to, balance) + * + * const tx = await sdk.provider.sendTransaction({ to, data }) + * ``` + */ + constructor( + public relayerAddress: string, + public poolsRepository: Findable, + public gaugesRepository: Findable, + public provider: JsonRpcProvider + ) {} + + /** + * Takes user, from and to pool IDs as strings and returns the transaction data + * + * @param user - user address + * @param from - pool ID + * @param to - pool ID + * @param balance - amount of liquidity to migrate in WAL (wei-ether) + * @param minBptOut - minimum amount of BPT to receive, when 0 it will include a peek for the amount + * @returns transaction data + */ + async pool2pool( + user: string, + from: string, + to: string, + balance: string, + minBptOut = '0' + ): Promise<{ to: string; data: string }> { + const fromPool = await buildMigrationPool(from, this.poolsRepository); + const toPool = await buildMigrationPool(to, this.poolsRepository); + + const data = migrationBuilder( + user, + this.relayerAddress, + String(balance), + minBptOut, + fromPool, + toPool, + minBptOut == '0' // if minBptOut is 0, we peek for the join amount + ); + + return { + to: this.relayerAddress, + data, + }; + } + + /** + * Takes user, from and to pool IDs as strings and returns the transaction data + * for a migration including unstaking and restaking + * + * @param user - user address + * @param from - pool ID + * @param to - pool ID + * @param balance - amount of liquidity to migrate in WAL (wei-ether) + * @returns transaction data + */ + async pool2poolWithGauges( + user: string, + from: string, + to: string, + balance: string, + minBptOut = '0' + ): Promise<{ to: string; data: string }> { + const fromGauge = await this.gaugesRepository.findBy('poolId', from); + const toGauge = await this.gaugesRepository.findBy('poolId', to); + if (!fromGauge || !fromGauge.poolId || !toGauge || !toGauge.poolId) { + throw new Error('Gauge not found'); + } + const fromPool = await buildMigrationPool( + fromGauge.poolId, + this.poolsRepository + ); + const toPool = await buildMigrationPool( + toGauge.poolId, + this.poolsRepository + ); + + const data = migrationBuilder( + user, + this.relayerAddress, + String(balance), + minBptOut, + fromPool, + toPool, + minBptOut == '0', // if minBptOut is 0, we peek for the join amount + fromGauge.id, + toGauge.id + ); + + return { + to: this.relayerAddress, + data, + }; + } + + /** + * Migrates staked liquidity for the same pool from one gauge to another. + * + * @param user - user address + * @param from - gauge address + * @param to - gauge address + * @param balance - amount of liquidity to migrate in WAL (wei-ether) + * @returns transaction data + */ + async gauge2gauge( + user: string, + from: string, + to: string, + balance: string + ): Promise<{ to: string; data: string }> { + const steps = [ + actions.gaugeWithdrawal(from, user, this.relayerAddress, balance), + actions.gaugeDeposit(to, this.relayerAddress, user, balance), + ]; + + const data = balancerRelayerInterface.encodeFunctionData('multicall', [ + steps, + ]); + + return { + to: this.relayerAddress, + data, + }; + } + + static getMinBptOut = getMinBptOut; + getMinBptOut = getMinBptOut; +} diff --git a/balancer-js/src/modules/liquidity-managment/migrations/builder.spec-helpers.ts b/balancer-js/src/modules/liquidity-managment/migrations/builder.spec-helpers.ts new file mode 100644 index 000000000..2fae20e23 --- /dev/null +++ b/balancer-js/src/modules/liquidity-managment/migrations/builder.spec-helpers.ts @@ -0,0 +1,111 @@ +import { SubgraphLiquidityGauge } from '@/modules/subgraph/subgraph'; +import { factories } from '@/test/factories'; +import { Pool } from '@/types'; +import pools from '@/test/fixtures/pools-mainnet.json'; +import polygon from '@/test/fixtures/pools-polygon.json'; + +const metaStable = { + ...pools.data.pools.find( + (p) => p.address === '0x32296969ef14eb0c6d29669c550d4a0449130230' + ), +} as Pool; +const bDaiPool = { + ...pools.data.pools.find( + (p) => p.address === '0xae37d54ae477268b9997d4161b96b8200755935c' + ), +} as Pool; +const bUsdcPool = { + ...pools.data.pools.find( + (p) => p.address === '0x82698aecc9e28e9bb27608bd52cf57f704bd1b83' + ), +} as Pool; +const bUsdtPool = { + ...pools.data.pools.find( + (p) => p.address === '0x2f4eb100552ef93840d5adc30560e5513dfffacb' + ), +} as Pool; +const composableStable = { + ...pools.data.pools.find( + (p) => p.address === '0xa13a9247ea42d743238089903570127dda72fe44' + ), +} as Pool; +const vitaDao1 = { + ...pools.data.pools.find( + (p) => p.address === '0xbaeec99c90e3420ec6c1e7a769d2a856d2898e4d' + ), +} as Pool; +const vitaDao2 = { + ...pools.data.pools.find( + (p) => p.address === '0x350196326aeaa9b98f1903fb5e8fc2686f85318c' + ), +} as Pool; +export { vitaDao1, vitaDao2, metaStable, composableStable, bDaiPool }; + +const poolsMap = new Map([ + [metaStable.id, metaStable as Pool], + [composableStable.id, composableStable as Pool], + [bDaiPool.id, bDaiPool as Pool], + [bUsdcPool.id, bUsdcPool as Pool], + [bUsdtPool.id, bUsdtPool as Pool], + [vitaDao1.id, vitaDao1 as Pool], + [vitaDao2.id, vitaDao2 as Pool], +]); + +export const poolRepository = factories.data.findable(poolsMap); + +const metaStableGauge = '0xcd4722b7c24c29e0413bdcd9e51404b4539d14ae'; +const composableStableGauge = '0xa6325e799d266632d347e41265a69af111b05403'; +const gaugesMap = new Map([ + [ + composableStableGauge, + { + id: composableStableGauge, + poolId: + '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d', + } as unknown as SubgraphLiquidityGauge, + ], + [ + metaStableGauge, + { + id: metaStableGauge, + poolId: + '0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080', + } as unknown as SubgraphLiquidityGauge, + ], +]); + +export const gaugesRepository = + factories.data.findable(gaugesMap); + +const polygonbDaiPool = { + ...polygon.data.pools.find( + (p) => p.address === '0x178e029173417b1f9c8bc16dcec6f697bc323746' + ), +} as Pool; +const polygonbUsdcPool = { + ...polygon.data.pools.find( + (p) => p.address === '0xf93579002dbe8046c43fefe86ec78b1112247bb8' + ), +} as Pool; +const polygonbUsdtPool = { + ...polygon.data.pools.find( + (p) => p.address === '0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea6' + ), +} as Pool; +const polygonComposableStable = { + ...polygon.data.pools.find( + (p) => p.address === '0x48e6b98ef6329f8f0a30ebb8c7c960330d648085' + ), +} as Pool; + +export { polygonComposableStable }; + +const polygonPoolsMap = new Map([ + [polygonComposableStable.id, polygonComposableStable as Pool], + [polygonbDaiPool.id, polygonbDaiPool as Pool], + [polygonbUsdcPool.id, polygonbUsdcPool as Pool], + [polygonbUsdtPool.id, polygonbUsdtPool as Pool], +]); + +export const polygonPoolRepository = + factories.data.findable(polygonPoolsMap); diff --git a/balancer-js/src/modules/liquidity-managment/migrations/builder.spec.ts b/balancer-js/src/modules/liquidity-managment/migrations/builder.spec.ts new file mode 100644 index 000000000..1e297926b --- /dev/null +++ b/balancer-js/src/modules/liquidity-managment/migrations/builder.spec.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import { migrationBuilder } from './builder'; +import { buildMigrationPool } from './helpers'; +import { + metaStable, + bDaiPool, + composableStable, + poolRepository, +} from './builder.spec-helpers'; + +describe('Migrations', () => { + context('Metastable to Metastable', () => { + const from = metaStable; + const to = from; + const address = '0xfacec29Ae158B26e234B1a81Db2431F6Bd8F8cE8'; + + it('should build a migration using exit / join only', async () => { + const migration = migrationBuilder( + address, + address, + '1000000000000000000', + '0', + from, + to + ); + expect(migration).to.match(/^0xac9650d8*/); + }); + }); + + describe('.buildMigrationPool', () => { + it('should build a migrationPool with nested tokens', async () => { + const migrationPool = await buildMigrationPool( + composableStable.id, + poolRepository + ); + const tokens = migrationPool.tokens.map(({ address }) => address).flat(); + expect(tokens.length).to.eq(4); + expect(tokens).to.include(bDaiPool.address); + const nestedTokens = migrationPool.tokens[3].tokens; + expect(nestedTokens).to.not.be.undefined; + if (nestedTokens) { + expect(nestedTokens.flatMap(({ address }) => address).length).to.eq(3); + expect(nestedTokens.flatMap(({ address }) => address)).to.include( + bDaiPool.address + ); + } + }); + }); +}); diff --git a/balancer-js/src/modules/liquidity-managment/migrations/builder.ts b/balancer-js/src/modules/liquidity-managment/migrations/builder.ts new file mode 100644 index 000000000..fff82f872 --- /dev/null +++ b/balancer-js/src/modules/liquidity-managment/migrations/builder.ts @@ -0,0 +1,166 @@ +import { OutputReference, Relayer } from '@/modules/relayer/relayer.module'; +import * as actions from '@/modules/relayer/actions'; +import { buildPaths, MigrationPool, balancerRelayerInterface } from './helpers'; +import { BigNumber } from '@ethersproject/bignumber'; + +/** + * Builds migration call data. + * + * @param account Address of the migrating account + * @param relayer Address of the relayer + * @param bptAmount Amount of BPT to migrate + * @param minBptOut Minimal amount of BPT to receive + * @param from Pool to migrate from + * @param to Pool to migrate to + * @param peek Add a peek call for the expected BPT amount, decodable by the `decodePeak` function + * @param fromGauge Unstake from gauge before migrating + * @param toGauge Restake to gauge after migrating + * @returns call data + */ +export const migrationBuilder = ( + account: string, + relayer: string, + bptAmount: string, + minBptOut: string, + from: MigrationPool, + to: MigrationPool, + peek = false, + fromGauge?: string, + toGauge?: string +): string => { + if ( + !from.id || + !to.id || + !from.tokens || + !to.tokens || + !from.poolType || + !to.poolType + ) { + throw 'Pool data is missing'; + } + + // Define tokens + const fromTokens = from.tokens.flatMap(({ address }) => address); + const toTokens = to.tokens.flatMap(({ address }) => address); + + // Prefer proportional exit, except for ComposableStableV1 + // Choose 0 as the exit token index + // TODO: make default exit token dynamic + const exitTokenIndex = + from.poolType == 'ComposableStable' && from.poolTypeVersion == 1 ? 0 : -1; + + // Define output references + let exitOutputReferences: OutputReference[]; + let swapOutputReferences: BigNumber[] = []; + if (exitTokenIndex > -1) { + exitOutputReferences = [ + { + index: exitTokenIndex, + key: Relayer.toChainedReference(`10${exitTokenIndex}`), + }, + ]; + swapOutputReferences = [Relayer.toChainedReference(`20${exitTokenIndex}`)]; + } else { + exitOutputReferences = fromTokens.map((_, idx) => ({ + index: idx, + key: Relayer.toChainedReference(`10${idx}`), + })); + swapOutputReferences = fromTokens.map((_, idx) => + Relayer.toChainedReference(`20${idx}`) + ); + } + + const joinAmount = Relayer.toChainedReference('999'); + + // Configure migration steps + const migrationSteps = []; + let needsSwap = false; // only if from is ComposableStable + + if (from.poolType === 'ComposableStable') { + needsSwap = true; + } + + // 1. Withdraw from old gauge + if (fromGauge) { + migrationSteps.push( + actions.gaugeWithdrawal(fromGauge, account, relayer, bptAmount) + ); + } + + // 2. Exit old pool + migrationSteps.push( + actions.exit( + from.id, + from.poolType, + from.poolTypeVersion || 1, + fromTokens, + exitTokenIndex, + exitOutputReferences, + bptAmount, + fromGauge ? relayer : account, + relayer + ) + ); + + // 3. Swap + const swapPaths = buildPaths(from.tokens, to.tokens, exitTokenIndex); + if (swapPaths.flat().length > 0) { + // Match exit to swap amounts + const swaps = swapPaths + .map((path, idx) => ({ + path, + inputAmount: String(exitOutputReferences[idx].key), + outputReference: swapOutputReferences[idx], + })) + .filter(({ path }) => path.length > 0); + + migrationSteps.push(actions.swaps(relayer, relayer, swaps)); + } + + // 3. Join + // Match swap or exit references to the positions of join tokens + // In case no reference is defined, the default is 0 + const references = toTokens + .filter((address) => to.address != address) + .map((to) => { + const fromIdx = fromTokens.indexOf(to); + return String( + (needsSwap && swapOutputReferences[fromIdx]) || + exitOutputReferences[fromIdx]?.key || + 0 + ); + }); + + migrationSteps.push( + actions.join( + to.id, + to.poolType, + to.poolTypeVersion || 1, + toTokens, + references, + minBptOut, + String(joinAmount), + relayer, + toGauge ? relayer : account, + true + ) + ); + + // Peek the last join amount + if (peek === true) { + migrationSteps.push(actions.peekChainedReferenceValue(String(joinAmount))); + } + + // 4. Deposit to the new gauge + if (toGauge) { + migrationSteps.push( + actions.gaugeDeposit(toGauge, relayer, account, String(joinAmount)) + ); + } + + const callData = balancerRelayerInterface.encodeFunctionData('multicall', [ + migrationSteps, + ]); + + return callData; +}; diff --git a/balancer-js/src/modules/liquidity-managment/migrations/helpers.ts b/balancer-js/src/modules/liquidity-managment/migrations/helpers.ts new file mode 100644 index 000000000..2bae07756 --- /dev/null +++ b/balancer-js/src/modules/liquidity-managment/migrations/helpers.ts @@ -0,0 +1,174 @@ +import { Findable, Pool, PoolAttribute } from '@/types'; +import balancerRelayerAbi from '@/lib/abi/BalancerRelayer.json'; +import { Interface } from '@ethersproject/abi'; + +export const balancerRelayerInterface = new Interface(balancerRelayerAbi); + +/** + * Using array of objects to preserve the tokens order + */ +export interface MigrationPool { + address: string; + id?: string; + poolType?: string; + poolTypeVersion?: number; + tokens?: MigrationPool[]; + mainIndex?: number; +} + +/** + * Foreach AaveLinear: AaveLinear > mainTokens > newAaveLinear + * + * @param fromTokens + * @param toTokens + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const buildPaths = ( + fromTokens: MigrationPool[], + toTokens: MigrationPool[], + exitTokenIndex: number +) => { + // Get the main token address for each pool + const getMainToken = ({ tokens, mainIndex }: MigrationPool) => + (tokens && mainIndex && tokens[mainIndex].address) || ''; + const mainFromTokens = fromTokens.flatMap(getMainToken); + const mainToTokens = toTokens.flatMap(getMainToken); + + // Find the index of the main token in both from and to pools + const pathIndexes = mainFromTokens.map( + (token, idx) => (token && [idx, mainToTokens.indexOf(token)]) || [-1, -1] + ); + + // Build the paths from the indexes + const exitSwaps = pathIndexes.map(([fromIdx, toIdx]) => { + if (fromIdx === -1 || toIdx === -1) { + return []; + } + const fromPool = fromTokens[fromIdx]; + const toPool = toTokens[toIdx]; + return buildPath(fromPool, toPool); + }); + + // If we want to exit a specific token, return only that path + if (exitTokenIndex > -1) { + return [exitSwaps[exitTokenIndex]]; + } + + return exitSwaps; +}; + +const buildPath = (from: MigrationPool, to: MigrationPool) => { + if (from.poolType?.match(/.*Linear.*/)) { + return buildLinearPath(from, to); + } + + return []; +}; + +const buildLinearPath = (from: MigrationPool, to: MigrationPool) => { + if ( + !from.id || + !to.id || + !from.tokens || + !to.tokens || + !from.mainIndex || + !to.mainIndex + ) { + throw 'Missing tokens'; + } + const mainToken = from.tokens[from.mainIndex]; + + const path = [ + { + poolId: from.id, + assetIn: from.address, + assetOut: mainToken.address, + }, + { + poolId: to.id, + assetIn: mainToken.address, + assetOut: to.address, + }, + ]; + + return path; +}; + +/** + * Converts Subgraph Pool to MigrationPool + * Recursively builds tokens + * + * @param id + * @param repository + * @returns + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const buildMigrationPool = async ( + id: string, + repository: Findable +) => { + const pool = await repository.find(id); + if (!pool) throw `Pool ${id} not found`; + + const findTokens = async (token: string, parentPool: string) => { + let tokens: Array = [{ address: token }]; + const pool = await repository.findBy('address', token); + if (pool && token != parentPool) { + const sortedTokens = pool.tokens.sort(cmpTokens); + const nestedTokens = await Promise.all( + sortedTokens.map(({ address }) => findTokens(address, pool.address)) + ); + tokens = [ + { + address: token, + id: pool.id, + poolType: pool.poolType, + poolTypeVersion: pool.poolTypeVersion, + mainIndex: pool.mainIndex, + tokens: nestedTokens.flat(), + }, + ]; + } + return tokens; + }; + + // Sorts amounts out into ascending order (referenced to token addresses) to match the format expected by the Vault. + const sortedTokens = pool.tokens.sort(cmpTokens); + + return { + id, + address: pool.address, + tokens: ( + await Promise.all( + sortedTokens.map(({ address }) => findTokens(address, pool.address)) + ) + ).flat(), + poolType: pool.poolType, + poolTypeVersion: pool.poolTypeVersion, + mainIndex: pool.mainIndex, + }; +}; + +const cmpTokens = (tokenA: MigrationPool, tokenB: MigrationPool): number => + tokenA.address.toLowerCase() > tokenB.address.toLowerCase() ? 1 : -1; + +/** + * Decodes the relayer return value to get the minimum BPT out. + * + * @param relayerReturnValue + * @returns + */ +export const getMinBptOut = (relayerReturnValue: string): string => { + // Get last two positions of the return value, bptOut is the last one or the second to last one in case there is a gauge deposit + // join and gauge deposit are always 0x, so any other value means that's the bptOut + const multicallResult = balancerRelayerInterface.decodeFunctionResult( + 'multicall', + relayerReturnValue + ); + + const minBptOut = multicallResult[0] + .slice(-2) + .filter((v: string) => v !== '0x'); + + return String(BigInt(minBptOut)); +}; diff --git a/balancer-js/src/modules/relayer/actions.spec.ts b/balancer-js/src/modules/relayer/actions.spec.ts new file mode 100644 index 000000000..97ddff791 --- /dev/null +++ b/balancer-js/src/modules/relayer/actions.spec.ts @@ -0,0 +1,101 @@ +import { expect } from 'chai'; +import { Relayer } from './relayer.module'; +import * as action from './actions'; + +/** + * Write spec for the relayer steps module. + */ + +describe('Relayer actions', () => { + const address = '0xfacec29Ae158B26e234B1a81Db2431F6Bd8F8cE8'; + const poolId = + '0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080'; + + describe('.gaugeWithdrawal', () => { + it('should encode', () => { + const subject = action.gaugeWithdrawal( + address, + address, + address, + '1000000000000000000' + ); + expect(subject).to.match(/^0x65ca4804*/); + }); + }); + + describe('.gaugeDeposit', () => { + it('should encode', () => { + const subject = action.gaugeDeposit( + address, + address, + address, + '1000000000000000000' + ); + expect(subject).to.match(/^0x7bc008f5*/); + }); + }); + + describe('.exit', () => { + it('should encode', () => { + const assets = [address, address, address]; + const refs = assets.map((_, idx) => ({ + index: idx, + key: Relayer.toChainedReference(`10${idx}`), + })); + const subject = action.exit( + poolId, + 'Weighted', + 1, + assets, + -1, + refs, + '1000000000000000000', + address, + address + ); + expect(subject).to.match(/^0xd80952d5*/); + }); + }); + + describe('.join', () => { + it('should encode', () => { + const assets = [address, address, address]; + const refs = assets.map((_, idx) => + String(Relayer.toChainedReference(`10${idx}`)) + ); + const output = Relayer.toChainedReference(`999`); + const subject = action.join( + poolId, + 'Weighted', + 1, + assets, + refs, + '0', + String(output), + address, + address + ); + expect(subject).to.match(/^0x8fe4624f*/); + }); + }); + + describe('.swaps', () => { + it('should encode', () => { + const swaps = [ + { + path: [ + { + poolId, + assetIn: address, + assetOut: address, + }, + ], + inputAmount: '1000000000000000000', + outputReference: Relayer.toChainedReference(`20${0}`), + }, + ]; + const subject = action.swaps(address, address, swaps); + expect(subject).to.match(/^0x18369446*/); + }); + }); +}); diff --git a/balancer-js/src/modules/relayer/actions.ts b/balancer-js/src/modules/relayer/actions.ts new file mode 100644 index 000000000..f5a8b3ed2 --- /dev/null +++ b/balancer-js/src/modules/relayer/actions.ts @@ -0,0 +1,242 @@ +import { BigNumber } from '@ethersproject/bignumber'; +import { MaxInt256 } from '@ethersproject/constants'; +import { BatchSwapStep, SwapType } from '../swaps/types'; +import { Relayer } from './relayer.module'; +import { OutputReference, PoolKind } from './types'; +import { StablePoolEncoder } from '@/pool-stable'; +import { ComposableStablePoolEncoder } from '@/pool-composable-stable'; + +/** + * Converts poolType and poolTypeVersion to PoolKind used by Relayer V5 defined in: + * https://github.com/balancer/balancer-v2-monorepo/blob/9b78879ee3a0dcae57094bdfdae973873e4262cf/pkg/standalone-utils/contracts/relayer/VaultActions.sol#L105 + * + * @internal + * @param poolType + * @param poolTypeVersion + * @returns PoolKind + */ +const poolType2PoolKind = ( + poolType: string, + poolTypeVersion: number +): PoolKind => { + if (poolType === 'Stable') { + return PoolKind.LEGACY_STABLE; + } else if (poolType === 'ComposableStable' && poolTypeVersion === 1) { + return PoolKind.COMPOSABLE_STABLE; + } else if (poolType === 'ComposableStable') { + return PoolKind.COMPOSABLE_STABLE_V2; + } else { + return PoolKind.WEIGHTED; + } +}; + +export const gaugeWithdrawal = Relayer.encodeGaugeWithdraw; +export const gaugeDeposit = Relayer.encodeGaugeDeposit; +export const peekChainedReferenceValue = + Relayer.encodePeekChainedReferenceValue; + +/** + * Encodes exitPool callData. + * Exit pool to underlying tokens and assigns output references to the exit amounts. + * + * @param poolId Pool ID. + * @param poolType Pool type. + * @param poolTypeVersion Pool type version. + * @param assets Ordered pool tokens. + * @param singleTokenExit When + * @param outputReferences reference to exit amounts for the next transaction + * @param amount Amount of BPT to exit with as a number with 18 digits of precision passed as a string. + * @param sender Sender address. + * @param recipient Recipient address. + * @param isComposable Whether the poolType is ComposableStable or not. + * @param toInternalBalance Use internal balance or not. + * @returns Encoded exitPool call. + */ +export const exit = ( + poolId: string, + poolType: string, + poolTypeVersion: number, + assets: string[], + exitTokenIndex = -1, + outputReferences: OutputReference[], + amount: string, + sender: string, + recipient: string, + toInternalBalance = true +): string => { + let userData: string; + const isComposable = poolType === 'ComposableStable' && poolTypeVersion === 1; + + // Exit pool proportionally or to a singleToken + if (exitTokenIndex > -1) { + userData = StablePoolEncoder.exitExactBPTInForOneTokenOut( + amount, + exitTokenIndex + ); + } else { + const encoder = isComposable + ? ComposableStablePoolEncoder.exitExactBPTInForAllTokensOut + : StablePoolEncoder.exitExactBPTInForTokensOut; + userData = encoder(amount); + } + + // Relayer V5 introduces PoolKind + const poolKind = poolType2PoolKind(poolType, poolTypeVersion); + + // Encode exit pool data + const callData = Relayer.encodeExitPool({ + poolId, + poolKind, + sender, + recipient, + outputReferences, + exitPoolRequest: { + assets, + minAmountsOut: new Array(assets.length).fill('0'), + userData, + toInternalBalance, + }, + }); + + return callData; +}; + +/** + * Encodes joinPool callData. + * Joins pool with underlying tokens and assigns output reference to the BPT amount. + * + * @param poolId Pool ID. + * @param poolType Pool type. + * @param poolTypeVersion Pool type version. + * @param assets Ordered pool tokens. + * @param amountsIn Amounts of tokens to join with as a number with 18 digits of precision passed as a string. + * @param minimumBPT Minimum BPT amount to receive as a number with 18 digits of precision passed as a string. + * @param outputReference reference to BPT amount for the next transaction + * @param sender Sender address. + * @param recipient Recipient address. + * @param fromInternalBalance Use internal balance or not. + * @returns Encoded joinPool call. + */ +export const join = ( + poolId: string, + poolType: string, + poolTypeVersion: number, + assets: string[], + amountsIn: string[], + minimumBPT: string, + outputReference: string, + sender: string, + recipient: string, + fromInternalBalance = true +): string => { + const maxAmountsIn = assets.map(() => MaxInt256); + + // Encoding join pool data with the type exactTokensIn (1) + // StablePoolEncoder.joinExactTokensInForBPTOut is the same for all pool types + const userData = StablePoolEncoder.joinExactTokensInForBPTOut( + amountsIn, + minimumBPT + ); + + const kind = poolType2PoolKind(poolType, poolTypeVersion); + + const callData = Relayer.encodeJoinPool({ + poolId, + kind, + sender, + recipient, + joinPoolRequest: { + assets, + maxAmountsIn, + userData, + fromInternalBalance, + }, + value: '0', + outputReference, + }); + + return callData; +}; + +/** + * Creates encoded batchSwap callData + * outputReferences contain the output amount for swap's last token + * + * @param sender Sender address. + * @param recipient Recipient address. + * @param swaps List of swaps to execute. + * @param deadline Deadline for the transaction. + * @param toInternalBalance Use internal balance or not. + * @returns Encoded batchSwap call + */ +export const swaps = ( + sender: string, + recipient: string, + swaps: { + path: { + poolId: string; + assetIn: string; + assetOut: string; + }[]; + inputAmount: string; + outputReference: BigNumber; + }[], + deadline?: string, + toInternalBalance = true +): string => { + const assets: string[] = []; + const limits: string[] = []; + const outputReferences: { index: number; key: BigNumber }[] = []; + const batchSwaps: BatchSwapStep[] = []; + + // Convert paths into batchSwap steps + swaps.forEach((swap) => { + const { path, inputAmount, outputReference } = swap; + + for (let i = 0; i < path.length; i++) { + const { poolId, assetIn, assetOut } = path[i]; + + assets.push(assetIn); + assets.push(assetOut); + + limits.push(MaxInt256.toString()); + limits.push('0'); + + const assetInIndex = i * 2; + const assetOutIndex = i * 2 + 1; + + const swap: BatchSwapStep = { + poolId, + assetInIndex, + assetOutIndex, + amount: i === 0 ? inputAmount : '0', + userData: '0x', + }; + + batchSwaps.push(swap); + } + + // Add output reference for the last swap + outputReferences.push({ index: path.length * 2 - 1, key: outputReference }); + }); + + const funds = { + sender, + recipient, + fromInternalBalance: true, + toInternalBalance, + }; + + const encodedBatchSwap = Relayer.encodeBatchSwap({ + swapType: SwapType.SwapExactIn, + swaps: batchSwaps, + assets, + funds, + limits, + deadline: deadline || BigNumber.from(Math.ceil(Date.now() / 1000) + 3600), // 1 hour from now + value: '0', + outputReferences, + }); + + return encodedBatchSwap; +}; diff --git a/balancer-js/src/modules/relayer/types.ts b/balancer-js/src/modules/relayer/types.ts index ead15a619..48613b6d7 100644 --- a/balancer-js/src/modules/relayer/types.ts +++ b/balancer-js/src/modules/relayer/types.ts @@ -3,6 +3,13 @@ import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; import { ExitPoolRequest, JoinPoolRequest } from '@/types'; import { SwapType, BatchSwapStep, FundManagement } from '@/modules/swaps/types'; +export enum PoolKind { + WEIGHTED = 0, + LEGACY_STABLE, + COMPOSABLE_STABLE, + COMPOSABLE_STABLE_V2, +} + export type OutputReference = { index: number; key: BigNumber; diff --git a/balancer-js/src/modules/sdk.module.ts b/balancer-js/src/modules/sdk.module.ts index 985d8a6f9..dd001bec0 100644 --- a/balancer-js/src/modules/sdk.module.ts +++ b/balancer-js/src/modules/sdk.module.ts @@ -12,6 +12,7 @@ import { Pools } from './pools'; import { Data } from './data'; import { VaultModel } from './vaultModel/vaultModel.module'; import { JsonRpcProvider } from '@ethersproject/providers'; +import { Migrations } from './liquidity-managment/migrations'; export interface BalancerSDKRoot { config: BalancerSdkConfig; @@ -38,6 +39,7 @@ export class BalancerSDK implements BalancerSDKRoot { readonly networkConfig: BalancerNetworkConfig; readonly provider: JsonRpcProvider; readonly claimService?: IClaimService; + readonly migrationService?: Migrations; constructor( public config: BalancerSdkConfig, @@ -68,7 +70,7 @@ export class BalancerSDK implements BalancerSDKRoot { ); this.zaps = new Zaps(this.networkConfig.chainId); - if (this.data.liquidityGauges) + if (this.data.liquidityGauges) { this.claimService = new ClaimService( this.data.liquidityGauges, this.data.feeDistributor, @@ -78,6 +80,13 @@ export class BalancerSDK implements BalancerSDKRoot { this.networkConfig.addresses.contracts.gaugeClaimHelper, this.networkConfig.addresses.contracts.balancerMinterAddress ); + this.migrationService = new Migrations( + this.networkConfig.addresses.contracts.relayerV5 as string, + this.data.pools, + this.data.liquidityGauges.subgraph, + this.provider + ); + } this.vaultModel = new VaultModel( this.data.poolsForSor, this.networkConfig.addresses.tokens.wrappedNativeAsset diff --git a/balancer-js/src/test/factories/data.ts b/balancer-js/src/test/factories/data.ts index 3cb967a73..f359e3bad 100644 --- a/balancer-js/src/test/factories/data.ts +++ b/balancer-js/src/test/factories/data.ts @@ -16,11 +16,23 @@ import { } from '@/types'; import { SubgraphPoolDataService } from '@/modules/sor/pool-data/subgraphPoolDataService'; +const searchMap = (map: Map, field: string, fieldValue: string) => { + let foundEntry = undefined; + map.forEach((value, key) => { + if (value[field] === fieldValue) { + foundEntry = value; + } + }); + + return foundEntry; +}; + export const findable = ( map: Map ): Findable & Searchable => ({ find: (id: string) => Promise.resolve(map.get(id)), - findBy: (param: P, value: V) => Promise.resolve(map.get(value)), + findBy: (param: P, value: V) => + Promise.resolve(searchMap(map, param as string, value as string)), all: () => Promise.resolve(Object.values(map)), where: (filters: (arg: T) => boolean) => Promise.resolve(Object.values(map)), }); diff --git a/balancer-js/src/test/factories/named-tokens.ts b/balancer-js/src/test/factories/named-tokens.ts index ec01e47b4..572df4cf4 100644 --- a/balancer-js/src/test/factories/named-tokens.ts +++ b/balancer-js/src/test/factories/named-tokens.ts @@ -25,7 +25,7 @@ export const namedTokens: Record = { decimals: 18, }, bDAI: { - address: '0x804cdb9116a10bb78768d3252355a1b18067bf8f', + address: '0xae37d54ae477268b9997d4161b96b8200755935c', decimals: 18, }, USDC: { @@ -37,7 +37,7 @@ export const namedTokens: Record = { decimals: 18, }, bUSDC: { - address: '0x9210f1204b5a24742eba12f710636d76240df3d0', + address: '0x82698aecc9e28e9bb27608bd52cf57f704bd1b83', decimals: 18, }, USDT: { @@ -49,7 +49,43 @@ export const namedTokens: Record = { decimals: 18, }, bUSDT: { - address: '0x2bbf681cc4eb09218bee85ea2a5d3d13fa40fc0c', + address: '0x2f4eb100552ef93840d5adc30560e5513dfffacb', + decimals: 18, + }, + 'polygon-DAI': { + address: '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', + decimals: 18, + }, + 'polygon-aDAI': { + address: '0xee029120c72b0607344f35b17cdd90025e647b00', + decimals: 18, + }, + 'polygon-bDAI': { + address: '0x178e029173417b1f9c8bc16dcec6f697bc323746', + decimals: 18, + }, + 'polygon-USDC': { + address: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', + decimals: 6, + }, + 'polygon-aUSDC': { + address: '0x221836a597948dce8f3568e044ff123108acc42a', + decimals: 18, + }, + 'polygon-bUSDC': { + address: '0xf93579002dbe8046c43fefe86ec78b1112247bb8', + decimals: 18, + }, + 'polygon-USDT': { + address: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', + decimals: 6, + }, + 'polygon-aUSDT': { + address: '0x19c60a251e525fa88cd6f3768416a8024e98fc19', + decimals: 18, + }, + 'polygon-bUSDT': { + address: '0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea6', decimals: 18, }, BAL: { diff --git a/balancer-js/src/test/lib/utils.ts b/balancer-js/src/test/lib/utils.ts index 1c17df083..dd2cfc3cc 100644 --- a/balancer-js/src/test/lib/utils.ts +++ b/balancer-js/src/test/lib/utils.ts @@ -91,6 +91,21 @@ export const forkSetup = async ( } }; +export const reset = async ( + jsonRpcUrl: string, + provider: JsonRpcProvider, + blockNumber?: number +): Promise => { + await provider.send('hardhat_reset', [ + { + forking: { + jsonRpcUrl, + blockNumber, + }, + }, + ]); +}; + /** * Set token balance for a given account * @@ -112,7 +127,6 @@ export const setTokenBalance = async ( const setStorageAt = async (token: string, index: string, value: string) => { await signer.provider.send('hardhat_setStorageAt', [token, index, value]); - await signer.provider.send('evm_mine', []); // Just mines to the next block }; const signerAddress = await signer.getAddress(); From 75956ca9b2bc3758fe43946f40ca948c15e13b52 Mon Sep 17 00:00:00 2001 From: johngrantuk <4797222+johngrantuk@users.noreply.github.com> Date: Thu, 6 Apr 2023 13:39:38 +0000 Subject: [PATCH 4/7] chore: version bump v1.0.4-beta.0 --- balancer-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer-js/package.json b/balancer-js/package.json index 7cc88af49..0aaffd3db 100644 --- a/balancer-js/package.json +++ b/balancer-js/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sdk", - "version": "1.0.3", + "version": "1.0.4-beta.0", "description": "JavaScript SDK for interacting with the Balancer Protocol V2", "license": "GPL-3.0-only", "homepage": "https://github.com/balancer-labs/balancer-sdk#readme", From e41ba8fc4e660d019e57e9648e138b11cf4e6104 Mon Sep 17 00:00:00 2001 From: johngrantuk <4797222+johngrantuk@users.noreply.github.com> Date: Tue, 11 Apr 2023 14:22:55 +0000 Subject: [PATCH 5/7] chore: version bump v1.0.4-beta.1 --- balancer-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer-js/package.json b/balancer-js/package.json index 0aaffd3db..59b128d7f 100644 --- a/balancer-js/package.json +++ b/balancer-js/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sdk", - "version": "1.0.4-beta.0", + "version": "1.0.4-beta.1", "description": "JavaScript SDK for interacting with the Balancer Protocol V2", "license": "GPL-3.0-only", "homepage": "https://github.com/balancer-labs/balancer-sdk#readme", From 6889030a34120c8a3bcb8bb2f3c2bb77966243ff Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 13 Apr 2023 09:51:34 +0100 Subject: [PATCH 6/7] Update to version 1.0.4 for release. --- balancer-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer-js/package.json b/balancer-js/package.json index 59b128d7f..6d1cbfc7f 100644 --- a/balancer-js/package.json +++ b/balancer-js/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sdk", - "version": "1.0.4-beta.1", + "version": "1.0.4", "description": "JavaScript SDK for interacting with the Balancer Protocol V2", "license": "GPL-3.0-only", "homepage": "https://github.com/balancer-labs/balancer-sdk#readme", From a1993bfd2b898bb8d3db468a9e109d0e5ef1d2e5 Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Thu, 13 Apr 2023 10:05:38 +0100 Subject: [PATCH 7/7] Update block number for test. --- .../concerns/composableStable/join.concern.integration.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts index 8878cfbbc..25a4fea46 100644 --- a/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts +++ b/balancer-js/src/modules/pools/pool-types/concerns/composableStable/join.concern.integration.spec.ts @@ -25,7 +25,7 @@ const { ALCHEMY_URL_POLYGON: jsonRpcUrl } = process.env; const rpcUrl = 'http://127.0.0.1:8137'; const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network); const signer = provider.getSigner(); -const blockNumber = 40767042; +const blockNumber = 41029487; const testPoolId = '0x02d2e2d7a89d6c5cb3681cfcb6f7dac02a55eda400000000000000000000088f';