From 195b3a86b7d20b2354d640d657e3f34761995b38 Mon Sep 17 00:00:00 2001 From: ChouAndy Date: Tue, 1 Aug 2023 16:18:23 +0800 Subject: [PATCH] feat: utility add flash loan aggregator logic --- .changeset/wicked-carpets-reflect.md | 5 + src/logics/aave-v2/logic.flash-loan.ts | 20 +++- src/logics/aave-v2/service.test.ts | 1 - src/logics/aave-v3/configs.ts | 8 +- src/logics/aave-v3/logic.flash-loan.ts | 20 +++- src/logics/balancer-v2/logic.flash-loan.ts | 8 +- src/logics/paraswap-v5/logic.swap-token.ts | 14 ++- src/logics/utility/index.ts | 1 + .../logic.flash-loan-aggregator.test.ts | 15 +++ .../utility/logic.flash-loan-aggregator.ts | 98 +++++++++++++++++++ test/logics/balancer-v2/flash-loan.test.ts | 14 +-- .../utility/flash-loan-aggregrator.test.ts | 70 +++++++++++++ 12 files changed, 250 insertions(+), 24 deletions(-) create mode 100644 .changeset/wicked-carpets-reflect.md create mode 100644 src/logics/utility/logic.flash-loan-aggregator.test.ts create mode 100644 src/logics/utility/logic.flash-loan-aggregator.ts create mode 100644 test/logics/utility/flash-loan-aggregrator.test.ts diff --git a/.changeset/wicked-carpets-reflect.md b/.changeset/wicked-carpets-reflect.md new file mode 100644 index 00000000..db4b0505 --- /dev/null +++ b/.changeset/wicked-carpets-reflect.md @@ -0,0 +1,5 @@ +--- +'@protocolink/logics': patch +--- + +feat: utility add flash loan aggregator logic diff --git a/src/logics/aave-v2/logic.flash-loan.ts b/src/logics/aave-v2/logic.flash-loan.ts index d294edec..a405f5e2 100644 --- a/src/logics/aave-v2/logic.flash-loan.ts +++ b/src/logics/aave-v2/logic.flash-loan.ts @@ -24,11 +24,23 @@ export type FlashLoanLogicFields = core.FlashLoanFields<{ referralCode?: number export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInterface, core.LogicBuilderInterface { static readonly supportedChainIds = supportedChainIds; + get callbackAddress() { + return getContractAddress(this.chainId, 'AaveV2FlashLoanCallback'); + } + async getTokenList() { const service = new Service(this.chainId, this.provider); - const tokens: FlashLoanLogicTokenList = await service.getAssets(); + const tokens = await service.getAssets(); + const { assetInfos } = await service.getFlashLoanConfiguration(tokens); + + const tokenList: FlashLoanLogicTokenList = []; + for (let i = 0; i < assetInfos.length; i++) { + const { isActive } = assetInfos[i]; + if (!isActive) continue; + tokenList.push(tokens[i]); + } - return tokens; + return tokenList; } async quote(params: FlashLoanLogicParams) { @@ -72,7 +84,7 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt modes.push(InterestRateMode.none); }); const data = LendingPool__factory.createInterface().encodeFunctionData('flashLoan', [ - getContractAddress(this.chainId, 'AaveV2FlashLoanCallback'), + this.callbackAddress, assets, amounts, modes, @@ -81,7 +93,7 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt referralCode, ]); - const callback = getContractAddress(this.chainId, 'AaveV2FlashLoanCallback'); + const callback = this.callbackAddress; return core.newLogic({ to, data, callback }); } diff --git a/src/logics/aave-v2/service.test.ts b/src/logics/aave-v2/service.test.ts index c642a044..cd96bb11 100644 --- a/src/logics/aave-v2/service.test.ts +++ b/src/logics/aave-v2/service.test.ts @@ -59,7 +59,6 @@ describe('AaveV2 Service', function () { testCases.forEach(({ assets }, i) => { it(`case ${i + 1}`, async function () { const flashLoanConfiguration = await service.getFlashLoanConfiguration(assets); - console.log('object :>> ', JSON.stringify(flashLoanConfiguration)); expect(flashLoanConfiguration).to.have.keys('feeBps', 'assetInfos'); expect(flashLoanConfiguration.assetInfos).to.have.lengthOf.above(0); for (const assetInfo of flashLoanConfiguration.assetInfos) { diff --git a/src/logics/aave-v3/configs.ts b/src/logics/aave-v3/configs.ts index e3502018..9aca9368 100644 --- a/src/logics/aave-v3/configs.ts +++ b/src/logics/aave-v3/configs.ts @@ -18,28 +18,28 @@ export const configs: Config[] = [ { chainId: common.ChainId.polygon, contract: { - PoolDataProvider: '0x69FA688f1Dc47d4B5d8029D5a35FB7a548310654', + PoolDataProvider: '0x9441B65EE553F70df9C77d45d3283B6BC24F222d', AaveV3FlashLoanCallback: '0xe1356560B683cA54e7D7e9e81b05319E9140a977', }, }, { chainId: common.ChainId.arbitrum, contract: { - PoolDataProvider: '0x69FA688f1Dc47d4B5d8029D5a35FB7a548310654', + PoolDataProvider: '0x6b4E260b765B3cA1514e618C0215A6B7839fF93e', AaveV3FlashLoanCallback: '0xe1356560B683cA54e7D7e9e81b05319E9140a977', }, }, { chainId: common.ChainId.optimism, contract: { - PoolDataProvider: '0x69FA688f1Dc47d4B5d8029D5a35FB7a548310654', + PoolDataProvider: '0xd9Ca4878dd38B021583c1B669905592EAe76E044', AaveV3FlashLoanCallback: '0xe1356560B683cA54e7D7e9e81b05319E9140a977', }, }, { chainId: common.ChainId.avalanche, contract: { - PoolDataProvider: '0x69FA688f1Dc47d4B5d8029D5a35FB7a548310654', + PoolDataProvider: '0x50ddd0Cd4266299527d25De9CBb55fE0EB8dAc30', AaveV3FlashLoanCallback: '0xe1356560B683cA54e7D7e9e81b05319E9140a977', }, }, diff --git a/src/logics/aave-v3/logic.flash-loan.ts b/src/logics/aave-v3/logic.flash-loan.ts index 6f5d2747..3fe9ee14 100644 --- a/src/logics/aave-v3/logic.flash-loan.ts +++ b/src/logics/aave-v3/logic.flash-loan.ts @@ -27,11 +27,23 @@ export class FlashLoanLogic { static readonly supportedChainIds = supportedChainIds; + get callbackAddress() { + return getContractAddress(this.chainId, 'AaveV3FlashLoanCallback'); + } + async getTokenList() { const service = new Service(this.chainId, this.provider); - const tokens: FlashLoanLogicTokenList = await service.getAssets(); + const tokens = await service.getAssets(); + const { assetInfos } = await service.getFlashLoanConfiguration(tokens); + + const tokenList: FlashLoanLogicTokenList = []; + for (let i = 0; i < assetInfos.length; i++) { + const { isActive, isPaused, isFlashLoanEnabled } = assetInfos[i]; + if (!isActive || isPaused || !isFlashLoanEnabled) continue; + tokenList.push(tokens[i]); + } - return tokens; + return tokenList; } async quote(params: FlashLoanLogicParams) { @@ -77,7 +89,7 @@ export class FlashLoanLogic modes.push(InterestRateMode.none); }); const data = Pool__factory.createInterface().encodeFunctionData('flashLoan', [ - getContractAddress(this.chainId, 'AaveV3FlashLoanCallback'), + this.callbackAddress, assets, amounts, modes, @@ -86,7 +98,7 @@ export class FlashLoanLogic referralCode, ]); - const callback = getContractAddress(this.chainId, 'AaveV3FlashLoanCallback'); + const callback = this.callbackAddress; return core.newLogic({ to, data, callback }); } diff --git a/src/logics/balancer-v2/logic.flash-loan.ts b/src/logics/balancer-v2/logic.flash-loan.ts index 9b2e47a7..054f885d 100644 --- a/src/logics/balancer-v2/logic.flash-loan.ts +++ b/src/logics/balancer-v2/logic.flash-loan.ts @@ -24,6 +24,10 @@ export type FlashLoanLogicFields = core.FlashLoanFields; export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInterface, core.LogicBuilderInterface { static readonly supportedChainIds = supportedChainIds; + get callbackAddress() { + return getContractAddress(this.chainId, 'BalancerV2FlashLoanCallback'); + } + async getTokenList() { const { data } = await axios.get( 'https://raw.githubusercontent.com/balancer/tokenlists/main/generated/listed-old.tokenlist.json' @@ -78,13 +82,13 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt amounts.push(output.amountWei); } const data = Vault__factory.createInterface().encodeFunctionData('flashLoan', [ - getContractAddress(this.chainId, 'BalancerV2FlashLoanCallback'), + this.callbackAddress, assets, amounts, params, ]); - const callback = getContractAddress(this.chainId, 'BalancerV2FlashLoanCallback'); + const callback = this.callbackAddress; return core.newLogic({ to, data, callback }); } diff --git a/src/logics/paraswap-v5/logic.swap-token.ts b/src/logics/paraswap-v5/logic.swap-token.ts index 5bebce13..be939e6c 100644 --- a/src/logics/paraswap-v5/logic.swap-token.ts +++ b/src/logics/paraswap-v5/logic.swap-token.ts @@ -28,12 +28,20 @@ export class SwapTokenLogic async getTokenList() { const tokenListUrls = getTokenListUrls(this.chainId); - const tokenLists = await Promise.all(tokenListUrls.map((tokenListUrl) => axios.get(tokenListUrl))); + const tokenLists: TokenList[] = []; + await Promise.all( + tokenListUrls.map(async (tokenListUrl) => { + try { + const { data } = await axios.get(tokenListUrl); + tokenLists.push(data); + } catch {} + }) + ); const tmp: Record = { [this.nativeToken.address]: true }; const tokenList: SwapTokenLogicTokenList = [this.nativeToken]; - for (const { data } of tokenLists) { - for (const { chainId, address, decimals, symbol, name } of data.tokens) { + for (const { tokens } of tokenLists) { + for (const { chainId, address, decimals, symbol, name } of tokens) { if (tmp[address] || chainId !== this.chainId || !name || !symbol || !decimals) continue; tokenList.push(new common.Token(chainId, address, decimals, symbol, name)); tmp[address] = true; diff --git a/src/logics/utility/index.ts b/src/logics/utility/index.ts index 1a131fd7..79cec3f6 100644 --- a/src/logics/utility/index.ts +++ b/src/logics/utility/index.ts @@ -1,4 +1,5 @@ export * from './logic.custom-data'; +export * from './logic.flash-loan-aggregator'; export * from './logic.multi-send'; export * from './logic.send-token'; export * from './logic.wrapped-native-token'; diff --git a/src/logics/utility/logic.flash-loan-aggregator.test.ts b/src/logics/utility/logic.flash-loan-aggregator.test.ts new file mode 100644 index 00000000..854c212f --- /dev/null +++ b/src/logics/utility/logic.flash-loan-aggregator.test.ts @@ -0,0 +1,15 @@ +import { FlashLoanAggregatorLogic } from './logic.flash-loan-aggregator'; +import * as common from '@protocolink/common'; +import { expect } from 'chai'; + +describe('Utility FlashLoanAggregatorLogic', function () { + context('Test getTokenList', async function () { + FlashLoanAggregatorLogic.supportedChainIds.forEach((chainId) => { + it(`network: ${common.toNetworkId(chainId)}`, async function () { + const logic = new FlashLoanAggregatorLogic(chainId); + const tokenList = await logic.getTokenList(); + expect(tokenList).to.have.lengthOf.above(0); + }); + }); + }); +}); diff --git a/src/logics/utility/logic.flash-loan-aggregator.ts b/src/logics/utility/logic.flash-loan-aggregator.ts new file mode 100644 index 00000000..94563d74 --- /dev/null +++ b/src/logics/utility/logic.flash-loan-aggregator.ts @@ -0,0 +1,98 @@ +import * as aavev2 from '../aave-v2'; +import * as aavev3 from '../aave-v3'; +import * as balancerv2 from '../balancer-v2'; +import * as common from '@protocolink/common'; +import * as core from '@protocolink/core'; + +export const supportedFlashLoanLogics = [aavev2.FlashLoanLogic, aavev3.FlashLoanLogic, balancerv2.FlashLoanLogic]; + +export type FlashLoanAggregatorLogicTokenList = common.Token[]; + +export type FlashLoanAggregatorLogicParams = core.TokensOutFields; + +export type FlashLoanAggregatorLogicQuotation = { + protocolId: string; + loans: common.TokenAmounts; + repays: common.TokenAmounts; + fees: common.TokenAmounts; + feeBps: number; + callback: string; +}; + +export type FlashLoanAggregatorLogicFields = core.FlashLoanFields<{ protocolId: string; referralCode?: number }>; + +@core.LogicDefinitionDecorator() +export class FlashLoanAggregatorLogic + extends core.Logic + implements core.LogicTokenListInterface, core.LogicBuilderInterface +{ + static readonly supportedChainIds = Array.from( + supportedFlashLoanLogics.reduce((accumulator, FlashLoanLogic) => { + for (const chainId of FlashLoanLogic.supportedChainIds) { + accumulator.add(chainId); + } + return accumulator; + }, new Set()) + ); + + async getTokenList() { + const flashLoanLogics = supportedFlashLoanLogics.filter((FlashLoanLogic) => + FlashLoanLogic.supportedChainIds.includes(this.chainId) + ); + const allTokens = await Promise.all( + flashLoanLogics.map((FlashLoanLogic) => { + const flashLoanLogic = new FlashLoanLogic(this.chainId, this.provider); + return flashLoanLogic.getTokenList(); + }) + ); + const tmp: Record = {}; + const tokenList: FlashLoanAggregatorLogicTokenList = []; + for (const tokens of allTokens) { + for (const token of tokens) { + if (tmp[token.address]) continue; + tokenList.push(token); + tmp[token.address] = true; + } + } + + return tokenList; + } + + async quote(params: FlashLoanAggregatorLogicParams) { + const flashLoanLogics = supportedFlashLoanLogics.filter((FlashLoanLogic) => + FlashLoanLogic.supportedChainIds.includes(this.chainId) + ); + + const quotations: FlashLoanAggregatorLogicQuotation[] = []; + await Promise.all( + flashLoanLogics.map(async (FlashLoanLogic) => { + const flashLoanLogic = new FlashLoanLogic(this.chainId, this.provider); + try { + const quotation = await flashLoanLogic.quote(params); + quotations.push({ + protocolId: FlashLoanLogic.protocolId, + callback: flashLoanLogic.callbackAddress, + ...quotation, + }); + } catch {} + }) + ); + + let quotation = quotations[0]; + for (let i = 1; i < quotations.length; i++) { + if (quotations[i].feeBps < quotation.feeBps) { + quotation = quotations[i]; + } + } + + return quotation; + } + + async build(fields: FlashLoanAggregatorLogicFields) { + const { protocolId, ...others } = fields; + const FlashLoanLogic = supportedFlashLoanLogics.find((Logic) => Logic.protocolId === protocolId)!; + const routerLogic = await new FlashLoanLogic(this.chainId, this.provider).build(others); + + return routerLogic; + } +} diff --git a/test/logics/balancer-v2/flash-loan.test.ts b/test/logics/balancer-v2/flash-loan.test.ts index 42f9c9f4..69be8caf 100644 --- a/test/logics/balancer-v2/flash-loan.test.ts +++ b/test/logics/balancer-v2/flash-loan.test.ts @@ -26,15 +26,18 @@ describe('Test BalancerV2 FlashLoan Logic', function () { testCases.forEach(({ outputs }, i) => { it(`case ${i + 1}`, async function () { // 1. get flash loan quotation - const aaveV3FlashLoanLogic = new balancerv2.FlashLoanLogic(chainId); - const { loans, repays, fees } = await aaveV3FlashLoanLogic.quote({ outputs }); + const balancerV2FlashLoanLogic = new balancerv2.FlashLoanLogic(chainId); + const { loans, repays, fees } = await balancerV2FlashLoanLogic.quote({ outputs }); // 2. build funds and router logics for flash loan const funds = new common.TokenAmounts(); const flashLoanRouterLogics: core.IParam.LogicStruct[] = []; const utilitySendTokenLogic = new utility.SendTokenLogic(chainId); for (let i = 0; i < fees.length; i++) { - funds.add(fees.at(i).clone()); + const fee = fees.at(i).clone(); + if (!fee.isZero) { + funds.add(fee); + } flashLoanRouterLogics.push( await utilitySendTokenLogic.build({ input: repays.at(i), @@ -46,9 +49,8 @@ describe('Test BalancerV2 FlashLoan Logic', function () { // 3. build router logics const routerLogics: core.IParam.LogicStruct[] = []; - const userData = core.newCallbackParams(flashLoanRouterLogics); - const balancerV2FlashLoanLogic = new balancerv2.FlashLoanLogic(chainId); - routerLogics.push(await balancerV2FlashLoanLogic.build({ outputs: loans, params: userData })); + const params = core.newCallbackParams(flashLoanRouterLogics); + routerLogics.push(await balancerV2FlashLoanLogic.build({ outputs: loans, params: params })); // 4. send router tx const transactionRequest = core.newRouterExecuteTransactionRequest({ chainId, routerLogics }); diff --git a/test/logics/utility/flash-loan-aggregrator.test.ts b/test/logics/utility/flash-loan-aggregrator.test.ts new file mode 100644 index 00000000..d38f254b --- /dev/null +++ b/test/logics/utility/flash-loan-aggregrator.test.ts @@ -0,0 +1,70 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import * as aavev3 from 'src/logics/aave-v3'; +import { claimToken, getChainId, mainnetTokens, snapshotAndRevertEach } from '@protocolink/test-helpers'; +import * as common from '@protocolink/common'; +import * as core from '@protocolink/core'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import * as utility from 'src/logics/utility'; +import * as utils from 'test/utils'; + +describe('Test Utility FlashLoanAggregator Logic', function () { + let chainId: number; + let user: SignerWithAddress; + + before(async function () { + chainId = await getChainId(); + [, user] = await hre.ethers.getSigners(); + await claimToken(chainId, user.address, aavev3.mainnetTokens['1INCH'], '2'); + await claimToken(chainId, user.address, mainnetTokens.USDC, '2'); + }); + + snapshotAndRevertEach(); + + const testCases = [ + // balancer-v2 + { outputs: new common.TokenAmounts([mainnetTokens.WETH, '1'], [mainnetTokens.USDC, '1']) }, + { outputs: new common.TokenAmounts([mainnetTokens.USDT, '1'], [mainnetTokens.DAI, '1']) }, + // aave-v3 + { outputs: new common.TokenAmounts([aavev3.mainnetTokens['1INCH'], '1'], [aavev3.mainnetTokens.USDC, '1']) }, + ]; + + testCases.forEach(({ outputs }, i) => { + it(`case ${i + 1}`, async function () { + // 1. get flash loan quotation + const utilityFlashLoanAggregatorLogic = new utility.FlashLoanAggregatorLogic(chainId); + const { protocolId, loans, repays, fees, callback } = await utilityFlashLoanAggregatorLogic.quote({ outputs }); + + // 2. build funds and router logics for flash loan + const funds = new common.TokenAmounts(); + const flashLoanRouterLogics: core.IParam.LogicStruct[] = []; + const utilitySendTokenLogic = new utility.SendTokenLogic(chainId); + for (let i = 0; i < fees.length; i++) { + const fee = fees.at(i).clone(); + if (!fee.isZero) { + funds.add(fee); + } + flashLoanRouterLogics.push( + await utilitySendTokenLogic.build({ + input: repays.at(i), + recipient: callback, + }) + ); + } + + // 3. build router logics + let routerLogics: core.IParam.LogicStruct[] = []; + const erc20Funds = funds.erc20; + if (erc20Funds.length > 0) { + routerLogics = await utils.getPermitAndPullTokenRouterLogics(chainId, user, erc20Funds); + } + + const params = core.newCallbackParams(flashLoanRouterLogics); + routerLogics.push(await utilityFlashLoanAggregatorLogic.build({ protocolId, outputs: loans, params })); + + // 4. send router tx + const transactionRequest = core.newRouterExecuteTransactionRequest({ chainId, routerLogics }); + await expect(user.sendTransaction(transactionRequest)).to.not.be.reverted; + }); + }); +});