Skip to content

Commit

Permalink
feat: aave v2 flash loan logic add quote func
Browse files Browse the repository at this point in the history
  • Loading branch information
chouandy committed Aug 1, 2023
1 parent b840675 commit da8f0fc
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-suns-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@protocolink/logics': patch
---

aave v2 flash loan logic add quote func
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions src/logics/aave-v2/logic.flash-loan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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;

Expand Down
21 changes: 21 additions & 0 deletions src/logics/aave-v2/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});
});
});
127 changes: 111 additions & 16 deletions src/logics/aave-v2/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -192,4 +244,47 @@ export class Service extends common.Web3Toolkit {

return { to, data };
}

async getFlashLoanConfiguration(assets: common.Token[]): Promise<FlashLoanConfiguration> {
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 };
}
}
10 changes: 10 additions & 0 deletions src/logics/aave-v2/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
24 changes: 11 additions & 13 deletions test/logics/aave-v2/flash-loan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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()) {
Expand Down
4 changes: 2 additions & 2 deletions test/logics/balancer-v2/flash-loan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
})
Expand Down
6 changes: 3 additions & 3 deletions test/logics/uniswap-v3/swap-token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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({
Expand Down
8 changes: 4 additions & 4 deletions test/logics/utility/custom-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit da8f0fc

Please sign in to comment.