From 76a650b7353cd263a356f624d9e6a87c4c62fea5 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 4 Jan 2024 16:56:25 +0800 Subject: [PATCH 1/5] feat: add Morphoblue flashloan --- .changeset/strange-phones-help.md | 5 + .env.goerli | 2 +- .../abis/MorphoFlashLoanCallback.json | 66 +++++++ src/logics/morphoblue/configs.ts | 3 +- .../contracts/MorphoFlashLoanCallback.ts | 178 ++++++++++++++++++ .../MorphoFlashLoanCallback__factory.ts | 150 +++++++++++++++ .../morphoblue/contracts/factories/index.ts | 1 + src/logics/morphoblue/contracts/index.ts | 2 + src/logics/morphoblue/index.ts | 1 + .../morphoblue/logic.flash-loan.test.ts | 49 +++++ src/logics/morphoblue/logic.flash-loan.ts | 124 ++++++++++++ .../utility/logic.flash-loan-aggregator.ts | 2 + test/logics/morphoblue/flash-loan.test.ts | 87 +++++++++ 13 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 .changeset/strange-phones-help.md create mode 100644 src/logics/morphoblue/abis/MorphoFlashLoanCallback.json create mode 100644 src/logics/morphoblue/contracts/MorphoFlashLoanCallback.ts create mode 100644 src/logics/morphoblue/contracts/factories/MorphoFlashLoanCallback__factory.ts create mode 100644 src/logics/morphoblue/logic.flash-loan.test.ts create mode 100644 src/logics/morphoblue/logic.flash-loan.ts create mode 100644 test/logics/morphoblue/flash-loan.test.ts diff --git a/.changeset/strange-phones-help.md b/.changeset/strange-phones-help.md new file mode 100644 index 00000000..d7647b96 --- /dev/null +++ b/.changeset/strange-phones-help.md @@ -0,0 +1,5 @@ +--- +'@protocolink/logics': patch +--- + +add Morphoblue flashloan diff --git a/.env.goerli b/.env.goerli index 6ebb6ebd..44e843ac 100644 --- a/.env.goerli +++ b/.env.goerli @@ -1,3 +1,3 @@ CHAIN_ID=5 HTTP_RPC_URL=https://rpc.ankr.com/eth_goerli -BLOCK_NUMBER=10310000 +BLOCK_NUMBER=10320000 diff --git a/src/logics/morphoblue/abis/MorphoFlashLoanCallback.json b/src/logics/morphoblue/abis/MorphoFlashLoanCallback.json new file mode 100644 index 00000000..398d87a0 --- /dev/null +++ b/src/logics/morphoblue/abis/MorphoFlashLoanCallback.json @@ -0,0 +1,66 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "router_", "type": "address" }, + { "internalType": "address", "name": "morpho_", "type": "address" }, + { "internalType": "uint256", "name": "feeRate_", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [{ "internalType": "address", "name": "token", "type": "address" }], + "name": "InvalidBalance", + "type": "error" + }, + { "inputs": [], "name": "InvalidCaller", "type": "error" }, + { + "inputs": [], + "name": "feeRate", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "assets", "type": "uint256" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "flashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "metadata", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "morpho", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "assets", "type": "uint256" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "onMorphoFlashLoan", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "router", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/logics/morphoblue/configs.ts b/src/logics/morphoblue/configs.ts index 86e454d8..4e328795 100644 --- a/src/logics/morphoblue/configs.ts +++ b/src/logics/morphoblue/configs.ts @@ -1,7 +1,7 @@ import * as common from '@protocolink/common'; import { goerliTokens } from './tokens'; -type ContractNames = 'Morpho'; +type ContractNames = 'Morpho' | 'MorphoFlashLoanCallback'; export interface MarketConfig { id: string; @@ -23,6 +23,7 @@ export const configs: Config[] = [ chainId: common.ChainId.goerli, contract: { Morpho: '0x64c7044050Ba0431252df24fEd4d9635a275CB41', + MorphoFlashLoanCallback: '0x24D5b6b712D1f0D0B628E21E39dBaDde3f28C56e', }, markets: [ { diff --git a/src/logics/morphoblue/contracts/MorphoFlashLoanCallback.ts b/src/logics/morphoblue/contracts/MorphoFlashLoanCallback.ts new file mode 100644 index 00000000..6f972f40 --- /dev/null +++ b/src/logics/morphoblue/contracts/MorphoFlashLoanCallback.ts @@ -0,0 +1,178 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ +import type { + BaseContract, + BigNumber, + BigNumberish, + BytesLike, + CallOverrides, + ContractTransaction, + Overrides, + PopulatedTransaction, + Signer, + utils, +} from 'ethers'; +import type { FunctionFragment, Result } from '@ethersproject/abi'; +import type { Listener, Provider } from '@ethersproject/providers'; +import type { TypedEventFilter, TypedEvent, TypedListener, OnEvent } from './common'; + +export interface MorphoFlashLoanCallbackInterface extends utils.Interface { + functions: { + 'feeRate()': FunctionFragment; + 'flashLoan(address,uint256,bytes)': FunctionFragment; + 'metadata()': FunctionFragment; + 'morpho()': FunctionFragment; + 'onMorphoFlashLoan(uint256,bytes)': FunctionFragment; + 'router()': FunctionFragment; + }; + + getFunction( + nameOrSignatureOrTopic: 'feeRate' | 'flashLoan' | 'metadata' | 'morpho' | 'onMorphoFlashLoan' | 'router' + ): FunctionFragment; + + encodeFunctionData(functionFragment: 'feeRate', values?: undefined): string; + encodeFunctionData(functionFragment: 'flashLoan', values: [string, BigNumberish, BytesLike]): string; + encodeFunctionData(functionFragment: 'metadata', values?: undefined): string; + encodeFunctionData(functionFragment: 'morpho', values?: undefined): string; + encodeFunctionData(functionFragment: 'onMorphoFlashLoan', values: [BigNumberish, BytesLike]): string; + encodeFunctionData(functionFragment: 'router', values?: undefined): string; + + decodeFunctionResult(functionFragment: 'feeRate', data: BytesLike): Result; + decodeFunctionResult(functionFragment: 'flashLoan', data: BytesLike): Result; + decodeFunctionResult(functionFragment: 'metadata', data: BytesLike): Result; + decodeFunctionResult(functionFragment: 'morpho', data: BytesLike): Result; + decodeFunctionResult(functionFragment: 'onMorphoFlashLoan', data: BytesLike): Result; + decodeFunctionResult(functionFragment: 'router', data: BytesLike): Result; + + events: {}; +} + +export interface MorphoFlashLoanCallback extends BaseContract { + connect(signerOrProvider: Signer | Provider | string): this; + attach(addressOrName: string): this; + deployed(): Promise; + + interface: MorphoFlashLoanCallbackInterface; + + queryFilter( + event: TypedEventFilter, + fromBlockOrBlockhash?: string | number | undefined, + toBlock?: string | number | undefined + ): Promise>; + + listeners(eventFilter?: TypedEventFilter): Array>; + listeners(eventName?: string): Array; + removeAllListeners(eventFilter: TypedEventFilter): this; + removeAllListeners(eventName?: string): this; + off: OnEvent; + on: OnEvent; + once: OnEvent; + removeListener: OnEvent; + + functions: { + feeRate(overrides?: CallOverrides): Promise<[BigNumber]>; + + flashLoan( + token: string, + assets: BigNumberish, + data: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + metadata(overrides?: CallOverrides): Promise<[string]>; + + morpho(overrides?: CallOverrides): Promise<[string]>; + + onMorphoFlashLoan( + assets: BigNumberish, + data: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + router(overrides?: CallOverrides): Promise<[string]>; + }; + + feeRate(overrides?: CallOverrides): Promise; + + flashLoan( + token: string, + assets: BigNumberish, + data: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + metadata(overrides?: CallOverrides): Promise; + + morpho(overrides?: CallOverrides): Promise; + + onMorphoFlashLoan( + assets: BigNumberish, + data: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + router(overrides?: CallOverrides): Promise; + + callStatic: { + feeRate(overrides?: CallOverrides): Promise; + + flashLoan(token: string, assets: BigNumberish, data: BytesLike, overrides?: CallOverrides): Promise; + + metadata(overrides?: CallOverrides): Promise; + + morpho(overrides?: CallOverrides): Promise; + + onMorphoFlashLoan(assets: BigNumberish, data: BytesLike, overrides?: CallOverrides): Promise; + + router(overrides?: CallOverrides): Promise; + }; + + filters: {}; + + estimateGas: { + feeRate(overrides?: CallOverrides): Promise; + + flashLoan( + token: string, + assets: BigNumberish, + data: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + metadata(overrides?: CallOverrides): Promise; + + morpho(overrides?: CallOverrides): Promise; + + onMorphoFlashLoan( + assets: BigNumberish, + data: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + router(overrides?: CallOverrides): Promise; + }; + + populateTransaction: { + feeRate(overrides?: CallOverrides): Promise; + + flashLoan( + token: string, + assets: BigNumberish, + data: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + metadata(overrides?: CallOverrides): Promise; + + morpho(overrides?: CallOverrides): Promise; + + onMorphoFlashLoan( + assets: BigNumberish, + data: BytesLike, + overrides?: Overrides & { from?: string } + ): Promise; + + router(overrides?: CallOverrides): Promise; + }; +} diff --git a/src/logics/morphoblue/contracts/factories/MorphoFlashLoanCallback__factory.ts b/src/logics/morphoblue/contracts/factories/MorphoFlashLoanCallback__factory.ts new file mode 100644 index 00000000..1dda8a6e --- /dev/null +++ b/src/logics/morphoblue/contracts/factories/MorphoFlashLoanCallback__factory.ts @@ -0,0 +1,150 @@ +/* Autogenerated file. Do not edit manually. */ +/* tslint:disable */ +/* eslint-disable */ + +import { Contract, Signer, utils } from 'ethers'; +import type { Provider } from '@ethersproject/providers'; +import type { MorphoFlashLoanCallback, MorphoFlashLoanCallbackInterface } from '../MorphoFlashLoanCallback'; + +const _abi = [ + { + inputs: [ + { + internalType: 'address', + name: 'router_', + type: 'address', + }, + { + internalType: 'address', + name: 'morpho_', + type: 'address', + }, + { + internalType: 'uint256', + name: 'feeRate_', + type: 'uint256', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + ], + name: 'InvalidBalance', + type: 'error', + }, + { + inputs: [], + name: 'InvalidCaller', + type: 'error', + }, + { + inputs: [], + name: 'feeRate', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + { + internalType: 'uint256', + name: 'assets', + type: 'uint256', + }, + { + internalType: 'bytes', + name: 'data', + type: 'bytes', + }, + ], + name: 'flashLoan', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'metadata', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'morpho', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'assets', + type: 'uint256', + }, + { + internalType: 'bytes', + name: 'data', + type: 'bytes', + }, + ], + name: 'onMorphoFlashLoan', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'router', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, +] as const; + +export class MorphoFlashLoanCallback__factory { + static readonly abi = _abi; + static createInterface(): MorphoFlashLoanCallbackInterface { + return new utils.Interface(_abi) as MorphoFlashLoanCallbackInterface; + } + static connect(address: string, signerOrProvider: Signer | Provider): MorphoFlashLoanCallback { + return new Contract(address, _abi, signerOrProvider) as MorphoFlashLoanCallback; + } +} diff --git a/src/logics/morphoblue/contracts/factories/index.ts b/src/logics/morphoblue/contracts/factories/index.ts index b5a136b1..6016fc49 100644 --- a/src/logics/morphoblue/contracts/factories/index.ts +++ b/src/logics/morphoblue/contracts/factories/index.ts @@ -2,3 +2,4 @@ /* tslint:disable */ /* eslint-disable */ export { Morpho__factory } from './Morpho__factory'; +export { MorphoFlashLoanCallback__factory } from './MorphoFlashLoanCallback__factory'; diff --git a/src/logics/morphoblue/contracts/index.ts b/src/logics/morphoblue/contracts/index.ts index cc50de2e..4871c1db 100644 --- a/src/logics/morphoblue/contracts/index.ts +++ b/src/logics/morphoblue/contracts/index.ts @@ -2,5 +2,7 @@ /* tslint:disable */ /* eslint-disable */ export type { Morpho } from './Morpho'; +export type { MorphoFlashLoanCallback } from './MorphoFlashLoanCallback'; export * as factories from './factories'; export { Morpho__factory } from './factories/Morpho__factory'; +export { MorphoFlashLoanCallback__factory } from './factories/MorphoFlashLoanCallback__factory'; diff --git a/src/logics/morphoblue/index.ts b/src/logics/morphoblue/index.ts index 2b2a60ea..2a3743fe 100644 --- a/src/logics/morphoblue/index.ts +++ b/src/logics/morphoblue/index.ts @@ -3,6 +3,7 @@ export * from './service'; export * from './tokens'; export * from './contracts'; export * from './logic.borrow'; +export * from './logic.flash-loan'; export * from './logic.supply'; export * from './logic.supply-collateral'; export * from './logic.repay'; diff --git a/src/logics/morphoblue/logic.flash-loan.test.ts b/src/logics/morphoblue/logic.flash-loan.test.ts new file mode 100644 index 00000000..98c044fd --- /dev/null +++ b/src/logics/morphoblue/logic.flash-loan.test.ts @@ -0,0 +1,49 @@ +import { FlashLoanLogic, FlashLoanLogicFields } from './logic.flash-loan'; +import { LogicTestCase } from 'test/types'; +import { Morpho__factory } from './contracts'; +import * as common from '@protocolink/common'; +import { constants, utils } from 'ethers'; +import { expect } from 'chai'; +import { getContractAddress } from './configs'; +import { goerliTokens } from './tokens'; + +describe('Morphoblue FlashLoanLogic', function () { + context('Test getTokenList', async function () { + FlashLoanLogic.supportedChainIds.forEach((chainId) => { + it(`network: ${common.toNetworkId(chainId)}`, async function () { + const logic = new FlashLoanLogic(chainId); + const tokenList = await logic.getTokenList(); + expect(tokenList).to.have.lengthOf.above(0); + }); + }); + }); + + context('Test build', function () { + const chainId = common.ChainId.goerli; + const logic = new FlashLoanLogic(chainId); + const iface = Morpho__factory.createInterface(); + + const testCases: LogicTestCase[] = [ + { + fields: { + loans: new common.TokenAmounts([goerliTokens.WETH, '1']), + params: '0x', + }, + }, + ]; + + testCases.forEach(({ fields }) => { + it(`flash loan ${fields.loans.map((loan) => loan.token.symbol).join(',')}`, async function () { + const routerLogic = await logic.build(fields); + const sig = routerLogic.data.substring(0, 10); + + expect(routerLogic.to).to.eq(getContractAddress(chainId, 'MorphoFlashLoanCallback')); + expect(utils.isBytesLike(routerLogic.data)).to.be.true; + expect(sig).to.eq(iface.getSighash('flashLoan')); + expect(routerLogic.inputs).to.deep.eq([]); + expect(routerLogic.approveTo).to.eq(constants.AddressZero); + expect(routerLogic.callback).to.eq(getContractAddress(chainId, 'MorphoFlashLoanCallback')); + }); + }); + }); +}); diff --git a/src/logics/morphoblue/logic.flash-loan.ts b/src/logics/morphoblue/logic.flash-loan.ts new file mode 100644 index 00000000..3e83e340 --- /dev/null +++ b/src/logics/morphoblue/logic.flash-loan.ts @@ -0,0 +1,124 @@ +import { MorphoFlashLoanCallback__factory, Morpho__factory } from './contracts'; +import { Service } from './service'; +import * as common from '@protocolink/common'; +import * as core from '@protocolink/core'; +import { getContractAddress, getMarkets, supportedChainIds } from './configs'; +import invariant from 'tiny-invariant'; + +export type FlashLoanLogicTokenList = common.Token[]; + +export type FlashLoanLogicParams = core.FlashLoanParams; + +export type FlashLoanLogicQuotation = core.FlashLoanQuotation; + +export type FlashLoanLogicFields = core.FlashLoanFields; + +export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInterface, core.LogicBuilderInterface { + static id = 'flash-loan'; + static protocolId = 'morphoblue'; + static readonly supportedChainIds = supportedChainIds; + + get callbackAddress() { + return getContractAddress(this.chainId, 'MorphoFlashLoanCallback'); + } + + async calcCallbackFee(loan: common.TokenAmount) { + const callback = MorphoFlashLoanCallback__factory.connect(this.callbackAddress, this.provider); + const feeRate = await callback.feeRate(); + const callbackFee = new common.TokenAmount(loan.token).setWei(common.calcFee(loan.amountWei, feeRate.toNumber())); + + return callbackFee; + } + + async getTokenList() { + const tokenList: FlashLoanLogicTokenList = []; + const service = new Service(this.chainId, this.provider); + + const markets = getMarkets(this.chainId); + + for (const market of markets) { + const loanTokens = await service.getLoanTokens(market.id); + tokenList.push(...loanTokens!); + } + + return tokenList; + } + + async quote(params: FlashLoanLogicParams) { + const assets = core.isFlashLoanLoanParams(params) + ? params.loans.map(({ token }) => token) + : params.repays.map(({ token }) => token); + invariant(assets.length === 1, 'flashLoan more than one token'); + + const morphoAddress = getContractAddress(this.chainId, 'Morpho'); + const asset = assets[0]; + const calls: common.Multicall3.CallStruct[] = [ + { + target: asset.address, + callData: this.erc20Iface.encodeFunctionData('balanceOf', [morphoAddress]), + }, + ]; + + const { returnData } = await this.multicall3.callStatic.aggregate(calls); + + let j = 0; + const feeBps = 0; + + let loans: common.TokenAmounts; + let repays: common.TokenAmounts; + if (core.isFlashLoanLoanParams(params)) { + ({ loans } = params); + + repays = new common.TokenAmounts(); + for (let i = 0; i < loans.length; i++) { + const loan = loans.at(i); + + const [balance] = this.erc20Iface.decodeFunctionResult('balanceOf', returnData[j]); + const availableToBorrow = new common.TokenAmount(loan.token).setWei(balance); + invariant(availableToBorrow.gte(loan), `insufficient borrowing capacity for the asset: ${loan.token.address}`); + j++; + + const feeAmountWei = common.calcFee(loan.amountWei, feeBps); + const fee = new common.TokenAmount(loan.token).setWei(feeAmountWei); + const repay = loan.clone().add(fee); + repays.add(repay); + } + } else { + loans = new common.TokenAmounts(); + repays = new common.TokenAmounts(); + for (let i = 0; i < params.repays.length; i++) { + const repay = params.repays.at(i); + + const loanAmountWei = common.reverseAmountWithFee(repay.amountWei, feeBps); + const loan = new common.TokenAmount(repay.token).setWei(loanAmountWei); + loans.add(loan); + + const [balance] = this.erc20Iface.decodeFunctionResult('balanceOf', returnData[j]); + const availableToBorrow = new common.TokenAmount(loan.token).setWei(balance); + invariant(availableToBorrow.gte(loan), `insufficient borrowing capacity for the asset: ${loan.token.address}`); + j++; + + const feeAmountWei = common.calcFee(loan.amountWei, feeBps); + const fee = new common.TokenAmount(loan.token).setWei(feeAmountWei); + repays.add(loan.clone().add(fee)); + } + } + + const quotation: FlashLoanLogicQuotation = { loans, repays, feeBps }; + + return quotation; + } + + async build(fields: FlashLoanLogicFields) { + const { loans, params } = fields; + + const to = this.callbackAddress; + const loan = loans.toArray()[0]; + const asset = loan.token.address; + const amount = loan.amountWei; + const data = Morpho__factory.createInterface().encodeFunctionData('flashLoan', [asset, amount, params]); + const callback = this.callbackAddress; + + return core.newLogic({ to, data, callback }); + } +} diff --git a/src/logics/utility/logic.flash-loan-aggregator.ts b/src/logics/utility/logic.flash-loan-aggregator.ts index 5bce4802..ec4a63f3 100644 --- a/src/logics/utility/logic.flash-loan-aggregator.ts +++ b/src/logics/utility/logic.flash-loan-aggregator.ts @@ -4,6 +4,7 @@ import * as balancerv2 from '../balancer-v2'; import * as common from '@protocolink/common'; import * as core from '@protocolink/core'; import invariant from 'tiny-invariant'; +import * as morphoblue from '../morphoblue'; import * as radiantv2 from '../radiant-v2'; import * as spark from '../spark'; @@ -11,6 +12,7 @@ export const supportedFlashLoanLogics = [ aavev2.FlashLoanLogic, aavev3.FlashLoanLogic, balancerv2.FlashLoanLogic, + morphoblue.FlashLoanLogic, radiantv2.FlashLoanLogic, spark.FlashLoanLogic, ]; diff --git a/test/logics/morphoblue/flash-loan.test.ts b/test/logics/morphoblue/flash-loan.test.ts new file mode 100644 index 00000000..2171bc1e --- /dev/null +++ b/test/logics/morphoblue/flash-loan.test.ts @@ -0,0 +1,87 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { claimToken, getChainId, 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 morphoblue from 'src/logics/morphoblue'; +import * as utility from 'src/logics/utility'; +import * as utils from 'test/utils'; + +describe('goerli: Test Morphoblue FlashLoan Logic', function () { + let chainId: number; + let user: SignerWithAddress; + + before(async function () { + chainId = await getChainId(); + [, user] = await hre.ethers.getSigners(); + + await claimToken( + chainId, + user.address, + morphoblue.goerliTokens.WETH, + '2', + '0x88124Ef4A9EC47e691F254F2E8e348fd1e341e9B' + ); + await claimToken( + chainId, + user.address, + morphoblue.goerliTokens.USDC, + '2', + '0x64c7044050Ba0431252df24fEd4d9635a275CB41' + ); + }); + + snapshotAndRevertEach(); + + const testCases = [ + { loans: new common.TokenAmounts([morphoblue.goerliTokens.WETH, '1']) }, + { repays: new common.TokenAmounts([morphoblue.goerliTokens.WETH, '1']) }, + { loans: new common.TokenAmounts([morphoblue.goerliTokens.USDC, '1']) }, + { repays: new common.TokenAmounts([morphoblue.goerliTokens.USDC, '1']) }, + ]; + + testCases.forEach((params, i) => { + it(`case ${i + 1}`, async function () { + // 1. get flash loan quotation + const morphoblueFlashLoanLogic = new morphoblue.FlashLoanLogic(chainId); + const { loans, repays } = await morphoblueFlashLoanLogic.quote(params); + + // 2. build funds and router logics for flash loan + const funds = new common.TokenAmounts(); + const flashLoanRouterLogics: core.DataType.LogicStruct[] = []; + const utilitySendTokenLogic = new utility.SendTokenLogic(chainId); + for (let i = 0; i < repays.length; i++) { + const loan = loans.at(i); + const repay = repays.at(i); + + const fee = repay.clone().sub(loan); + funds.add(fee); + + const callbackFee = await morphoblueFlashLoanLogic.calcCallbackFee(loan); + funds.add(callbackFee); + repay.add(callbackFee); + + flashLoanRouterLogics.push( + await utilitySendTokenLogic.build({ + input: repay, + recipient: morphoblue.getContractAddress(chainId, 'MorphoFlashLoanCallback'), + }) + ); + } + + // 3. build router logics + const routerLogics: core.DataType.LogicStruct[] = []; + const callbackParams = core.newCallbackParams(flashLoanRouterLogics); + routerLogics.push(await morphoblueFlashLoanLogic.build({ loans, params: callbackParams })); + + // 4. get router permit2 datas + const permit2Datas = await utils.getRouterPermit2Datas(chainId, user, funds.erc20); + + // 5. send router tx + const routerKit = new core.RouterKit(chainId); + const transactionRequest = routerKit.buildExecuteTransactionRequest({ permit2Datas, routerLogics }); + await expect(user.sendTransaction(transactionRequest)).to.not.be.reverted; + }); + }); +}); From 94f17c3de9727eced199e146bdbe6737afe22d67 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 5 Jan 2024 10:06:51 +0800 Subject: [PATCH 2/5] fix: tokenlist and simplify flashloan buid --- src/logics/morphoblue/logic.flash-loan.ts | 59 +++++++++++------------ 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/src/logics/morphoblue/logic.flash-loan.ts b/src/logics/morphoblue/logic.flash-loan.ts index 3e83e340..67fda654 100644 --- a/src/logics/morphoblue/logic.flash-loan.ts +++ b/src/logics/morphoblue/logic.flash-loan.ts @@ -37,11 +37,14 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt const markets = getMarkets(this.chainId); for (const market of markets) { - const loanTokens = await service.getLoanTokens(market.id); - tokenList.push(...loanTokens!); - } + const tokens = await service.getMarketTokens(market.id); - return tokenList; + for (const token of tokens) { + tokenList.push(token.loanToken, token.collateralToken); + } + } + const tokenSet = new Set(tokenList.map((token) => token)); + return [...tokenSet]; } async quote(params: FlashLoanLogicParams) { @@ -60,8 +63,6 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt ]; const { returnData } = await this.multicall3.callStatic.aggregate(calls); - - let j = 0; const feeBps = 0; let loans: common.TokenAmounts; @@ -70,38 +71,32 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt ({ loans } = params); repays = new common.TokenAmounts(); - for (let i = 0; i < loans.length; i++) { - const loan = loans.at(i); - - const [balance] = this.erc20Iface.decodeFunctionResult('balanceOf', returnData[j]); - const availableToBorrow = new common.TokenAmount(loan.token).setWei(balance); - invariant(availableToBorrow.gte(loan), `insufficient borrowing capacity for the asset: ${loan.token.address}`); - j++; - - const feeAmountWei = common.calcFee(loan.amountWei, feeBps); - const fee = new common.TokenAmount(loan.token).setWei(feeAmountWei); - const repay = loan.clone().add(fee); - repays.add(repay); - } + const loan = loans.at(0); + + const [balance] = this.erc20Iface.decodeFunctionResult('balanceOf', returnData[0]); + const availableToBorrow = new common.TokenAmount(loan.token).setWei(balance); + invariant(availableToBorrow.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); + const repay = loan.clone().add(fee); + repays.add(repay); } else { loans = new common.TokenAmounts(); repays = new common.TokenAmounts(); - for (let i = 0; i < params.repays.length; i++) { - const repay = params.repays.at(i); + const repay = params.repays.at(0); - const loanAmountWei = common.reverseAmountWithFee(repay.amountWei, feeBps); - const loan = new common.TokenAmount(repay.token).setWei(loanAmountWei); - loans.add(loan); + const loanAmountWei = common.reverseAmountWithFee(repay.amountWei, feeBps); + const loan = new common.TokenAmount(repay.token).setWei(loanAmountWei); + loans.add(loan); - const [balance] = this.erc20Iface.decodeFunctionResult('balanceOf', returnData[j]); - const availableToBorrow = new common.TokenAmount(loan.token).setWei(balance); - invariant(availableToBorrow.gte(loan), `insufficient borrowing capacity for the asset: ${loan.token.address}`); - j++; + const [balance] = this.erc20Iface.decodeFunctionResult('balanceOf', returnData[0]); + const availableToBorrow = new common.TokenAmount(loan.token).setWei(balance); + invariant(availableToBorrow.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); - repays.add(loan.clone().add(fee)); - } + const feeAmountWei = common.calcFee(loan.amountWei, feeBps); + const fee = new common.TokenAmount(loan.token).setWei(feeAmountWei); + repays.add(loan.clone().add(fee)); } const quotation: FlashLoanLogicQuotation = { loans, repays, feeBps }; From 343afdb523000bee2fded2d9265a12086d4fca0a Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 5 Jan 2024 11:29:47 +0800 Subject: [PATCH 3/5] fix: flashloan tokenlist and get market tokens --- src/logics/morphoblue/logic.borrow.ts | 10 ++-- src/logics/morphoblue/logic.flash-loan.ts | 14 +++-- src/logics/morphoblue/logic.repay.ts | 10 ++-- .../morphoblue/logic.supply-collateral.ts | 10 ++-- src/logics/morphoblue/logic.supply.ts | 10 ++-- .../morphoblue/logic.withdraw-collateral.ts | 10 ++-- src/logics/morphoblue/logic.withdraw.ts | 10 ++-- src/logics/morphoblue/service.ts | 52 +++++++------------ src/logics/morphoblue/types.ts | 6 +-- 9 files changed, 53 insertions(+), 79 deletions(-) diff --git a/src/logics/morphoblue/logic.borrow.ts b/src/logics/morphoblue/logic.borrow.ts index 7b2c7e91..75e68ee8 100644 --- a/src/logics/morphoblue/logic.borrow.ts +++ b/src/logics/morphoblue/logic.borrow.ts @@ -21,12 +21,10 @@ export class BorrowLogic extends core.Logic implements core.LogicTokenListInterf const markets = getMarkets(this.chainId); for (const market of markets) { - const loanTokens = await service.getLoanTokens(market.id); - for (const loanToken of loanTokens!) { - tokenList[market.id] = []; - if (loanToken.isWrapped) tokenList[market.id].push(loanToken.unwrapped); - tokenList[market.id].push(loanToken); - } + const loanToken = await service.getLoanToken(market.id); + tokenList[market.id] = []; + if (loanToken.isWrapped) tokenList[market.id].push(loanToken.unwrapped); + tokenList[market.id].push(loanToken); } return tokenList; diff --git a/src/logics/morphoblue/logic.flash-loan.ts b/src/logics/morphoblue/logic.flash-loan.ts index 67fda654..65cf00db 100644 --- a/src/logics/morphoblue/logic.flash-loan.ts +++ b/src/logics/morphoblue/logic.flash-loan.ts @@ -32,19 +32,23 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt async getTokenList() { const tokenList: FlashLoanLogicTokenList = []; + const tokenSet = new Set(); const service = new Service(this.chainId, this.provider); const markets = getMarkets(this.chainId); for (const market of markets) { const tokens = await service.getMarketTokens(market.id); + tokenSet.add(tokens.loanToken); + tokenSet.add(tokens.collateralToken); + } - for (const token of tokens) { - tokenList.push(token.loanToken, token.collateralToken); - } + const tokenAddressList = [...tokenSet]; + for (const tokenAddress of tokenAddressList) { + tokenList.push(await service.getToken(tokenAddress)); } - const tokenSet = new Set(tokenList.map((token) => token)); - return [...tokenSet]; + + return tokenList; } async quote(params: FlashLoanLogicParams) { diff --git a/src/logics/morphoblue/logic.repay.ts b/src/logics/morphoblue/logic.repay.ts index 0f48d854..0c7b5023 100644 --- a/src/logics/morphoblue/logic.repay.ts +++ b/src/logics/morphoblue/logic.repay.ts @@ -21,12 +21,10 @@ export class RepayLogic extends core.Logic implements core.LogicTokenListInterfa const markets = getMarkets(this.chainId); for (const market of markets) { - const loanTokens = await service.getLoanTokens(market.id); - for (const loanToken of loanTokens!) { - tokenList[market.id] = []; - if (loanToken.isWrapped) tokenList[market.id].push(loanToken.unwrapped); - tokenList[market.id].push(loanToken); - } + const loanToken = await service.getLoanToken(market.id); + tokenList[market.id] = []; + if (loanToken.isWrapped) tokenList[market.id].push(loanToken.unwrapped); + tokenList[market.id].push(loanToken); } return tokenList; diff --git a/src/logics/morphoblue/logic.supply-collateral.ts b/src/logics/morphoblue/logic.supply-collateral.ts index 78d9b6e5..dea7915e 100644 --- a/src/logics/morphoblue/logic.supply-collateral.ts +++ b/src/logics/morphoblue/logic.supply-collateral.ts @@ -24,12 +24,10 @@ export class SupplyCollateralLogic const markets = getMarkets(this.chainId); for (const market of markets) { - const collateralTokens = await service.getCollateralTokens(market.id); - for (const collateralToken of collateralTokens!) { - tokenList[market.id] = []; - if (collateralToken.isWrapped) tokenList[market.id].push(collateralToken.unwrapped); - tokenList[market.id].push(collateralToken); - } + const collateralToken = await service.getCollateralToken(market.id); + tokenList[market.id] = []; + if (collateralToken.isWrapped) tokenList[market.id].push(collateralToken.unwrapped); + tokenList[market.id].push(collateralToken); } return tokenList; diff --git a/src/logics/morphoblue/logic.supply.ts b/src/logics/morphoblue/logic.supply.ts index ae743320..c3329d12 100644 --- a/src/logics/morphoblue/logic.supply.ts +++ b/src/logics/morphoblue/logic.supply.ts @@ -21,12 +21,10 @@ export class SupplyLogic extends core.Logic implements core.LogicTokenListInterf const markets = getMarkets(this.chainId); for (const market of markets) { - const loanTokens = await service.getLoanTokens(market.id); - for (const loanToken of loanTokens!) { - tokenList[market.id] = []; - if (loanToken.isWrapped) tokenList[market.id].push(loanToken.unwrapped); - tokenList[market.id].push(loanToken); - } + const loanToken = await service.getLoanToken(market.id); + tokenList[market.id] = []; + if (loanToken.isWrapped) tokenList[market.id].push(loanToken.unwrapped); + tokenList[market.id].push(loanToken); } return tokenList; diff --git a/src/logics/morphoblue/logic.withdraw-collateral.ts b/src/logics/morphoblue/logic.withdraw-collateral.ts index 6bfb372a..4597c994 100644 --- a/src/logics/morphoblue/logic.withdraw-collateral.ts +++ b/src/logics/morphoblue/logic.withdraw-collateral.ts @@ -24,12 +24,10 @@ export class WithdrawCollateralLogic const markets = getMarkets(this.chainId); for (const market of markets) { - const collateralTokens = await service.getCollateralTokens(market.id); - for (const collateralToken of collateralTokens!) { - tokenList[market.id] = []; - if (collateralToken.isWrapped) tokenList[market.id].push(collateralToken.unwrapped); - tokenList[market.id].push(collateralToken); - } + const collateralToken = await service.getCollateralToken(market.id); + tokenList[market.id] = []; + if (collateralToken.isWrapped) tokenList[market.id].push(collateralToken.unwrapped); + tokenList[market.id].push(collateralToken); } return tokenList; diff --git a/src/logics/morphoblue/logic.withdraw.ts b/src/logics/morphoblue/logic.withdraw.ts index a3fba143..a7177d7c 100644 --- a/src/logics/morphoblue/logic.withdraw.ts +++ b/src/logics/morphoblue/logic.withdraw.ts @@ -21,12 +21,10 @@ export class WithdrawLogic extends core.Logic implements core.LogicTokenListInte const markets = getMarkets(this.chainId); for (const market of markets) { - const loanTokens = await service.getLoanTokens(market.id); - for (const loanToken of loanTokens!) { - tokenList[market.id] = []; - if (loanToken.isWrapped) tokenList[market.id].push(loanToken.unwrapped); - tokenList[market.id].push(loanToken); - } + const loanToken = await service.getLoanToken(market.id); + tokenList[market.id] = []; + if (loanToken.isWrapped) tokenList[market.id].push(loanToken.unwrapped); + tokenList[market.id].push(loanToken); } return tokenList; diff --git a/src/logics/morphoblue/service.ts b/src/logics/morphoblue/service.ts index 02a3399f..d1cbc6c3 100644 --- a/src/logics/morphoblue/service.ts +++ b/src/logics/morphoblue/service.ts @@ -25,50 +25,34 @@ export class Service extends common.Web3Toolkit { return this._morphoIface; } - private loanTokens?: common.Token[]; - async getLoanTokens(marketId: string) { - if (!this.loanTokens) { - await this.getMarketTokens(marketId); - } - return this.loanTokens; + async getLoanToken(marketId: string) { + const tokens = await this.getMarketTokens(marketId); + const loanToken = await this.getToken(tokens.loanToken); + return loanToken; } - private collateralTokens?: common.Token[]; - async getCollateralTokens(marketId: string) { - if (!this.collateralTokens) { - await this.getMarketTokens(marketId); - } - return this.collateralTokens; + async getCollateralToken(marketId: string) { + const tokens = await this.getMarketTokens(marketId); + const collateralToken = await this.getToken(tokens.collateralToken); + return collateralToken; } async getMarket(marketId: string) { return getMarket(this.chainId, marketId); } - private tokens?: Tokens[]; async getMarketTokens(marketId: string) { - if (!this.tokens) { - const calls: common.Multicall3.CallStruct[] = []; - calls.push({ - target: this.morpho.address, - callData: this.morphoIface.encodeFunctionData('idToMarketParams', [marketId]), - }); - const { returnData } = await this.multicall3.callStatic.aggregate(calls); - - this.tokens = []; - this.loanTokens = []; - this.collateralTokens = []; - let { loanToken, collateralToken } = this.morphoIface.decodeFunctionResult('idToMarketParams', returnData[0]); - - loanToken = await this.getToken(loanToken); - collateralToken = await this.getToken(collateralToken); - - this.tokens.push({ loanToken, collateralToken }); - this.loanTokens.push(loanToken); - this.collateralTokens.push(collateralToken); - } + const calls: common.Multicall3.CallStruct[] = []; + calls.push({ + target: this.morpho.address, + callData: this.morphoIface.encodeFunctionData('idToMarketParams', [marketId]), + }); + const { returnData } = await this.multicall3.callStatic.aggregate(calls); + + const { loanToken, collateralToken } = this.morphoIface.decodeFunctionResult('idToMarketParams', returnData[0]); - return this.tokens; + const tokens: Tokens = { loanToken, collateralToken }; + return tokens; } async getSupplyBalance(marketId: string, account: string) { diff --git a/src/logics/morphoblue/types.ts b/src/logics/morphoblue/types.ts index 37ad0686..c9678060 100644 --- a/src/logics/morphoblue/types.ts +++ b/src/logics/morphoblue/types.ts @@ -1,6 +1,4 @@ -import * as common from '@protocolink/common'; - export interface Tokens { - loanToken: common.Token; - collateralToken: common.Token; + loanToken: string; + collateralToken: string; } From 6f947d91e52820f6537fb637b72d5fe87ad79af3 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 5 Jan 2024 11:56:12 +0800 Subject: [PATCH 4/5] fix: simplify token list get tokens --- src/logics/morphoblue/logic.flash-loan.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/logics/morphoblue/logic.flash-loan.ts b/src/logics/morphoblue/logic.flash-loan.ts index 65cf00db..da6721c7 100644 --- a/src/logics/morphoblue/logic.flash-loan.ts +++ b/src/logics/morphoblue/logic.flash-loan.ts @@ -31,23 +31,17 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt } async getTokenList() { - const tokenList: FlashLoanLogicTokenList = []; const tokenSet = new Set(); const service = new Service(this.chainId, this.provider); const markets = getMarkets(this.chainId); - for (const market of markets) { const tokens = await service.getMarketTokens(market.id); tokenSet.add(tokens.loanToken); tokenSet.add(tokens.collateralToken); } - const tokenAddressList = [...tokenSet]; - for (const tokenAddress of tokenAddressList) { - tokenList.push(await service.getToken(tokenAddress)); - } - + const tokenList: FlashLoanLogicTokenList = await service.getTokens([...tokenSet]); return tokenList; } From c97eb5c7d67b8f989f5c3fbd317b8d03768580e3 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 5 Jan 2024 13:43:27 +0800 Subject: [PATCH 5/5] fix: token address naming --- src/logics/morphoblue/logic.flash-loan.ts | 8 ++++---- src/logics/morphoblue/service.ts | 8 ++++---- src/logics/morphoblue/types.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/logics/morphoblue/logic.flash-loan.ts b/src/logics/morphoblue/logic.flash-loan.ts index da6721c7..e75552da 100644 --- a/src/logics/morphoblue/logic.flash-loan.ts +++ b/src/logics/morphoblue/logic.flash-loan.ts @@ -31,17 +31,17 @@ export class FlashLoanLogic extends core.Logic implements core.LogicTokenListInt } async getTokenList() { - const tokenSet = new Set(); + const tokenAddressSet = new Set(); const service = new Service(this.chainId, this.provider); const markets = getMarkets(this.chainId); for (const market of markets) { const tokens = await service.getMarketTokens(market.id); - tokenSet.add(tokens.loanToken); - tokenSet.add(tokens.collateralToken); + tokenAddressSet.add(tokens.loanTokenAddress); + tokenAddressSet.add(tokens.collateralTokenAddress); } - const tokenList: FlashLoanLogicTokenList = await service.getTokens([...tokenSet]); + const tokenList: FlashLoanLogicTokenList = await service.getTokens([...tokenAddressSet]); return tokenList; } diff --git a/src/logics/morphoblue/service.ts b/src/logics/morphoblue/service.ts index d1cbc6c3..74ed5cdd 100644 --- a/src/logics/morphoblue/service.ts +++ b/src/logics/morphoblue/service.ts @@ -1,7 +1,7 @@ import { BigNumber } from 'ethers'; import { Morpho, Morpho__factory } from './contracts'; import { MorphoInterface } from './contracts/Morpho'; -import { Tokens } from './types'; +import { TokenAddresses } from './types'; import * as common from '@protocolink/common'; import { getContractAddress, getMarket } from './configs'; @@ -27,13 +27,13 @@ export class Service extends common.Web3Toolkit { async getLoanToken(marketId: string) { const tokens = await this.getMarketTokens(marketId); - const loanToken = await this.getToken(tokens.loanToken); + const loanToken = await this.getToken(tokens.loanTokenAddress); return loanToken; } async getCollateralToken(marketId: string) { const tokens = await this.getMarketTokens(marketId); - const collateralToken = await this.getToken(tokens.collateralToken); + const collateralToken = await this.getToken(tokens.collateralTokenAddress); return collateralToken; } @@ -51,7 +51,7 @@ export class Service extends common.Web3Toolkit { const { loanToken, collateralToken } = this.morphoIface.decodeFunctionResult('idToMarketParams', returnData[0]); - const tokens: Tokens = { loanToken, collateralToken }; + const tokens: TokenAddresses = { loanTokenAddress: loanToken, collateralTokenAddress: collateralToken }; return tokens; } diff --git a/src/logics/morphoblue/types.ts b/src/logics/morphoblue/types.ts index c9678060..deb6589a 100644 --- a/src/logics/morphoblue/types.ts +++ b/src/logics/morphoblue/types.ts @@ -1,4 +1,4 @@ -export interface Tokens { - loanToken: string; - collateralToken: string; +export interface TokenAddresses { + loanTokenAddress: string; + collateralTokenAddress: string; }