diff --git a/.changeset/fresh-suns-think.md b/.changeset/fresh-suns-think.md new file mode 100644 index 00000000..c2b370e5 --- /dev/null +++ b/.changeset/fresh-suns-think.md @@ -0,0 +1,5 @@ +--- +'@protocolink/logics': patch +--- + +aave v2 flash loan logic add quote func diff --git a/package.json b/package.json index 81642373..2e44a7fd 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "axios-retry": "^3.5.1", "bignumber.js": "^9.1.1", "ethers": "^5.7.2", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "tiny-invariant": "^1.3.1" }, "devDependencies": { "@changesets/cli": "^2.26.1", diff --git a/src/logics/aave-v2/logic.flash-loan.ts b/src/logics/aave-v2/logic.flash-loan.ts index 2a243fbc..aae35182 100644 --- a/src/logics/aave-v2/logic.flash-loan.ts +++ b/src/logics/aave-v2/logic.flash-loan.ts @@ -5,9 +5,12 @@ import { Service } from './service'; import * as common from '@protocolink/common'; import * as core from '@protocolink/core'; import { getContractAddress, supportedChainIds } from './configs'; +import invariant from 'tiny-invariant'; export type FlashLoanLogicTokenList = common.Token[]; +export type FlashLoanLogicParams = core.TokensOutFields; + export type FlashLoanLogicFields = core.FlashLoanFields<{ referralCode?: number }>; @core.LogicDefinitionDecorator() @@ -21,6 +24,31 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt return tokens; } + async quote(params: FlashLoanLogicParams) { + const { outputs: loans } = params; + + const service = new Service(this.chainId, this.provider); + const { feeBps, assetInfos } = await service.getFlashLoanConfiguration(loans.map((loan) => loan.token)); + + const repays = new common.TokenAmounts(); + const fees = new common.TokenAmounts(); + for (let i = 0; i < loans.length; i++) { + const loan = loans.at(i); + const { isActive, avaliableToBorrow } = assetInfos[i]; + invariant(isActive, `asset is not active: ${loan.token.address}`); + invariant(avaliableToBorrow.gte(loan), `insufficient borrowing capacity for the asset: ${loan.token.address}`); + + const feeAmountWei = common.calcFee(loan.amountWei, feeBps); + const fee = new common.TokenAmount(loan.token).setWei(feeAmountWei); + fees.add(fee); + + const repay = loan.clone().add(fee); + repays.add(repay); + } + + return { loans, repays, fees, feeBps }; + } + async build(fields: FlashLoanLogicFields) { const { outputs, params, referralCode = 0 } = fields; diff --git a/src/logics/aave-v2/service.test.ts b/src/logics/aave-v2/service.test.ts index b11b6805..c642a044 100644 --- a/src/logics/aave-v2/service.test.ts +++ b/src/logics/aave-v2/service.test.ts @@ -47,4 +47,25 @@ describe('AaveV2 Service', function () { }); }); }); + + context('Test getFlashLoanConfiguration', function () { + const service = new Service(common.ChainId.mainnet); + + const testCases = [ + { assets: [mainnetTokens.WETH, mainnetTokens.USDC] }, + { assets: [mainnetTokens.WBTC, mainnetTokens.USDT] }, + ]; + + 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) { + expect(assetInfo).to.have.keys('isActive', 'avaliableToBorrow'); + } + }); + }); + }); }); diff --git a/src/logics/aave-v2/service.ts b/src/logics/aave-v2/service.ts index ecca46f4..3373baf6 100644 --- a/src/logics/aave-v2/service.ts +++ b/src/logics/aave-v2/service.ts @@ -3,19 +3,52 @@ import { DebtTokenBase__factory, LendingPoolAddressesProvider__factory, LendingPool__factory, + ProtocolDataProvider, ProtocolDataProvider__factory, } from './contracts'; -import { InterestRateMode, ReserveTokens, ReserveTokensAddress } from './types'; +import { + FlashLoanAssetInfo, + FlashLoanConfiguration, + InterestRateMode, + ReserveTokens, + ReserveTokensAddress, +} from './types'; +import { LendingPoolInterface } from './contracts/LendingPool'; +import { ProtocolDataProviderInterface } from './contracts/ProtocolDataProvider'; import * as common from '@protocolink/common'; import { constants } from 'ethers'; import { getContractAddress } from './configs'; +import invariant from 'tiny-invariant'; export class Service extends common.Web3Toolkit { + private _protocolDataProvider?: ProtocolDataProvider; + get protocolDataProvider() { - return ProtocolDataProvider__factory.connect( - getContractAddress(this.chainId, 'ProtocolDataProvider'), - this.provider - ); + if (!this._protocolDataProvider) { + this._protocolDataProvider = ProtocolDataProvider__factory.connect( + getContractAddress(this.chainId, 'ProtocolDataProvider'), + this.provider + ); + } + return this._protocolDataProvider; + } + + private _protocolDataProviderIface?: ProtocolDataProviderInterface; + + get protocolDataProviderIface() { + if (!this._protocolDataProviderIface) { + this._protocolDataProviderIface = ProtocolDataProvider__factory.createInterface(); + } + return this._protocolDataProviderIface; + } + + private _lendingPoolIface?: LendingPoolInterface; + + get lendingPoolIface() { + if (!this._lendingPoolIface) { + this._lendingPoolIface = LendingPool__factory.createInterface(); + } + return this._lendingPoolIface; } private lendingPoolAddress?: string; @@ -39,17 +72,19 @@ export class Service extends common.Web3Toolkit { const lendingPoolAddress = await this.getLendingPoolAddress(); const assetAddresses = await LendingPool__factory.connect(lendingPoolAddress, this.provider).getReservesList(); - const iface = ProtocolDataProvider__factory.createInterface(); const calls: common.Multicall2.CallStruct[] = assetAddresses.map((assetAddress) => ({ - target: getContractAddress(this.chainId, 'ProtocolDataProvider'), - callData: iface.encodeFunctionData('getReserveConfigurationData', [assetAddress]), + target: this.protocolDataProvider.address, + callData: this.protocolDataProviderIface.encodeFunctionData('getReserveConfigurationData', [assetAddress]), })); const { returnData } = await this.multicall2.callStatic.aggregate(calls); this.assetAddresses = []; for (let i = 0; i < assetAddresses.length; i++) { const assetAddress = assetAddresses[i]; - const { isActive, isFrozen } = iface.decodeFunctionResult('getReserveConfigurationData', returnData[i]); + const { isActive, isFrozen } = this.protocolDataProviderIface.decodeFunctionResult( + 'getReserveConfigurationData', + returnData[i] + ); if (isActive && !isFrozen) this.assetAddresses.push(assetAddress); } } @@ -63,20 +98,17 @@ export class Service extends common.Web3Toolkit { if (!this.reserveTokensAddresses) { const assetAddresses = await this.getAssetAddresses(); - const iface = ProtocolDataProvider__factory.createInterface(); const calls: common.Multicall2.CallStruct[] = assetAddresses.map((asset) => ({ - target: getContractAddress(this.chainId, 'ProtocolDataProvider'), - callData: iface.encodeFunctionData('getReserveTokensAddresses', [asset]), + target: this.protocolDataProvider.address, + callData: this.protocolDataProviderIface.encodeFunctionData('getReserveTokensAddresses', [asset]), })); const { returnData } = await this.multicall2.callStatic.aggregate(calls); this.reserveTokensAddresses = []; for (let i = 0; i < assetAddresses.length; i++) { const assetAddress = assetAddresses[i]; - const { aTokenAddress, stableDebtTokenAddress, variableDebtTokenAddress } = iface.decodeFunctionResult( - 'getReserveTokensAddresses', - returnData[i] - ); + const { aTokenAddress, stableDebtTokenAddress, variableDebtTokenAddress } = + this.protocolDataProviderIface.decodeFunctionResult('getReserveTokensAddresses', returnData[i]); this.reserveTokensAddresses.push({ assetAddress, aTokenAddress, @@ -147,6 +179,26 @@ export class Service extends common.Web3Toolkit { return this.getToken(aTokenAddress); } + async toATokens(assets: common.Token[]) { + const calls: common.Multicall2.CallStruct[] = assets.map((asset) => ({ + target: this.protocolDataProvider.address, + callData: this.protocolDataProviderIface.encodeFunctionData('getReserveTokensAddresses', [asset.wrapped.address]), + })); + const { returnData } = await this.multicall2.callStatic.aggregate(calls); + + const aTokenAddresses: string[] = []; + for (let i = 0; i < assets.length; i++) { + const { aTokenAddress } = this.protocolDataProviderIface.decodeFunctionResult( + 'getReserveTokensAddresses', + returnData[i] + ); + invariant(aTokenAddress !== constants.AddressZero, `unsupported asset: ${assets[i].wrapped.address}`); + aTokenAddresses.push(aTokenAddress); + } + + return this.getTokens(aTokenAddresses); + } + async toAsset(aToken: common.Token) { const assetAddress = await AToken__factory.connect(aToken.address, this.provider).UNDERLYING_ASSET_ADDRESS(); return this.getToken(assetAddress); @@ -192,4 +244,47 @@ export class Service extends common.Web3Toolkit { return { to, data }; } + + async getFlashLoanConfiguration(assets: common.Token[]): Promise { + const aTokens = await this.toATokens(assets); + const poolAddress = await this.getLendingPoolAddress(); + + const calls: common.Multicall2.CallStruct[] = [ + { target: poolAddress, callData: this.lendingPoolIface.encodeFunctionData('FLASHLOAN_PREMIUM_TOTAL') }, + ]; + for (let i = 0; i < assets.length; i++) { + const assetAddress = assets[i].wrapped.address; + calls.push({ + target: this.protocolDataProvider.address, + callData: this.protocolDataProviderIface.encodeFunctionData('getReserveConfigurationData', [assetAddress]), + }); + calls.push({ + target: assetAddress, + callData: this.erc20Iface.encodeFunctionData('balanceOf', [aTokens[i].address]), + }); + } + const { returnData } = await this.multicall2.callStatic.aggregate(calls); + + let j = 0; + const [premium] = this.lendingPoolIface.decodeFunctionResult('FLASHLOAN_PREMIUM_TOTAL', returnData[j]); + const feeBps = premium.toNumber(); + j++; + + const assetInfos: FlashLoanAssetInfo[] = []; + for (let i = 0; i < assets.length; i++) { + const { isActive } = this.protocolDataProviderIface.decodeFunctionResult( + 'getReserveConfigurationData', + returnData[j] + ); + j++; + + const [balance] = this.erc20Iface.decodeFunctionResult('balanceOf', returnData[j]); + const avaliableToBorrow = new common.TokenAmount(assets[i]).setWei(balance); + j++; + + assetInfos.push({ isActive, avaliableToBorrow }); + } + + return { feeBps: feeBps, assetInfos }; + } } diff --git a/src/logics/aave-v2/types.ts b/src/logics/aave-v2/types.ts index 2388357c..93d91134 100644 --- a/src/logics/aave-v2/types.ts +++ b/src/logics/aave-v2/types.ts @@ -19,3 +19,13 @@ export enum InterestRateMode { stable = 1, variable = 2, } + +export interface FlashLoanAssetInfo { + isActive: boolean; + avaliableToBorrow: common.TokenAmount; +} + +export interface FlashLoanConfiguration { + feeBps: number; + assetInfos: FlashLoanAssetInfo[]; +} diff --git a/test/logics/aave-v2/flash-loan.test.ts b/test/logics/aave-v2/flash-loan.test.ts index 2738d98f..20bccaf1 100644 --- a/test/logics/aave-v2/flash-loan.test.ts +++ b/test/logics/aave-v2/flash-loan.test.ts @@ -11,7 +11,6 @@ import * as utils from 'test/utils'; describe('Test AaveV2 FlashLoan Logic', function () { let chainId: number; let user: SignerWithAddress; - let flashLoanPremiumTotal: number; before(async function () { chainId = await getChainId(); @@ -20,9 +19,6 @@ describe('Test AaveV2 FlashLoan Logic', function () { await claimToken(chainId, user.address, mainnetTokens.USDC, '2'); await claimToken(chainId, user.address, mainnetTokens.USDT, '2'); await claimToken(chainId, user.address, mainnetTokens.DAI, '2'); - - const service = new aavev2.Service(chainId, hre.ethers.provider); - flashLoanPremiumTotal = await service.getFlashLoanPremiumTotal(); }); snapshotAndRevertEach(); @@ -34,31 +30,33 @@ describe('Test AaveV2 FlashLoan Logic', function () { testCases.forEach(({ outputs }, i) => { it(`case ${i + 1}`, async function () { - // 1. build funds and router logics for flash loan by flash loan fee + // 1. get flash loan quotation + const logicAaveV3FlashLoan = new aavev2.FlashLoanLogic(chainId); + const { loans, repays, fees } = await logicAaveV3FlashLoan.quote({ outputs }); + + // 2. build funds and router logics for flash loan by flash loan fee const funds = new common.TokenAmounts(); const flashLoanRouterLogics: core.IParam.LogicStruct[] = []; const utilitySendTokenLogic = new utility.SendTokenLogic(chainId); - for (const output of outputs.toArray()) { - const feeWei = common.calcFee(output.amountWei, flashLoanPremiumTotal); - const fund = new common.TokenAmount(output.token).addWei(feeWei); - funds.add(fund); + for (let i = 0; i < fees.length; i++) { + funds.add(fees.at(i).clone()); flashLoanRouterLogics.push( await utilitySendTokenLogic.build({ - input: output.clone().addWei(feeWei), + input: repays.at(i), recipient: aavev2.getContractAddress(chainId, 'AaveV2FlashLoanCallback'), }) ); } - // 2. build router logics + // 3. build router logics const erc20Funds = funds.erc20; const routerLogics = await utils.getPermitAndPullTokenRouterLogics(chainId, user, erc20Funds); const params = core.newCallbackParams(flashLoanRouterLogics); const logicAaveV2FlashLoan = new aavev2.FlashLoanLogic(chainId); - routerLogics.push(await logicAaveV2FlashLoan.build({ outputs, params })); + routerLogics.push(await logicAaveV2FlashLoan.build({ outputs: loans, params })); - // 3. send router tx + // 4. send router tx const transactionRequest = core.newRouterExecuteTransactionRequest({ chainId, routerLogics }); await expect(user.sendTransaction(transactionRequest)).to.not.be.reverted; for (const fund of funds.toArray()) { diff --git a/test/logics/balancer-v2/flash-loan.test.ts b/test/logics/balancer-v2/flash-loan.test.ts index c1b49c9b..5496b969 100644 --- a/test/logics/balancer-v2/flash-loan.test.ts +++ b/test/logics/balancer-v2/flash-loan.test.ts @@ -27,10 +27,10 @@ describe('Test BalancerV2 FlashLoan Logic', function () { it(`case ${i + 1}`, async function () { // 1. build funds and router logics for flash loan const flashLoanRouterLogics: core.IParam.LogicStruct[] = []; - const logicUtilitySendToken = new utility.SendTokenLogic(chainId); + const utilitySendTokenLogic = new utility.SendTokenLogic(chainId); for (const output of outputs.toArray()) { flashLoanRouterLogics.push( - await logicUtilitySendToken.build({ + await utilitySendTokenLogic.build({ input: output, recipient: balancerv2.getContractAddress(chainId, 'BalancerV2FlashLoanCallback'), }) diff --git a/test/logics/uniswap-v3/swap-token.test.ts b/test/logics/uniswap-v3/swap-token.test.ts index ad931e7b..3e0a1bec 100644 --- a/test/logics/uniswap-v3/swap-token.test.ts +++ b/test/logics/uniswap-v3/swap-token.test.ts @@ -91,8 +91,8 @@ describe('Test UniswapV3 SwapToken Logic', function () { testCases.forEach(({ params, balanceBps }, i) => { it(`case ${i + 1}`, async function () { // 1. get input or output - const logicUniswapV3SwapToken = new uniswapv3.SwapTokenLogic(chainId); - const quotation = await logicUniswapV3SwapToken.quote(params); + const uniswapV3SwapTokenLogic = new uniswapv3.SwapTokenLogic(chainId); + const quotation = await uniswapV3SwapTokenLogic.quote(params); const { tradeType, input, output } = quotation; // 2. build funds, tokensReturn @@ -108,7 +108,7 @@ describe('Test UniswapV3 SwapToken Logic', function () { // 3. build router logics const erc20Funds = funds.erc20; const routerLogics = await utils.getPermitAndPullTokenRouterLogics(chainId, user, erc20Funds); - routerLogics.push(await logicUniswapV3SwapToken.build(quotation, { account: user.address })); + routerLogics.push(await uniswapV3SwapTokenLogic.build(quotation, { account: user.address })); // 4. send router tx const transactionRequest = core.newRouterExecuteTransactionRequest({ diff --git a/test/logics/utility/custom-data.test.ts b/test/logics/utility/custom-data.test.ts index 824246e5..155c3d84 100644 --- a/test/logics/utility/custom-data.test.ts +++ b/test/logics/utility/custom-data.test.ts @@ -36,8 +36,8 @@ describe('Test Utility CustomData Logic', function () { // 3. build router logics const routerLogics: core.IParam.LogicStruct[] = []; - const logicUtilityCustomData = new utility.CustomDataLogic(chainId); - routerLogics.push(await logicUtilityCustomData.build({ to, data })); + const utilityCustomDataLogic = new utility.CustomDataLogic(chainId); + routerLogics.push(await utilityCustomDataLogic.build({ to, data })); // 4. send router tx const transactionRequest = core.newRouterExecuteTransactionRequest({ @@ -75,9 +75,9 @@ describe('Test Utility CustomData Logic', function () { const erc20Funds = funds.erc20; const routerLogics = await utils.getPermitAndPullTokenRouterLogics(chainId, user1, erc20Funds); - const logicUtilityCustomData = new utility.CustomDataLogic(chainId); + const utilityCustomDataLogic = new utility.CustomDataLogic(chainId); routerLogics.push( - await logicUtilityCustomData.build({ + await utilityCustomDataLogic.build({ inputs: new common.TokenAmounts(input), outputs: new common.TokenAmounts(output), to: data.tx.to, diff --git a/test/logics/utility/send-token.test.ts b/test/logics/utility/send-token.test.ts index 6cc3702d..c3b74089 100644 --- a/test/logics/utility/send-token.test.ts +++ b/test/logics/utility/send-token.test.ts @@ -46,8 +46,8 @@ describe('Test Utility SendToken Logic', function () { const erc20Funds = funds.erc20; const routerLogics = await utils.getPermitAndPullTokenRouterLogics(chainId, user1, erc20Funds); - const logicUtilitySendToken = new utility.SendTokenLogic(chainId); - routerLogics.push(await logicUtilitySendToken.build({ input, recipient: user2.address })); + const utilitySendTokenLogic = new utility.SendTokenLogic(chainId); + routerLogics.push(await utilitySendTokenLogic.build({ input, recipient: user2.address })); // 3. send router tx const transactionRequest = core.newRouterExecuteTransactionRequest({ diff --git a/test/logics/utility/wrapped-native-token.test.ts b/test/logics/utility/wrapped-native-token.test.ts index 93c41723..9508c51c 100644 --- a/test/logics/utility/wrapped-native-token.test.ts +++ b/test/logics/utility/wrapped-native-token.test.ts @@ -30,8 +30,8 @@ describe('Test Utility WrappedNativeToken Logic', function () { testCases.forEach(({ input, tokenOut, balanceBps }, i) => { it(`case ${i + 1}`, async function () { // 1. get output - const logicUtilityWrappedNativeToken = new utility.WrappedNativeTokenLogic(chainId); - const { output } = logicUtilityWrappedNativeToken.quote({ input, tokenOut }); + const utilityWrappedNativeTokenLogic = new utility.WrappedNativeTokenLogic(chainId); + const { output } = utilityWrappedNativeTokenLogic.quote({ input, tokenOut }); // 2. build funds, tokensReturn const tokensReturn = [output.token.elasticAddress]; @@ -47,7 +47,7 @@ describe('Test Utility WrappedNativeToken Logic', function () { const erc20Funds = funds.erc20; const routerLogics = await utils.getPermitAndPullTokenRouterLogics(chainId, user, erc20Funds); - routerLogics.push(await logicUtilityWrappedNativeToken.build({ input, output, balanceBps })); + routerLogics.push(await utilityWrappedNativeTokenLogic.build({ input, output, balanceBps })); // 4. send router tx const transactionRequest = core.newRouterExecuteTransactionRequest({