From 02d95272e968c2b681a43ea2031fb81a3d494231 Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Tue, 25 Feb 2025 15:49:38 +0100 Subject: [PATCH 01/12] feat(migration-sdk-viem): create abstract borrow position --- .../borrow/MigratableBorrowPosition.ts | 110 ++++++++++++++++++ .../migration-sdk-viem/src/types/positions.ts | 2 + 2 files changed, 112 insertions(+) create mode 100644 packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts diff --git a/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts b/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts new file mode 100644 index 00000000..5a4ff226 --- /dev/null +++ b/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts @@ -0,0 +1,110 @@ +import type { Address, ChainId, MarketId } from "@morpho-org/blue-sdk"; + +import type { MigrationBundle } from "../../types/actions.js"; +import type { + BorrowMigrationLimiter, + CollateralMigrationLimiter, + MigratableProtocol, +} from "../../types/index.js"; + +/** + * Namespace containing argument definitions for Migratable Borrow Position. + */ +export namespace MigratableBorrowPosition { + /** + * Arguments required for building a migration operation. + */ + export interface Args { + /** The collateral amount to migrate. */ + collateralAmount: bigint; + /** The borrow amount to migrate. */ + borrowAmount: bigint; + /** The id of the market to migrate to. */ + marketTo: MarketId; + /** Slippage tolerance for the current position (optional). */ + slippageFrom?: bigint; + /** Slippage tolerance for the target market (optional). */ + slippageTo?: bigint; + } +} + +/** + * Interface representing the structure of a migratable borrow position. + */ +export interface IMigratableBorrowPosition { + /** The chain ID where the position resides. */ + chainId: ChainId; + /** The protocol associated with the borrow position. */ + protocol: MigratableProtocol; + /** The user's address. */ + user: Address; + /** The address of the token being used as collateral. */ + collateralToken: Address; + /** The address of the loan token being borrow. */ + loanToken: Address; + /** The total collateral balance of the position. */ + collateral: bigint; + /** The total borrow balance of the position. */ + borrow: bigint; + /** The annual percentage yield (APY) of the collateral position. */ + collateralApy: number; + /** The annual percentage yield (APY) of the borrow position. */ + borrowApy: number; + /** The maximum collateral migration limit and its corresponding limiter. */ + maxCollateral: { value: bigint; limiter: CollateralMigrationLimiter }; + /** The maximum borrow migration limit and its corresponding limiter. */ + maxBorrow: { value: bigint; limiter: BorrowMigrationLimiter }; +} + +/** + * Abstract class representing a migratable borrow position. + */ +export abstract class MigratableBorrowPosition + implements IMigratableBorrowPosition +{ + public readonly protocol; + public readonly user; + public readonly loanToken; + public readonly borrow; + public readonly borrowApy; + public readonly maxCollateral; + public readonly chainId; + public readonly collateralToken; + public readonly collateral; + public readonly collateralApy; + public readonly maxBorrow; + + /** + * Creates an instance of MigratableBorrowPosition. + * + * @param config - Configuration object containing the position details. + */ + constructor(config: IMigratableBorrowPosition) { + this.protocol = config.protocol; + this.user = config.user; + this.loanToken = config.loanToken; + this.borrow = config.borrow; + this.borrowApy = config.borrowApy; + this.maxCollateral = config.maxCollateral; + this.chainId = config.chainId; + this.collateralToken = config.collateralToken; + this.collateral = config.collateral; + this.collateralApy = config.collateralApy; + this.maxBorrow = config.maxBorrow; + } + + /** + * Method to retrieve a migration operation for the borrow position. + * + * @param args - The arguments required to execute the migration. + * @param chainId - The chain ID of the migration. + * @param supportsSignature - Whether the migration supports signature-based execution. + * + * @returns A migration bundle containing the migration details. + */ + abstract getMigrationTx( + args: MigratableBorrowPosition.Args, + chainId: ChainId, + supportsSignature: boolean, + ): MigrationBundle; +} diff --git a/packages/migration-sdk-viem/src/types/positions.ts b/packages/migration-sdk-viem/src/types/positions.ts index c087f328..4d845af5 100644 --- a/packages/migration-sdk-viem/src/types/positions.ts +++ b/packages/migration-sdk-viem/src/types/positions.ts @@ -7,3 +7,5 @@ export enum SupplyMigrationLimiter { } export enum BorrowMigrationLimiter {} + +export enum CollateralMigrationLimiter {} From 544c1f5358b096c1597f0ce458179da50d3cc031 Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Thu, 27 Feb 2025 11:21:05 +0100 Subject: [PATCH 02/12] feat(migration-sdk-viem): fetch aave v3 borrow positions --- .../migration-sdk-viem/src/abis/aaveV3.ts | 574 ++++++++++++++++++ packages/migration-sdk-viem/src/config.ts | 10 + .../src/fetchers/aaveV3/aaveV3.fetchers.ts | 496 +++++++++------ .../borrow/MigratableBorrowPosition.ts | 32 +- .../src/positions/borrow/aaveV3.borrow.ts | 61 ++ .../src/positions/borrow/index.ts | 3 + .../migration-sdk-viem/src/positions/index.ts | 11 +- .../migration-sdk-viem/src/types/positions.ts | 5 +- .../test/e2e/aaveV3/borrow.test.ts | 267 ++++++++ .../test/e2e/aaveV3/supply.test.ts | 48 +- 10 files changed, 1301 insertions(+), 206 deletions(-) create mode 100644 packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts create mode 100644 packages/migration-sdk-viem/src/positions/borrow/index.ts create mode 100644 packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts diff --git a/packages/migration-sdk-viem/src/abis/aaveV3.ts b/packages/migration-sdk-viem/src/abis/aaveV3.ts index 67f6bb6d..c070cfbb 100644 --- a/packages/migration-sdk-viem/src/abis/aaveV3.ts +++ b/packages/migration-sdk-viem/src/abis/aaveV3.ts @@ -2432,3 +2432,577 @@ export const variableDebtTokenV3Abi = [ type: "function", }, ] as const; + +export const aaveV3OracleAbi = [ + { + inputs: [ + { + internalType: "contract IPoolAddressesProvider", + name: "provider", + type: "address", + }, + { internalType: "address[]", name: "assets", type: "address[]" }, + { internalType: "address[]", name: "sources", type: "address[]" }, + { internalType: "address", name: "fallbackOracle", type: "address" }, + { internalType: "address", name: "baseCurrency", type: "address" }, + { internalType: "uint256", name: "baseCurrencyUnit", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "asset", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "source", + type: "address", + }, + ], + name: "AssetSourceUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "baseCurrency", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "baseCurrencyUnit", + type: "uint256", + }, + ], + name: "BaseCurrencySet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "fallbackOracle", + type: "address", + }, + ], + name: "FallbackOracleUpdated", + type: "event", + }, + { + inputs: [], + name: "ADDRESSES_PROVIDER", + outputs: [ + { + internalType: "contract IPoolAddressesProvider", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "BASE_CURRENCY", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "BASE_CURRENCY_UNIT", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "asset", type: "address" }], + name: "getAssetPrice", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address[]", name: "assets", type: "address[]" }], + name: "getAssetsPrices", + outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getFallbackOracle", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "asset", type: "address" }], + name: "getSourceOfAsset", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address[]", name: "assets", type: "address[]" }, + { internalType: "address[]", name: "sources", type: "address[]" }, + ], + name: "setAssetSources", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "fallbackOracle", type: "address" }, + ], + name: "setFallbackOracle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +export const addressesProviderAbi = [ + { + inputs: [ + { internalType: "string", name: "marketId", type: "string" }, + { internalType: "address", name: "owner", type: "address" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "ACLAdminUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "ACLManagerUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "oldAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "AddressSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "proxyAddress", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "oldImplementationAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newImplementationAddress", + type: "address", + }, + ], + name: "AddressSetAsProxy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "string", + name: "oldMarketId", + type: "string", + }, + { + indexed: true, + internalType: "string", + name: "newMarketId", + type: "string", + }, + ], + name: "MarketIdSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "PoolConfiguratorUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "PoolDataProviderUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "PoolUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "PriceOracleSentinelUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "oldAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "PriceOracleUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "proxyAddress", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "implementationAddress", + type: "address", + }, + ], + name: "ProxyCreated", + type: "event", + }, + { + inputs: [], + name: "getACLAdmin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getACLManager", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "id", type: "bytes32" }], + name: "getAddress", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getMarketId", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPool", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolConfigurator", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolDataProvider", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPriceOracle", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPriceOracleSentinel", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newAclAdmin", type: "address" }], + name: "setACLAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newAclManager", type: "address" }, + ], + name: "setACLManager", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "id", type: "bytes32" }, + { internalType: "address", name: "newAddress", type: "address" }, + ], + name: "setAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "id", type: "bytes32" }, + { + internalType: "address", + name: "newImplementationAddress", + type: "address", + }, + ], + name: "setAddressAsProxy", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "string", name: "newMarketId", type: "string" }], + name: "setMarketId", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newPoolConfiguratorImpl", + type: "address", + }, + ], + name: "setPoolConfiguratorImpl", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newDataProvider", type: "address" }, + ], + name: "setPoolDataProvider", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newPoolImpl", type: "address" }], + name: "setPoolImpl", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newPriceOracle", type: "address" }, + ], + name: "setPriceOracle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newPriceOracleSentinel", + type: "address", + }, + ], + name: "setPriceOracleSentinel", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/packages/migration-sdk-viem/src/config.ts b/packages/migration-sdk-viem/src/config.ts index 1f6020f2..816e756b 100644 --- a/packages/migration-sdk-viem/src/config.ts +++ b/packages/migration-sdk-viem/src/config.ts @@ -6,6 +6,7 @@ import { protocolDataProviderAbi as protocolDataProviderAbi_v2, } from "./abis/aaveV2.js"; import { + addressesProviderAbi as addressesProviderAbi_v3, poolAbi, protocolDataProviderAbi as protocolDataProviderAbi_v3, } from "./abis/aaveV3.js"; @@ -48,6 +49,7 @@ export interface ProtocolMigrationContracts { [MigratableProtocol.aaveV3]: { pool: Contract; protocolDataProvider: Contract; + addressesProvider: Contract; } | null; [MigratableProtocol.compoundV3]: Record< string, @@ -91,6 +93,10 @@ export const migrationAddressesRegistry = { address: "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", abi: protocolDataProviderAbi_v3, }, + addressesProvider: { + address: "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", + abi: addressesProviderAbi_v3, + }, }, [MigratableProtocol.compoundV3]: { usdc: { @@ -127,6 +133,10 @@ export const migrationAddressesRegistry = { address: "0x2d8A3C5677189723C4cB8873CfC9C8976FDF38Ac", abi: protocolDataProviderAbi_v3, }, + addressesProvider: { + address: "0xe20fCBdBfFC4Dd138cE8b2E6FBb6CB49777ad64D", + abi: addressesProviderAbi_v3, + }, }, [MigratableProtocol.compoundV3]: { usdc: { diff --git a/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts b/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts index 0f6d79b2..ad821b83 100644 --- a/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts +++ b/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts @@ -1,17 +1,26 @@ -import { type Address, MathLib, Token } from "@morpho-org/blue-sdk"; +import { type Address, MathLib } from "@morpho-org/blue-sdk"; import { isDefined } from "@morpho-org/morpho-ts"; -import type { FetchParameters } from "@morpho-org/blue-sdk-viem"; +import { type FetchParameters, fetchToken } from "@morpho-org/blue-sdk-viem"; -import { type Client, erc20Abi } from "viem"; +import { type Client, erc20Abi, parseUnits } from "viem"; import { getChainId, readContract } from "viem/actions"; -import { aTokenV3Abi } from "../../abis/aaveV3.js"; +import { + aTokenV3Abi, + aaveV3OracleAbi, + variableDebtTokenV3Abi, +} from "../../abis/aaveV3.js"; import { migrationAddresses } from "../../config.js"; import type { MigratablePosition } from "../../positions/index.js"; import { MigratableSupplyPosition_AaveV3 } from "../../positions/supply/aaveV3.supply.js"; +import { values } from "lodash"; +import { MigratableBorrowPosition_AaveV3 } from "../../positions/borrow/index.js"; import { MigratableProtocol } from "../../types/index.js"; -import { SupplyMigrationLimiter } from "../../types/positions.js"; +import { + BorrowMigrationLimiter, + SupplyMigrationLimiter, +} from "../../types/positions.js"; import { rateToApy } from "../../utils/rates.js"; export async function fetchAaveV3Positions( @@ -28,29 +37,37 @@ export async function fetchAaveV3Positions( if (!migrationContracts) return []; - const [allATokens, userConfig, reservesList] = await Promise.all([ - readContract(client, { - ...parameters, - abi: migrationContracts.protocolDataProvider.abi, - address: migrationContracts.protocolDataProvider.address, - functionName: "getAllATokens", - args: [], - }), - readContract(client, { - ...parameters, - abi: migrationContracts.pool.abi, - address: migrationContracts.pool.address, - functionName: "getUserConfiguration", - args: [user], - }), - readContract(client, { - ...parameters, - abi: migrationContracts.pool.abi, - address: migrationContracts.pool.address, - functionName: "getReservesList", - args: [], - }), - ]); + const [allATokens, userConfig, reservesList, oracleAddress] = + await Promise.all([ + readContract(client, { + ...parameters, + abi: migrationContracts.protocolDataProvider.abi, + address: migrationContracts.protocolDataProvider.address, + functionName: "getAllATokens", + args: [], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.pool.abi, + address: migrationContracts.pool.address, + functionName: "getUserConfiguration", + args: [user], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.pool.abi, + address: migrationContracts.pool.address, + functionName: "getReservesList", + args: [], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.addressesProvider.abi, + address: migrationContracts.addressesProvider.address, + functionName: "getPriceOracle", + args: [], + }), + ]); /* cf https://docs.aave.com/developers/v/2.0/the-core-protocol/lendingpool#getuserconfiguration */ const orderedUserConfig = userConfig.data @@ -78,161 +95,290 @@ export async function fetchAaveV3Positions( }), ); - const isBorrowing = Object.values(userConfigByToken).some( - ({ isBorrowed }) => isBorrowed, - ); + const positionsData = ( + await Promise.all( + allATokens.map(async ({ tokenAddress }) => { + const [underlyingAddress, totalSupply, nonce, aTokenData] = + await Promise.all([ + readContract(client, { + ...parameters, + abi: aTokenV3Abi, + address: tokenAddress, + functionName: "UNDERLYING_ASSET_ADDRESS", + args: [], + }), + readContract(client, { + ...parameters, + abi: aTokenV3Abi, + address: tokenAddress, + functionName: "balanceOf", + args: [user], + }), + readContract(client, { + ...parameters, + abi: aTokenV3Abi, + address: tokenAddress, + functionName: "nonces", + args: [user], + }), + fetchToken(tokenAddress, client, parameters), + ]); - const positions = await Promise.all( - allATokens.map(async ({ tokenAddress, symbol }) => { - const [ - underlyingAddress, - totalSupply, - nonce, - aTokenDecimals, - aTokenName, - ] = await Promise.all([ - readContract(client, { - ...parameters, - abi: aTokenV3Abi, - address: tokenAddress, - functionName: "UNDERLYING_ASSET_ADDRESS", - args: [], - }), - readContract(client, { - ...parameters, - abi: aTokenV3Abi, - address: tokenAddress, - functionName: "balanceOf", - args: [user], - }), - readContract(client, { - ...parameters, - abi: aTokenV3Abi, - address: tokenAddress, - functionName: "nonces", - args: [user], - }), - readContract(client, { - ...parameters, - abi: aTokenV3Abi, - address: tokenAddress, - functionName: "decimals", - args: [], - }), - readContract(client, { - ...parameters, - abi: aTokenV3Abi, - address: tokenAddress, - functionName: "name", - args: [], - }), - ]); - - const aTokenData = new Token({ - address: tokenAddress as Address, - symbol, - decimals: aTokenDecimals, - name: aTokenName, - }); - - if (totalSupply === 0n) return null; - - const userReserveConfig = userConfigByToken[underlyingAddress]; - - if (!userReserveConfig) return null; - - const [ - poolLiquidity, - [, , , , , usageAsCollateralEnabled, , , isActive], - { currentLiquidityRate }, - ] = await Promise.all([ - readContract(client, { - ...parameters, - abi: erc20Abi, - address: underlyingAddress, - functionName: "balanceOf", - args: [tokenAddress], - }), - readContract(client, { - ...parameters, - abi: migrationContracts.protocolDataProvider.abi, - address: migrationContracts.protocolDataProvider.address, - functionName: "getReserveConfigurationData", - args: [underlyingAddress], - }), - readContract(client, { + const userReserveConfig = userConfigByToken[underlyingAddress]; + + if (!userReserveConfig) return; + + const [ + poolLiquidity, + [ + , + , + liquidationThreshold, + , + , + usageAsCollateralEnabled, + , + , + isActive, + ], + { + currentLiquidityRate, + variableDebtTokenAddress, + currentVariableBorrowRate, + }, + eModeId, + underlying, + ] = await Promise.all([ + readContract(client, { + ...parameters, + abi: erc20Abi, + address: underlyingAddress, + functionName: "balanceOf", + args: [tokenAddress], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.protocolDataProvider.abi, + address: migrationContracts.protocolDataProvider.address, + functionName: "getReserveConfigurationData", + args: [underlyingAddress], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.pool.abi, + address: migrationContracts.pool.address, + functionName: "getReserveData", + args: [underlyingAddress], + }), + readContract(client, { + ...parameters, + ...migrationContracts.protocolDataProvider, + functionName: "getReserveEModeCategory", + args: [underlyingAddress], + }), + fetchToken(underlyingAddress, client, parameters), + ]); + + const [totalBorrow, eModeCategoryData] = await Promise.all([ + readContract(client, { + ...parameters, + abi: variableDebtTokenV3Abi, + address: variableDebtTokenAddress, + functionName: "balanceOf", + args: [user], + }), + readContract(client, { + ...parameters, + ...migrationContracts.pool, + functionName: "getEModeCategoryData", + args: [Number(eModeId)], + }), + ]); + + const ethPrice = await readContract(client, { ...parameters, - abi: migrationContracts.pool.abi, - address: migrationContracts.pool.address, - functionName: "getReserveData", - args: [underlyingAddress], - }), - ]); - - // TODO we only focus on pure suppliers now - // We need to check both `usageAsCollateralEnabled` and `userReserveConfig.isUsedAsCollateral`` - // Because `userReserveConfig.isUsedAsCollateral` is true by default for everyone when `usageAsCollateralEnabled` is false - if ( - userReserveConfig.isUsedAsCollateral && - usageAsCollateralEnabled && - isBorrowing - ) - return null; - - /* MAX */ - const max = (() => { - if (!isActive) - return { value: 0n, limiter: SupplyMigrationLimiter.withdrawPaused }; - - const maxWithdrawFromAvailableLiquidity = poolLiquidity; - const maxWithdrawFromSupplyBalance = totalSupply; - - const maxWithdraw = MathLib.min( - maxWithdrawFromAvailableLiquidity, - maxWithdrawFromSupplyBalance, - ); - - if (maxWithdraw === maxWithdrawFromAvailableLiquidity) - return { - value: maxWithdrawFromAvailableLiquidity, - limiter: SupplyMigrationLimiter.liquidity, - }; - - if (maxWithdraw === maxWithdrawFromSupplyBalance) - return { - value: maxWithdrawFromSupplyBalance, - limiter: SupplyMigrationLimiter.position, - }; - })()!; + abi: aaveV3OracleAbi, + address: oracleAddress, + functionName: "getAssetPrice", + args: [ + eModeId === 0n ? underlyingAddress : eModeCategoryData.priceSource, + ], + }); - return { - underlyingAddress, - supply: totalSupply, - supplyApy: rateToApy(currentLiquidityRate, "s", 27), - max, - nonce, - aToken: aTokenData, - }; - }), + // We need to check both `usageAsCollateralEnabled` and `userReserveConfig.isUsedAsCollateral`` + // Because `userReserveConfig.isUsedAsCollateral` is true by default for everyone when `usageAsCollateralEnabled` is false + const isCollateral = + userReserveConfig.isUsedAsCollateral && usageAsCollateralEnabled; + + return { + underlying, + supply: { + isCollateral, + poolLiquidity, + totalSupply, + isActive, + currentLiquidityRate, + aTokenData, + nonce, + ethPrice, + }, + borrow: { + liquidationThreshold: + eModeId === 0n + ? liquidationThreshold + : BigInt(eModeCategoryData.liquidationThreshold), + currentVariableBorrowRate, + totalBorrow, + isActive, + ethPrice, + }, + }; + }), + ) + ).filter(isDefined); + + const isBorrowing = values(userConfigByToken).some( + ({ isBorrowed }) => isBorrowed, ); - return positions - .filter(isDefined) - .flatMap(({ underlyingAddress, supply, supplyApy, max, nonce, aToken }) => { - if (supply > 0n) - return [ - new MigratableSupplyPosition_AaveV3({ + const positions: MigratablePosition[] = positionsData + .map( + ({ + underlying, + supply: { + isCollateral, + isActive, + poolLiquidity, + totalSupply, + currentLiquidityRate, + nonce, + aTokenData, + }, + }) => { + if (isBorrowing && isCollateral) return; + + /* MAX */ + const max = (() => { + if (!isActive) + return { + value: 0n, + limiter: SupplyMigrationLimiter.withdrawPaused, + }; + + const maxWithdrawFromAvailableLiquidity = poolLiquidity; + const maxWithdrawFromSupplyBalance = totalSupply; + + const maxWithdraw = MathLib.min( + maxWithdrawFromAvailableLiquidity, + maxWithdrawFromSupplyBalance, + ); + + if (maxWithdraw === maxWithdrawFromAvailableLiquidity) + return { + value: maxWithdrawFromAvailableLiquidity, + limiter: SupplyMigrationLimiter.liquidity, + }; + + if (maxWithdraw === maxWithdrawFromSupplyBalance) + return { + value: maxWithdrawFromSupplyBalance, + limiter: SupplyMigrationLimiter.position, + }; + })()!; + + if (totalSupply > 0n) + return new MigratableSupplyPosition_AaveV3({ user, - loanToken: underlyingAddress as Address, - supply, - supplyApy, + loanToken: underlying.address, + supply: totalSupply, + supplyApy: rateToApy(currentLiquidityRate, "s", 27), max, nonce, - aToken, + aToken: aTokenData, chainId, - }), - ]; + }); + }, + ) + .filter(isDefined); + + const collateralPositionsData = positionsData.filter( + ({ supply: { isCollateral, totalSupply } }) => + isCollateral && totalSupply > 0n, + ); + const borrowPositionsData = positionsData.filter( + ({ borrow: { totalBorrow } }) => totalBorrow > 0n, + ); + + // We only handle 1-Borrow 1-Collateral positions + if ( + collateralPositionsData.length === 1 && + borrowPositionsData.length === 1 + ) { + const { underlying: collateralToken, supply: collateralData } = + collateralPositionsData[0]!; + const { underlying: loanToken, borrow: loanData } = borrowPositionsData[0]!; + + /* MAX */ + const maxCollateral = (() => { + if (!collateralData.isActive) + return { + value: 0n, + limiter: SupplyMigrationLimiter.withdrawPaused, + }; + + const maxWithdrawFromAvailableLiquidity = collateralData.poolLiquidity; + const maxWithdrawFromSupplyBalance = collateralData.totalSupply; + + const maxWithdraw = MathLib.min( + maxWithdrawFromAvailableLiquidity, + maxWithdrawFromSupplyBalance, + ); + + if (maxWithdraw === maxWithdrawFromAvailableLiquidity) + return { + value: maxWithdrawFromAvailableLiquidity, + limiter: SupplyMigrationLimiter.liquidity, + }; + + if (maxWithdraw === maxWithdrawFromSupplyBalance) + return { + value: maxWithdrawFromSupplyBalance, + limiter: SupplyMigrationLimiter.position, + }; + })()!; + const maxBorrow = (() => { + if (!loanData.isActive) + return { + value: 0n, + limiter: BorrowMigrationLimiter.repayPaused, + }; + + return { + value: loanData.totalBorrow, + limiter: BorrowMigrationLimiter.position, + }; + })()!; + + positions.push( + new MigratableBorrowPosition_AaveV3({ + loanToken, + collateralToken, + collateral: collateralData.totalSupply, + borrow: loanData.totalBorrow, + collateralApy: rateToApy(collateralData.currentLiquidityRate, "s", 27), + borrowApy: rateToApy(loanData.currentVariableBorrowRate, "s", 27), + lltv: loanData.liquidationThreshold * parseUnits("1", 14), // lltv has 4 decimals on aave V3 + aToken: collateralData.aTokenData, + nonce: collateralData.nonce, + chainId, + user, + maxRepay: maxBorrow, + maxWithdraw: maxCollateral, + collateralPriceEth: collateralData.ethPrice, + loanPriceEth: loanData.ethPrice, + }), + ); + } - return []; - }); + return positions; } diff --git a/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts b/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts index 5a4ff226..4c09addd 100644 --- a/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts +++ b/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts @@ -1,10 +1,10 @@ -import type { Address, ChainId, MarketId } from "@morpho-org/blue-sdk"; +import type { Address, ChainId, MarketId, Token } from "@morpho-org/blue-sdk"; import type { MigrationBundle } from "../../types/actions.js"; import type { BorrowMigrationLimiter, - CollateralMigrationLimiter, MigratableProtocol, + SupplyMigrationLimiter, } from "../../types/index.js"; /** @@ -38,10 +38,10 @@ export interface IMigratableBorrowPosition { protocol: MigratableProtocol; /** The user's address. */ user: Address; - /** The address of the token being used as collateral. */ - collateralToken: Address; - /** The address of the loan token being borrow. */ - loanToken: Address; + /** The token being used as collateral. */ + collateralToken: Token; + /** The loan token being borrowed. */ + loanToken: Token; /** The total collateral balance of the position. */ collateral: bigint; /** The total borrow balance of the position. */ @@ -51,9 +51,11 @@ export interface IMigratableBorrowPosition { /** The annual percentage yield (APY) of the borrow position. */ borrowApy: number; /** The maximum collateral migration limit and its corresponding limiter. */ - maxCollateral: { value: bigint; limiter: CollateralMigrationLimiter }; + maxWithdraw: { value: bigint; limiter: SupplyMigrationLimiter }; /** The maximum borrow migration limit and its corresponding limiter. */ - maxBorrow: { value: bigint; limiter: BorrowMigrationLimiter }; + maxRepay: { value: bigint; limiter: BorrowMigrationLimiter }; + /** The liquidation loan to value (LLTV) of the market */ + lltv: bigint; } /** @@ -67,12 +69,13 @@ export abstract class MigratableBorrowPosition public readonly loanToken; public readonly borrow; public readonly borrowApy; - public readonly maxCollateral; public readonly chainId; public readonly collateralToken; public readonly collateral; public readonly collateralApy; - public readonly maxBorrow; + public readonly maxRepay; + public readonly maxWithdraw; + public readonly lltv; /** * Creates an instance of MigratableBorrowPosition. @@ -85,14 +88,19 @@ export abstract class MigratableBorrowPosition this.loanToken = config.loanToken; this.borrow = config.borrow; this.borrowApy = config.borrowApy; - this.maxCollateral = config.maxCollateral; + this.maxWithdraw = config.maxWithdraw; this.chainId = config.chainId; this.collateralToken = config.collateralToken; this.collateral = config.collateral; this.collateralApy = config.collateralApy; - this.maxBorrow = config.maxBorrow; + this.maxRepay = config.maxRepay; + this.lltv = config.lltv; } + abstract getLtv(options?: { withdrawn?: bigint; repaid?: bigint }): + | bigint + | null; + /** * Method to retrieve a migration operation for the borrow position. * diff --git a/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts b/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts new file mode 100644 index 00000000..ac070a24 --- /dev/null +++ b/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts @@ -0,0 +1,61 @@ +import { MathLib, type Token } from "@morpho-org/blue-sdk"; + +import { maxUint256, parseUnits } from "viem"; +import type { MigrationBundle } from "../../types/actions.js"; +import { MigratableProtocol } from "../../types/index.js"; +import { + type IMigratableBorrowPosition, + MigratableBorrowPosition, +} from "./index.js"; + +interface IMigratableBorrowPosition_AaveV3 + extends Omit { + nonce: bigint; + aToken: Token; + collateralPriceEth: bigint; + loanPriceEth: bigint; +} + +export class MigratableBorrowPosition_AaveV3 + extends MigratableBorrowPosition + implements IMigratableBorrowPosition_AaveV3 +{ + private _nonce; + public readonly aToken; + public readonly collateralPriceEth; + public readonly loanPriceEth; + + constructor(config: IMigratableBorrowPosition_AaveV3) { + super({ ...config, protocol: MigratableProtocol.aaveV3 }); + this.aToken = config.aToken; + this._nonce = config.nonce; + this.collateralPriceEth = config.collateralPriceEth; + this.loanPriceEth = config.loanPriceEth; + } + + getLtv({ + withdrawn = 0n, + repaid = 0n, + }: { withdrawn?: bigint; repaid?: bigint } = {}): bigint | null { + const totalCollateralEth = + ((this.collateral - withdrawn) * this.collateralPriceEth) / + parseUnits("1", this.collateralToken.decimals); + + const totalBorrowEth = + ((this.borrow - repaid) * this.loanPriceEth) / + parseUnits("1", this.loanToken.decimals); + + if (totalBorrowEth <= 0n) return null; + if (totalCollateralEth <= 0n) return maxUint256; + + return MathLib.wDivUp(totalBorrowEth, totalCollateralEth); + } + + get nonce() { + return this._nonce; + } + + getMigrationTx(): MigrationBundle { + throw "not implemented"; // TODO + } +} diff --git a/packages/migration-sdk-viem/src/positions/borrow/index.ts b/packages/migration-sdk-viem/src/positions/borrow/index.ts new file mode 100644 index 00000000..dcb6ea56 --- /dev/null +++ b/packages/migration-sdk-viem/src/positions/borrow/index.ts @@ -0,0 +1,3 @@ +export * from "./MigratableBorrowPosition.js"; +export * from "./aaveV3.borrow.js"; +export * from "./blue.borrow.js"; diff --git a/packages/migration-sdk-viem/src/positions/index.ts b/packages/migration-sdk-viem/src/positions/index.ts index e5ae2372..2e454a94 100644 --- a/packages/migration-sdk-viem/src/positions/index.ts +++ b/packages/migration-sdk-viem/src/positions/index.ts @@ -1,11 +1,18 @@ +import type { MigratableBorrowPosition } from "./borrow/MigratableBorrowPosition.js"; import type { MigratableSupplyPosition } from "./supply/index.js"; export { MigratableSupplyPosition } from "./supply/index.js"; -export type MigratablePosition = MigratableSupplyPosition; +export type MigratablePosition = + | MigratableSupplyPosition + | MigratableBorrowPosition; export namespace MigratablePosition { export type Args = - T extends MigratableSupplyPosition ? MigratableSupplyPosition.Args : never; + T extends MigratableSupplyPosition + ? MigratableSupplyPosition.Args + : T extends MigratableBorrowPosition + ? MigratableBorrowPosition.Args + : never; } export { MigratableBorrowPosition_Blue } from "./borrow/blue.borrow.js"; diff --git a/packages/migration-sdk-viem/src/types/positions.ts b/packages/migration-sdk-viem/src/types/positions.ts index 4d845af5..c3622579 100644 --- a/packages/migration-sdk-viem/src/types/positions.ts +++ b/packages/migration-sdk-viem/src/types/positions.ts @@ -6,6 +6,9 @@ export enum SupplyMigrationLimiter { protocolCap = "protocolCap", } -export enum BorrowMigrationLimiter {} +export enum BorrowMigrationLimiter { + position = "position", + repayPaused = "repayPaused", +} export enum CollateralMigrationLimiter {} diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts new file mode 100644 index 00000000..7eed9aeb --- /dev/null +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts @@ -0,0 +1,267 @@ +import { + BorrowMigrationLimiter, + MigratableProtocol, + SupplyMigrationLimiter, + fetchMigratablePositions, +} from "../../../src/index.js"; + +import { ChainId, addresses } from "@morpho-org/blue-sdk"; + +import type { ViemTestContext } from "@morpho-org/test/vitest"; +import { type Address, parseEther, parseUnits } from "viem"; +import { type TestAPI, describe, expect } from "vitest"; +import { MIGRATION_ADDRESSES } from "../../../src/config.js"; +import { MigratableBorrowPosition_AaveV3 } from "../../../src/positions/borrow/aaveV3.borrow.js"; +import { MigratableSupplyPosition_AaveV3 } from "../../../src/positions/supply/aaveV3.supply.js"; +import { test } from "../setup.js"; + +const TEST_CONFIGS = [ + { + chainId: ChainId.EthMainnet, + aUsdc: "0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c", + testFn: test[ChainId.EthMainnet] as TestAPI, + marketTo: "0x", //TODO + }, + { + chainId: ChainId.BaseMainnet, + aUsdc: "0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB", + testFn: test[ChainId.BaseMainnet] as TestAPI, + marketTo: "0x", //TODO + }, +] as const; + +describe("Borrow position on AAVE V3", () => { + for (const { chainId, aUsdc, testFn } of TEST_CONFIGS) { + const { pool } = MIGRATION_ADDRESSES[chainId].aaveV3; + const { + // bundler3: { generalAdapter1, aaveV3CoreMigrationAdapter }, + wNative, + usdc, + wstEth, + } = addresses[chainId]; + + const writeSupply = async ( + client: ViemTestContext["client"], + market: Address, + amount: bigint, + asCollateral = false, + ) => { + await client.deal({ + erc20: market, + amount: amount, + }); + await client.approve({ + address: market, + args: [pool.address, amount], + }); + await client.writeContract({ + ...pool, + functionName: "deposit", + args: [market, amount, client.account.address, 0], + }); + await client.writeContract({ + ...pool, + functionName: "setUserUseReserveAsCollateral", + args: [market, asCollateral], + }); + + await client.mine({ blocks: 500 }); //accrue some interests + }; + + const writeBorrow = async ( + client: ViemTestContext["client"], + market: Address, + amount: bigint, + ) => { + await client.writeContract({ + ...pool, + functionName: "borrow", + args: [market, amount, 2n, 0, client.account.address], + }); + }; + + describe(`on chain ${chainId}`, () => { + testFn( + "should fetch user position", + async ({ client }: ViemTestContext) => { + const collateralAmount = parseUnits("1000000", 6); + const borrowAmount = parseEther("1"); + + await writeSupply(client, usdc, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + + const position = + aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); + + expect(position.protocol).toEqual(MigratableProtocol.aaveV3); + expect(position.user).toEqual(client.account.address); + expect(position.loanToken.address).toEqual(wNative); + expect(position.nonce).toEqual(0n); + expect(position.aToken.address).toEqual(aUsdc); + expect(position.collateral).toBeGreaterThanOrEqual(collateralAmount); //interest accrued + expect(position.borrow).toBeGreaterThanOrEqual(borrowAmount); //interest accrued + expect(position.chainId).toEqual(chainId); + expect(position.collateralToken.address).toEqual(usdc); + expect(position.loanToken.address).toEqual(wNative); + expect(position.maxRepay.limiter).toEqual( + BorrowMigrationLimiter.position, + ); + expect(position.maxRepay.value).toEqual(position.borrow); + expect(position.maxWithdraw.limiter).toEqual( + SupplyMigrationLimiter.position, + ); + expect(position.maxWithdraw.value).toEqual(position.collateral); + }, + ); + + testFn( + "shouldn't fetch user position if multiple collaterals", + async ({ client }) => { + const collateralAmount1 = parseUnits("100000", 6); + const collateralAmount2 = parseUnits("1", 18); + const borrowAmount = parseEther("1"); + + await writeSupply(client, usdc, collateralAmount1, true); + await writeSupply(client, wstEth, collateralAmount2, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(0); + }, + ); + + testFn( + "should fetch multiple user positions if only one collateral", + async ({ client }) => { + const collateralAmount = parseUnits("100000", 6); + const pureSupply = parseUnits("10", 18); + const borrowAmount = parseEther("1"); + + await writeSupply(client, usdc, collateralAmount, true); + await writeSupply(client, wstEth, pureSupply, false); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(2); + expect(aaveV3Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV3, + ); + expect(aaveV3Positions[1]).toBeInstanceOf( + MigratableBorrowPosition_AaveV3, + ); + + const position = + aaveV3Positions[1] as MigratableBorrowPosition_AaveV3; + + expect(position.collateralToken.address).toBe(usdc); + }, + ); + + testFn( + "shouldn't fetch user position if multiple loans", + async ({ client }) => { + const collateralAmount = parseUnits("100000", 6); + const borrowAmount1 = parseUnits("1", 18); + const borrowAmount2 = parseEther("1"); + + await writeSupply(client, usdc, collateralAmount, true); + await writeBorrow(client, wstEth, borrowAmount1); + await writeBorrow(client, wNative, borrowAmount2); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(0); + }, + ); + + testFn( + "shouldn't fetch user collateral positions if no borrow", + async ({ client }) => { + const collateralAmount = parseUnits("100000", 6); + + await writeSupply(client, usdc, collateralAmount, true); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + expect(aaveV3Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV3, + ); + }, + ); + + testFn( + "should fetch user position with limited liquidity", + async ({ client }) => { + const collateralAmount = parseUnits("1000000", 6); + const borrowAmount = parseEther("5"); + const liquidity = parseUnits("100000", 6); + + await writeSupply(client, usdc, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + await client.deal({ + erc20: usdc, + account: aUsdc, + amount: liquidity, + }); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + + const position = + aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); + + expect(position.maxWithdraw).toEqual({ + limiter: SupplyMigrationLimiter.liquidity, + value: liquidity, + }); + }, + ); + }); + } +}); diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/supply.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/supply.test.ts index e88db24e..9f710c92 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3/supply.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/supply.test.ts @@ -14,26 +14,21 @@ import type { ViemTestContext } from "@morpho-org/test/vitest"; import { sendTransaction } from "viem/actions"; import { type TestAPI, describe, expect } from "vitest"; import { migrationAddressesRegistry } from "../../../src/config.js"; +import { MigratableBorrowPosition_AaveV3 } from "../../../src/positions/borrow/aaveV3.borrow.js"; import { MigratableSupplyPosition_AaveV3 } from "../../../src/positions/supply/aaveV3.supply.js"; import { test } from "../setup.js"; -const TEST_CONFIGS: { - chainId: ChainId.EthMainnet | ChainId.BaseMainnet; - aWeth: Address; - testFn: TestAPI; - mmWeth: Address; -}[] = [ +const TEST_CONFIGS = [ { chainId: ChainId.EthMainnet, aWeth: "0x4d5F47FA6A74757f35C14fD3a6Ef8E3C9BC514E8", - testFn: test[ChainId.EthMainnet], + testFn: test[ChainId.EthMainnet] as TestAPI, mmWeth: vaults[ChainId.EthMainnet].steakEth.address, }, { chainId: ChainId.BaseMainnet, aWeth: "0xD4a0e0b9149BCee3C920d2E00b5dE09138fd8bb7", - //@ts-expect-error - testFn: test[ChainId.BaseMainnet], + testFn: test[ChainId.BaseMainnet] as TestAPI, mmWeth: "0xa0E430870c4604CcfC7B38Ca7845B1FF653D0ff1", }, ] as const; @@ -166,7 +161,10 @@ describe("Supply position on AAVE V3", () => { const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; expect(aaveV3Positions).toBeDefined(); - expect(aaveV3Positions).toHaveLength(0); + expect(aaveV3Positions).toHaveLength(1); + expect(aaveV3Positions[0]).toBeInstanceOf( + MigratableBorrowPosition_AaveV3, + ); }, ); @@ -219,8 +217,13 @@ describe("Supply position on AAVE V3", () => { const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; expect(aaveV3Positions).toBeDefined(); expect(aaveV3Positions).toHaveLength(1); + expect(aaveV3Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV3, + ); - const migrationBundle = aaveV3Positions[0]!.getMigrationTx( + const position = aaveV3Positions[0] as MigratableSupplyPosition_AaveV3; + + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: migratedAmount, @@ -308,10 +311,13 @@ describe("Supply position on AAVE V3", () => { const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; expect(aaveV3Positions).toBeDefined(); expect(aaveV3Positions).toHaveLength(1); + expect(aaveV3Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV3, + ); - const position = aaveV3Positions[0]!; + const position = aaveV3Positions[0] as MigratableSupplyPosition_AaveV3; - const migrationBundle = aaveV3Positions[0]!.getMigrationTx( + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: position.supply, @@ -402,8 +408,14 @@ describe("Supply position on AAVE V3", () => { const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; expect(aaveV3Positions).toBeDefined(); expect(aaveV3Positions).toHaveLength(1); + expect(aaveV3Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV3, + ); + + const position = + aaveV3Positions[0] as MigratableSupplyPosition_AaveV3; - const migrationBundle = aaveV3Positions[0]!.getMigrationTx( + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: migratedAmount, @@ -490,10 +502,14 @@ describe("Supply position on AAVE V3", () => { const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; expect(aaveV3Positions).toBeDefined(); expect(aaveV3Positions).toHaveLength(1); + expect(aaveV3Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV3, + ); - const position = aaveV3Positions[0]!; + const position = + aaveV3Positions[0] as MigratableSupplyPosition_AaveV3; - const migrationBundle = aaveV3Positions[0]!.getMigrationTx( + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: position.supply, From 667d53acacd95290ba587ed9c73a8ca2df4b9e51 Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Tue, 4 Mar 2025 14:41:44 +0100 Subject: [PATCH 03/12] fix(morpho-test): add market fixture --- packages/morpho-test/src/fixtures/markets.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/morpho-test/src/fixtures/markets.ts b/packages/morpho-test/src/fixtures/markets.ts index ac56dca4..e3e24d23 100644 --- a/packages/morpho-test/src/fixtures/markets.ts +++ b/packages/morpho-test/src/fixtures/markets.ts @@ -10,6 +10,9 @@ import { parseEther, parseUnits } from "viem"; const { adaptiveCurveIrm, wNative, sDai, usdc, wstEth, wbIB01, usdt, dai } = addressesRegistry[ChainId.EthMainnet]; +const { adaptiveCurveIrm: adaptiveCurveIrm_base, wNative: wNative_base } = + addressesRegistry[ChainId.BaseMainnet]; + export const markets = { [ChainId.EthMainnet]: { eth_idle: MarketParams.idle(wNative), @@ -207,6 +210,15 @@ export const markets = { lltv: parseUnits("86", 16), }), }, + [ChainId.BaseMainnet]: { + eth_wstEth: new MarketParams({ + loanToken: wNative_base, + collateralToken: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + oracle: "0x4A11590e5326138B514E08A9B52202D42077Ca65", + irm: adaptiveCurveIrm_base, + lltv: parseUnits("94.5", 16), + }), + }, } as const; export const randomMarket = (params: Partial = {}) => From 4eed1d853caa198059b07ed86fb4603173747c45 Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Thu, 27 Feb 2025 18:36:05 +0100 Subject: [PATCH 04/12] feat(migraiton-sdk-viem): fix and test aave-v3 borrow migration --- .../src/fetchers/aaveV3/aaveV3.fetchers.ts | 61 ++-- .../borrow/MigratableBorrowPosition.ts | 24 +- .../src/positions/borrow/aaveV3.borrow.ts | 275 +++++++++++++++++- .../migration-sdk-viem/src/types/actions.ts | 3 + .../test/e2e/aaveV3/borrow.test.ts | 258 ++++++++++++++-- 5 files changed, 562 insertions(+), 59 deletions(-) diff --git a/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts b/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts index ad821b83..c5a80daf 100644 --- a/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts +++ b/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts @@ -1,7 +1,11 @@ -import { type Address, MathLib } from "@morpho-org/blue-sdk"; +import { type Address, MathLib, getChainAddresses } from "@morpho-org/blue-sdk"; import { isDefined } from "@morpho-org/morpho-ts"; -import { type FetchParameters, fetchToken } from "@morpho-org/blue-sdk-viem"; +import { + type FetchParameters, + blueAbi, + fetchToken, +} from "@morpho-org/blue-sdk-viem"; import { type Client, erc20Abi, parseUnits } from "viem"; import { getChainId, readContract } from "viem/actions"; @@ -31,6 +35,10 @@ export async function fetchAaveV3Positions( parameters.chainId ??= await getChainId(client); const chainId = parameters.chainId; + const { + morpho, + bundler3: { generalAdapter1 }, + } = getChainAddresses(chainId); const migrationContracts = migrationAddresses[chainId]?.[MigratableProtocol.aaveV3]; @@ -179,21 +187,36 @@ export async function fetchAaveV3Positions( fetchToken(underlyingAddress, client, parameters), ]); - const [totalBorrow, eModeCategoryData] = await Promise.all([ - readContract(client, { - ...parameters, - abi: variableDebtTokenV3Abi, - address: variableDebtTokenAddress, - functionName: "balanceOf", - args: [user], - }), - readContract(client, { - ...parameters, - ...migrationContracts.pool, - functionName: "getEModeCategoryData", - args: [Number(eModeId)], - }), - ]); + const [totalBorrow, eModeCategoryData, morphoNonce, isBundlerManaging] = + await Promise.all([ + readContract(client, { + ...parameters, + abi: variableDebtTokenV3Abi, + address: variableDebtTokenAddress, + functionName: "balanceOf", + args: [user], + }), + readContract(client, { + ...parameters, + ...migrationContracts.pool, + functionName: "getEModeCategoryData", + args: [Number(eModeId)], + }), + readContract(client, { + ...parameters, + abi: blueAbi, + address: morpho, + functionName: "nonce", + args: [user], + }), + readContract(client, { + ...parameters, + abi: blueAbi, + address: morpho, + functionName: "isAuthorized", + args: [user, generalAdapter1], + }), + ]); const ethPrice = await readContract(client, { ...parameters, @@ -231,6 +254,8 @@ export async function fetchAaveV3Positions( totalBorrow, isActive, ethPrice, + morphoNonce, + isBundlerManaging, }, }; }), @@ -376,6 +401,8 @@ export async function fetchAaveV3Positions( maxWithdraw: maxCollateral, collateralPriceEth: collateralData.ethPrice, loanPriceEth: loanData.ethPrice, + morphoNonce: loanData.morphoNonce, + isBundlerManaging: loanData.isBundlerManaging, }), ); } diff --git a/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts b/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts index 4c09addd..a7b202ed 100644 --- a/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts +++ b/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts @@ -1,4 +1,9 @@ -import type { Address, ChainId, MarketId, Token } from "@morpho-org/blue-sdk"; +import type { + Address, + ChainId, + MarketParams, + Token, +} from "@morpho-org/blue-sdk"; import type { MigrationBundle } from "../../types/actions.js"; import type { @@ -19,12 +24,12 @@ export namespace MigratableBorrowPosition { collateralAmount: bigint; /** The borrow amount to migrate. */ borrowAmount: bigint; - /** The id of the market to migrate to. */ - marketTo: MarketId; + /** The market to migrate to. */ + marketTo: MarketParams; /** Slippage tolerance for the current position (optional). */ slippageFrom?: bigint; - /** Slippage tolerance for the target market (optional). */ - slippageTo?: bigint; + /** The maximum amount of borrow shares mint (protects the sender from unexpected slippage). */ + minSharePrice: bigint; } } @@ -56,6 +61,10 @@ export interface IMigratableBorrowPosition { maxRepay: { value: bigint; limiter: BorrowMigrationLimiter }; /** The liquidation loan to value (LLTV) of the market */ lltv: bigint; + /** Whether the migration adapter is authorized to manage user's position on blue */ + isBundlerManaging: boolean; + /** User nonce on morpho contract */ + morphoNonce: bigint; } /** @@ -76,6 +85,8 @@ export abstract class MigratableBorrowPosition public readonly maxRepay; public readonly maxWithdraw; public readonly lltv; + public readonly isBundlerManaging; + public readonly morphoNonce; /** * Creates an instance of MigratableBorrowPosition. @@ -95,6 +106,8 @@ export abstract class MigratableBorrowPosition this.collateralApy = config.collateralApy; this.maxRepay = config.maxRepay; this.lltv = config.lltv; + this.isBundlerManaging = config.isBundlerManaging; + this.morphoNonce = config.morphoNonce; } abstract getLtv(options?: { withdrawn?: bigint; repaid?: bigint }): @@ -112,7 +125,6 @@ export abstract class MigratableBorrowPosition */ abstract getMigrationTx( args: MigratableBorrowPosition.Args, - chainId: ChainId, supportsSignature: boolean, ): MigrationBundle; } diff --git a/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts b/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts index ac070a24..81ac6d9f 100644 --- a/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts +++ b/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts @@ -1,8 +1,40 @@ -import { MathLib, type Token } from "@morpho-org/blue-sdk"; +import { + DEFAULT_SLIPPAGE_TOLERANCE, + MathLib, + type Token, + getChainAddresses, +} from "@morpho-org/blue-sdk"; -import { maxUint256, parseUnits } from "viem"; -import type { MigrationBundle } from "../../types/actions.js"; -import { MigratableProtocol } from "../../types/index.js"; +import { + blueAbi, + getAuthorizationTypedData, + getPermitTypedData, +} from "@morpho-org/blue-sdk-viem"; +import type { + Action, + SignatureRequirement, +} from "@morpho-org/bundler-sdk-viem"; +import BundlerAction from "@morpho-org/bundler-sdk-viem/src/BundlerAction.js"; +import { Time } from "@morpho-org/morpho-ts"; +import { + type Account, + type Client, + encodeFunctionData, + maxUint256, + parseUnits, + verifyTypedData, +} from "viem"; +import { signTypedData } from "viem/actions"; +import { aTokenV3Abi } from "../../abis/aaveV3.js"; +import type { + MigrationBundle, + MigrationTransactionRequirement, +} from "../../types/actions.js"; +import { + BorrowMigrationLimiter, + MigratableProtocol, + SupplyMigrationLimiter, +} from "../../types/index.js"; import { type IMigratableBorrowPosition, MigratableBorrowPosition, @@ -55,7 +87,238 @@ export class MigratableBorrowPosition_AaveV3 return this._nonce; } - getMigrationTx(): MigrationBundle { - throw "not implemented"; // TODO + getMigrationTx( + { + collateralAmount, + borrowAmount, + marketTo, + slippageFrom = DEFAULT_SLIPPAGE_TOLERANCE, + minSharePrice, + }: MigratableBorrowPosition.Args, + supportsSignature = true, + ): MigrationBundle { + if ( + marketTo.collateralToken !== this.collateralToken.address || + marketTo.loanToken !== this.loanToken.address + ) + throw new Error("Invalid market"); + + const signRequirements: SignatureRequirement[] = []; + const txRequirements: MigrationTransactionRequirement[] = []; + const actions: Action[] = []; + + const user = this.user; + const chainId = this.chainId; + + const { + morpho, + bundler3: { generalAdapter1, aaveV3CoreMigrationAdapter }, + } = getChainAddresses(chainId); + if (aaveV3CoreMigrationAdapter == null) + throw new Error("missing aaveV3CoreMigrationAdapter address"); + + const aToken = this.aToken; + + let migratedBorrow = borrowAmount; + let migratedCollateral = collateralAmount; + + const migrateMaxBorrow = + this.maxRepay.limiter === BorrowMigrationLimiter.position && + this.maxRepay.value === migratedBorrow; + if (migrateMaxBorrow) { + migratedBorrow = maxUint256; + } + + const migrateMaxCollateral = + this.maxWithdraw.limiter === SupplyMigrationLimiter.position && + this.maxWithdraw.value === migratedCollateral; + if (migrateMaxCollateral) { + migratedCollateral = maxUint256; + } + + if (supportsSignature) { + const deadline = Time.timestamp() + Time.s.from.d(1n); + const nonce = this._nonce; + + if (migratedBorrow > 0n && !this.isBundlerManaging) { + const authorization = { + authorizer: user, + authorized: generalAdapter1, + isAuthorized: true, + deadline, + nonce: this.morphoNonce, + }; + + const authorizeAction: Action = { + type: "morphoSetAuthorizationWithSig", + args: [authorization, null], + }; + + actions.push(authorizeAction); + + signRequirements.push({ + action: authorizeAction, + async sign(client: Client, account: Account = client.account!) { + let signature = authorizeAction.args[1]; + if (signature != null) return signature; + + const typedData = getAuthorizationTypedData(authorization, chainId); + signature = await signTypedData(client, { + ...typedData, + account, + }); + + await verifyTypedData({ + ...typedData, + address: user, // Verify against the authorization's owner. + signature, + }); + + return (authorizeAction.args[1] = signature); + }, + }); + } + + if (migratedCollateral > 0n) { + const permitAction: Action = { + type: "permit", + args: [user, aToken.address, migratedCollateral, deadline, null], + }; + + actions.push(permitAction); + + signRequirements.push({ + action: permitAction, + async sign(client: Client, account: Account = client.account!) { + let signature = permitAction.args[4]; + if (signature != null) return signature; // action is already signed + + const typedData = getPermitTypedData( + { + erc20: aToken, + owner: user, + spender: generalAdapter1, + allowance: migratedCollateral, + nonce, + deadline, + }, + chainId, + ); + signature = await signTypedData(client, { + ...typedData, + account, + }); + + await verifyTypedData({ + ...typedData, + address: user, // Verify against the permit's owner. + signature, + }); + + return (permitAction.args[4] = signature); + }, + }); + } + } else { + if (migratedBorrow > 0n && !this.isBundlerManaging) { + txRequirements.push({ + type: "morphoSetAuthorization", + args: [generalAdapter1, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [generalAdapter1, true], + }), + }, + }); + } + + if (migratedCollateral > 0n) + txRequirements.push({ + type: "erc20Approve", + args: [aToken.address, generalAdapter1, migratedCollateral], + tx: { + to: aToken.address, + data: encodeFunctionData({ + abi: aTokenV3Abi, + functionName: "approve", + args: [generalAdapter1, migratedCollateral], + }), + }, + }); + } + + const borrowActions: Action[] = + migratedBorrow > 0n + ? [ + { + type: "morphoBorrow", + args: [ + marketTo, + migrateMaxBorrow + ? MathLib.wMulUp(this.borrow, MathLib.WAD + slippageFrom) + : migratedBorrow, + 0n, + minSharePrice, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Repay", + args: [this.loanToken.address, maxUint256, user, 2n], + }, + ] + : []; + + if (migratedCollateral > 0n) { + const callbackActions = borrowActions.concat( + { + type: "erc20TransferFrom", + args: [ + aToken.address, + migratedCollateral, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Withdraw", + args: [ + this.collateralToken.address, + migratedCollateral, + generalAdapter1, + ], + }, + ); + actions.push( + { + type: "morphoSupplyCollateral", + args: [marketTo, collateralAmount, user, callbackActions], + }, + { + type: "erc20Transfer", + args: [ + migrateMaxCollateral + ? this.collateralToken.address + : this.aToken.address, + user, + maxUint256, + ], + }, + ); + if (migrateMaxCollateral) actions.push(); + } else { + actions.push(...borrowActions); + } + + return { + actions, + requirements: { + signatures: signRequirements, + txs: txRequirements, + }, + tx: () => BundlerAction.encodeBundle(chainId, actions), + }; } } diff --git a/packages/migration-sdk-viem/src/types/actions.ts b/packages/migration-sdk-viem/src/types/actions.ts index 5a29a4b0..42a75146 100644 --- a/packages/migration-sdk-viem/src/types/actions.ts +++ b/packages/migration-sdk-viem/src/types/actions.ts @@ -13,6 +13,9 @@ export interface MigrationTransactionRequirementArgs { /* ERC20 */ erc20Approve: [asset: Address, recipient: Address, amount: bigint]; + + /* Morpho */ + morphoSetAuthorization: [authorized: Address, isAuthorized: boolean]; } export type MigrationTransactionRequirementType = diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts index 7eed9aeb..fc58c1ef 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts @@ -7,9 +7,22 @@ import { import { ChainId, addresses } from "@morpho-org/blue-sdk"; +import { fetchAccrualPosition } from "@morpho-org/blue-sdk-viem"; +import { markets } from "@morpho-org/morpho-test"; import type { ViemTestContext } from "@morpho-org/test/vitest"; -import { type Address, parseEther, parseUnits } from "viem"; +import { + type Address, + erc20Abi, + maxUint256, + parseEther, + parseUnits, +} from "viem"; +import { readContract, sendTransaction } from "viem/actions"; import { type TestAPI, describe, expect } from "vitest"; +import { + aTokenV3Abi, + variableDebtTokenV3Abi, +} from "../../../src/abis/aaveV3.js"; import { MIGRATION_ADDRESSES } from "../../../src/config.js"; import { MigratableBorrowPosition_AaveV3 } from "../../../src/positions/borrow/aaveV3.borrow.js"; import { MigratableSupplyPosition_AaveV3 } from "../../../src/positions/supply/aaveV3.supply.js"; @@ -18,26 +31,36 @@ import { test } from "../setup.js"; const TEST_CONFIGS = [ { chainId: ChainId.EthMainnet, - aUsdc: "0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c", + aWstEth: "0x0B925eD163218f6662a35e0f0371Ac234f9E9371", + wstEth: addresses[ChainId.EthMainnet].wstEth, + variableDebtToken: "0xeA51d7853EEFb32b6ee06b1C12E6dcCA88Be0fFE", testFn: test[ChainId.EthMainnet] as TestAPI, - marketTo: "0x", //TODO + marketTo: markets[ChainId.EthMainnet].eth_wstEth_2, }, { chainId: ChainId.BaseMainnet, - aUsdc: "0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB", + aWstEth: "0x99CBC45ea5bb7eF3a5BC08FB1B7E56bB2442Ef0D", + wstEth: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + variableDebtToken: "0x24e6e0795b3c7c71D965fCc4f371803d1c1DcA1E", testFn: test[ChainId.BaseMainnet] as TestAPI, - marketTo: "0x", //TODO + marketTo: markets[ChainId.BaseMainnet].eth_wstEth, }, ] as const; describe("Borrow position on AAVE V3", () => { - for (const { chainId, aUsdc, testFn } of TEST_CONFIGS) { + for (const { + chainId, + aWstEth, + wstEth, + testFn, + marketTo, + variableDebtToken, + } of TEST_CONFIGS) { const { pool } = MIGRATION_ADDRESSES[chainId].aaveV3; const { - // bundler3: { generalAdapter1, aaveV3CoreMigrationAdapter }, + bundler3: { generalAdapter1, aaveV3CoreMigrationAdapter }, wNative, usdc, - wstEth, } = addresses[chainId]; const writeSupply = async ( @@ -84,10 +107,10 @@ describe("Borrow position on AAVE V3", () => { testFn( "should fetch user position", async ({ client }: ViemTestContext) => { - const collateralAmount = parseUnits("1000000", 6); + const collateralAmount = parseEther("10"); const borrowAmount = parseEther("1"); - await writeSupply(client, usdc, collateralAmount, true); + await writeSupply(client, wstEth, collateralAmount, true); await writeBorrow(client, wNative, borrowAmount); const allPositions = await fetchMigratablePositions( @@ -108,11 +131,11 @@ describe("Borrow position on AAVE V3", () => { expect(position.user).toEqual(client.account.address); expect(position.loanToken.address).toEqual(wNative); expect(position.nonce).toEqual(0n); - expect(position.aToken.address).toEqual(aUsdc); + expect(position.aToken.address).toEqual(aWstEth); expect(position.collateral).toBeGreaterThanOrEqual(collateralAmount); //interest accrued expect(position.borrow).toBeGreaterThanOrEqual(borrowAmount); //interest accrued expect(position.chainId).toEqual(chainId); - expect(position.collateralToken.address).toEqual(usdc); + expect(position.collateralToken.address).toEqual(wstEth); expect(position.loanToken.address).toEqual(wNative); expect(position.maxRepay.limiter).toEqual( BorrowMigrationLimiter.position, @@ -128,8 +151,8 @@ describe("Borrow position on AAVE V3", () => { testFn( "shouldn't fetch user position if multiple collaterals", async ({ client }) => { - const collateralAmount1 = parseUnits("100000", 6); - const collateralAmount2 = parseUnits("1", 18); + const collateralAmount1 = parseUnits("1000", 6); + const collateralAmount2 = parseEther("10"); const borrowAmount = parseEther("1"); await writeSupply(client, usdc, collateralAmount1, true); @@ -151,12 +174,12 @@ describe("Borrow position on AAVE V3", () => { testFn( "should fetch multiple user positions if only one collateral", async ({ client }) => { - const collateralAmount = parseUnits("100000", 6); - const pureSupply = parseUnits("10", 18); + const collateralAmount = parseEther("10"); + const pureSupply = parseUnits("10000", 6); const borrowAmount = parseEther("1"); - await writeSupply(client, usdc, collateralAmount, true); - await writeSupply(client, wstEth, pureSupply, false); + await writeSupply(client, wstEth, collateralAmount, true); + await writeSupply(client, usdc, pureSupply, false); await writeBorrow(client, wNative, borrowAmount); const allPositions = await fetchMigratablePositions( @@ -178,19 +201,19 @@ describe("Borrow position on AAVE V3", () => { const position = aaveV3Positions[1] as MigratableBorrowPosition_AaveV3; - expect(position.collateralToken.address).toBe(usdc); + expect(position.collateralToken.address).toBe(wstEth); }, ); testFn( "shouldn't fetch user position if multiple loans", async ({ client }) => { - const collateralAmount = parseUnits("100000", 6); - const borrowAmount1 = parseUnits("1", 18); + const collateralAmount = parseEther("10"); + const borrowAmount1 = parseUnits("1000", 6); const borrowAmount2 = parseEther("1"); - await writeSupply(client, usdc, collateralAmount, true); - await writeBorrow(client, wstEth, borrowAmount1); + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, usdc, borrowAmount1); await writeBorrow(client, wNative, borrowAmount2); const allPositions = await fetchMigratablePositions( @@ -208,9 +231,9 @@ describe("Borrow position on AAVE V3", () => { testFn( "shouldn't fetch user collateral positions if no borrow", async ({ client }) => { - const collateralAmount = parseUnits("100000", 6); + const collateralAmount = parseEther("10"); - await writeSupply(client, usdc, collateralAmount, true); + await writeSupply(client, wstEth, collateralAmount, true); const allPositions = await fetchMigratablePositions( client.account.address, @@ -230,15 +253,15 @@ describe("Borrow position on AAVE V3", () => { testFn( "should fetch user position with limited liquidity", async ({ client }) => { - const collateralAmount = parseUnits("1000000", 6); + const collateralAmount = parseEther("10"); const borrowAmount = parseEther("5"); - const liquidity = parseUnits("100000", 6); + const liquidity = parseEther("6"); - await writeSupply(client, usdc, collateralAmount, true); + await writeSupply(client, wstEth, collateralAmount, true); await writeBorrow(client, wNative, borrowAmount); await client.deal({ - erc20: usdc, - account: aUsdc, + erc20: wstEth, + account: aWstEth, amount: liquidity, }); @@ -262,6 +285,181 @@ describe("Borrow position on AAVE V3", () => { }); }, ); + + testFn.only( + "should partially migrate user position", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + const migratedBorrow = borrowAmount / 2n; + const migratedCollateral = collateralAmount / 2n; + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + + const position = + aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: migratedCollateral, + minSharePrice, + }, + true, + ); + + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWstEth, + migratedCollateral, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + migratedCollateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [ + aWstEth, + migratedCollateral, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Withdraw", + args: [wstEth, migratedCollateral, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWstEth, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV3CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV3Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV3Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toEqual(migratedCollateral); + expect(finalPositionTo.borrowAssets).approximately( + migratedBorrow, + 2n, + ); + + expect(finalCollateralFrom).approximately( + collateralAmount - migratedCollateral, + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalDebtFrom).approximately( + borrowAmount - migratedBorrow, + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }, + ); }); } }); From 4d024a73c88ddd6cd76f3b956cea21a3c3bc5c3d Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Tue, 4 Mar 2025 15:08:27 +0100 Subject: [PATCH 05/12] fix(bundler-sdk-viem): allow transfer between adapters --- packages/bundler-sdk-viem/src/BundlerAction.ts | 5 ++++- packages/bundler-sdk-viem/src/types/actions.ts | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/bundler-sdk-viem/src/BundlerAction.ts b/packages/bundler-sdk-viem/src/BundlerAction.ts index 9c7c3730..92175afc 100644 --- a/packages/bundler-sdk-viem/src/BundlerAction.ts +++ b/packages/bundler-sdk-viem/src/BundlerAction.ts @@ -471,14 +471,17 @@ export namespace BundlerAction { asset: Address, recipient: Address, amount: bigint, + adapter?: Address, ): BundlerCall[] { const { bundler3: { generalAdapter1 }, } = getChainAddresses(chainId); + adapter ??= generalAdapter1; + return [ { - to: generalAdapter1, + to: adapter, data: encodeFunctionData({ abi: coreAdapterAbi, functionName: "erc20Transfer", diff --git a/packages/bundler-sdk-viem/src/types/actions.ts b/packages/bundler-sdk-viem/src/types/actions.ts index bdef2cc9..4723217d 100644 --- a/packages/bundler-sdk-viem/src/types/actions.ts +++ b/packages/bundler-sdk-viem/src/types/actions.ts @@ -39,7 +39,12 @@ export interface Permit2PermitSingle { export interface ActionArgs { /* ERC20 */ nativeTransfer: [owner: Address, recipient: Address, amount: bigint]; - erc20Transfer: [asset: Address, recipient: Address, amount: bigint]; + erc20Transfer: [ + asset: Address, + recipient: Address, + amount: bigint, + adapter?: Address, + ]; erc20TransferFrom: [asset: Address, amount: bigint, recipient?: Address]; /* ERC20Wrapper */ From 34a7eab23e975f07e00d9b47f0a403370888aee0 Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Tue, 4 Mar 2025 15:09:24 +0100 Subject: [PATCH 06/12] fix(migration-sdk-viem): fix and test full aave-v3 borrow migration --- .../src/positions/borrow/aaveV3.borrow.ts | 17 + .../test/e2e/aaveV3/borrow.test.ts | 517 ++++++++++++------ 2 files changed, 367 insertions(+), 167 deletions(-) diff --git a/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts b/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts index 81ac6d9f..3fe3f75c 100644 --- a/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts +++ b/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts @@ -272,6 +272,23 @@ export class MigratableBorrowPosition_AaveV3 ] : []; + if (migrateMaxBorrow && slippageFrom > 0n) + borrowActions.push( + { + type: "erc20Transfer", + args: [ + marketTo.loanToken, + generalAdapter1, + maxUint256, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "morphoRepay", + args: [marketTo, maxUint256, 0n, maxUint256, user, []], + }, + ); + if (migratedCollateral > 0n) { const callbackActions = borrowActions.concat( { diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts index fc58c1ef..7a68e2b9 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts @@ -5,7 +5,12 @@ import { fetchMigratablePositions, } from "../../../src/index.js"; -import { ChainId, addresses } from "@morpho-org/blue-sdk"; +import { + ChainId, + DEFAULT_SLIPPAGE_TOLERANCE, + MathLib, + addresses, +} from "@morpho-org/blue-sdk"; import { fetchAccrualPosition } from "@morpho-org/blue-sdk-viem"; import { markets } from "@morpho-org/morpho-test"; @@ -286,180 +291,358 @@ describe("Borrow position on AAVE V3", () => { }, ); - testFn.only( - "should partially migrate user position", - async ({ client }) => { - const collateralAmount = parseEther("10"); - const borrowAmount = parseEther("3"); - - const migratedBorrow = borrowAmount / 2n; - const migratedCollateral = collateralAmount / 2n; - - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, wNative, borrowAmount); - - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV3] }, - ); - - const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; - expect(aaveV3Positions).toBeDefined(); - expect(aaveV3Positions).toHaveLength(1); - - const position = - aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; - expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); - - // initial share price is 10^-6 because of virtual shares - const minSharePrice = parseUnits("1", 21); - - const migrationBundle = position.getMigrationTx( - { + testFn("should partially migrate user position", async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + const migratedBorrow = borrowAmount / 2n; + const migratedCollateral = collateralAmount / 2n; + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + + const position = aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: migratedCollateral, + minSharePrice, + }, + true, + ); + + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWstEth, + migratedCollateral, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ marketTo, - borrowAmount: migratedBorrow, - collateralAmount: migratedCollateral, - minSharePrice, - }, - true, + migratedCollateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [ + aWstEth, + migratedCollateral, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Withdraw", + args: [wstEth, migratedCollateral, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWstEth, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV3CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV3Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV3Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toEqual(migratedCollateral); + expect(finalPositionTo.borrowAssets).approximately(migratedBorrow, 2n); + + expect(finalCollateralFrom).toBeGreaterThan( + collateralAmount - migratedCollateral, + ); + expect(finalCollateralFrom).toBeLessThan( + collateralAmount - migratedCollateral + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); + expect(finalDebtFrom).toBeLessThan( + borrowAmount - migratedBorrow + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, ); + } + }); - expect(migrationBundle.requirements.txs).toHaveLength(0); - expect(migrationBundle.requirements.signatures).toHaveLength(2); - expect(migrationBundle.actions).toEqual([ - { - args: [ + testFn.only("should fully migrate user position", async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + + const position = aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: position.borrow, + collateralAmount: position.collateral, + minSharePrice, + }, + true, + ); + + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWstEth, + maxUint256, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + position.collateral, + client.account.address, + [ { - authorizer: client.account.address, - authorized: generalAdapter1, - isAuthorized: true, - deadline: expect.any(BigInt), - nonce: 0n, + type: "morphoBorrow", + args: [ + marketTo, + MathLib.wMulUp( + borrowAmount, + MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, + ), + 0n, + minSharePrice, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20Transfer", + args: [ + marketTo.loanToken, + generalAdapter1, + maxUint256, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "morphoRepay", + args: [ + marketTo, + maxUint256, + 0n, + maxUint256, + client.account.address, + [], + ], + }, + { + type: "erc20TransferFrom", + args: [aWstEth, maxUint256, aaveV3CoreMigrationAdapter], + }, + { + type: "aaveV3Withdraw", + args: [wstEth, maxUint256, generalAdapter1], }, - null, - ], - type: "morphoSetAuthorizationWithSig", - }, - { - args: [ - client.account.address, - aWstEth, - migratedCollateral, - expect.any(BigInt), - null, - ], - type: "permit", - }, - { - args: [ - marketTo, - migratedCollateral, - client.account.address, - [ - { - type: "morphoBorrow", - args: [ - marketTo, - migratedBorrow, - 0n, - minSharePrice, - aaveV3CoreMigrationAdapter, - ], - }, - { - type: "aaveV3Repay", - args: [wNative, maxUint256, client.account.address, 2n], - }, - { - type: "erc20TransferFrom", - args: [ - aWstEth, - migratedCollateral, - aaveV3CoreMigrationAdapter, - ], - }, - { - type: "aaveV3Withdraw", - args: [wstEth, migratedCollateral, generalAdapter1], - }, - ], ], - type: "morphoSupplyCollateral", - }, - { - type: "erc20Transfer", - args: [aWstEth, client.account.address, maxUint256], - }, - ]); - - await migrationBundle.requirements.signatures[0]!.sign(client); - await migrationBundle.requirements.signatures[1]!.sign(client); - - await sendTransaction(client, migrationBundle.tx()); - - const transferredAssets = [wNative, wstEth, aWstEth]; - const adapters = [generalAdapter1, aaveV3CoreMigrationAdapter]; - - const [ - finalPositionTo, - finalCollateralFrom, - finalDebtFrom, - adaptersBalances, - ] = await Promise.all([ - fetchAccrualPosition(client.account.address, marketTo.id, client), - readContract(client, { - abi: aTokenV3Abi, - address: aWstEth, - functionName: "balanceOf", - args: [client.account.address], - }), - readContract(client, { - abi: variableDebtTokenV3Abi, - address: variableDebtToken, - functionName: "balanceOf", - args: [client.account.address], - }), - Promise.all( - transferredAssets.flatMap((asset) => - adapters.map(async (adapter) => ({ - balance: await readContract(client, { - abi: erc20Abi, - address: asset, - functionName: "balanceOf", - args: [adapter], - }), - asset, - adapter, - })), - ), + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [wstEth, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV3CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV3Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV3Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), ), - ]); - - expect(finalPositionTo.collateral).toEqual(migratedCollateral); - expect(finalPositionTo.borrowAssets).approximately( - migratedBorrow, - 2n, + ), + ]); + + expect(finalPositionTo.collateral).toBeGreaterThan(collateralAmount); + expect(finalPositionTo.collateral).toBeLessThanOrEqual( + collateralAmount + 10n ** 12n, + ); // interest accrued (empirical) + expect(finalPositionTo.borrowAssets).toBeGreaterThan(borrowAmount); + expect(finalPositionTo.borrowAssets).toBeLessThanOrEqual( + borrowAmount + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalCollateralFrom).toBe(0n); + expect(finalDebtFrom).toBe(0n); + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, ); - - expect(finalCollateralFrom).approximately( - collateralAmount - migratedCollateral, - 10n ** 12n, - ); // interest accrued (empirical) - - expect(finalDebtFrom).approximately( - borrowAmount - migratedBorrow, - 10n ** 12n, - ); // interest accrued (empirical) - - for (const { balance, asset, adapter } of adaptersBalances) { - expect(balance).to.equal( - 0n, - `Adapter ${adapter} shouldn't hold ${asset}.`, - ); - } - }, - ); + } + }); }); } }); From 4fdfe8bdef3ff10ff9608cccb41c10df81d145e0 Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Fri, 28 Feb 2025 16:54:19 +0100 Subject: [PATCH 07/12] test(migration-sdk-viem): test aave-v3 borrow migration without signature --- .../test/e2e/aaveV3/borrow.test.ts | 409 +++++++++++++++++- 1 file changed, 407 insertions(+), 2 deletions(-) diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts index 7a68e2b9..51ce6991 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts @@ -12,11 +12,12 @@ import { addresses, } from "@morpho-org/blue-sdk"; -import { fetchAccrualPosition } from "@morpho-org/blue-sdk-viem"; +import { blueAbi, fetchAccrualPosition } from "@morpho-org/blue-sdk-viem"; import { markets } from "@morpho-org/morpho-test"; import type { ViemTestContext } from "@morpho-org/test/vitest"; import { type Address, + encodeFunctionData, erc20Abi, maxUint256, parseEther, @@ -66,6 +67,7 @@ describe("Borrow position on AAVE V3", () => { bundler3: { generalAdapter1, aaveV3CoreMigrationAdapter }, wNative, usdc, + morpho, } = addresses[chainId]; const writeSupply = async ( @@ -461,7 +463,7 @@ describe("Borrow position on AAVE V3", () => { } }); - testFn.only("should fully migrate user position", async ({ client }) => { + testFn("should fully migrate user position", async ({ client }) => { const collateralAmount = parseEther("10"); const borrowAmount = parseEther("3"); @@ -643,6 +645,409 @@ describe("Borrow position on AAVE V3", () => { ); } }); + + testFn( + "should partially migrate user position without signature", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + const migratedBorrow = borrowAmount / 2n; + const migratedCollateral = collateralAmount / 2n; + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + + const position = + aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: migratedCollateral, + minSharePrice, + }, + false, + ); + + expect(migrationBundle.requirements.signatures).toHaveLength(0); + expect(migrationBundle.requirements.txs).toHaveLength(2); + expect(migrationBundle.requirements.txs[0]).toEqual({ + type: "morphoSetAuthorization", + args: [generalAdapter1, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [generalAdapter1, true], + }), + }, + }); + expect(migrationBundle.requirements.txs[1]).toEqual({ + type: "erc20Approve", + args: [aWstEth, generalAdapter1, migratedCollateral], + tx: { + to: aWstEth, + data: encodeFunctionData({ + abi: aTokenV3Abi, + functionName: "approve", + args: [generalAdapter1, migratedCollateral], + }), + }, + }); + + expect(migrationBundle.actions).toEqual([ + { + args: [ + marketTo, + migratedCollateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [ + aWstEth, + migratedCollateral, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Withdraw", + args: [wstEth, migratedCollateral, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWstEth, client.account.address, maxUint256], + }, + ]); + + await sendTransaction( + client, + migrationBundle.requirements.txs[0]!.tx, + ); + await sendTransaction( + client, + migrationBundle.requirements.txs[1]!.tx, + ); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV3CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV3Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV3Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toEqual(migratedCollateral); + expect(finalPositionTo.borrowAssets).approximately( + migratedBorrow, + 2n, + ); + + expect(finalCollateralFrom).toBeGreaterThan( + collateralAmount - migratedCollateral, + ); + expect(finalCollateralFrom).toBeLessThan( + collateralAmount - migratedCollateral + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); + expect(finalDebtFrom).toBeLessThan( + borrowAmount - migratedBorrow + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }, + ); + + testFn( + "should fully migrate user position without signature", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + + const position = + aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: position.borrow, + collateralAmount: position.collateral, + minSharePrice, + }, + false, + ); + + expect(migrationBundle.requirements.signatures).toHaveLength(0); + expect(migrationBundle.requirements.txs).toHaveLength(2); + expect(migrationBundle.requirements.txs[0]).toEqual({ + type: "morphoSetAuthorization", + args: [generalAdapter1, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [generalAdapter1, true], + }), + }, + }); + expect(migrationBundle.requirements.txs[1]).toEqual({ + type: "erc20Approve", + args: [aWstEth, generalAdapter1, maxUint256], + tx: { + to: aWstEth, + data: encodeFunctionData({ + abi: aTokenV3Abi, + functionName: "approve", + args: [generalAdapter1, maxUint256], + }), + }, + }); + + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWstEth, + maxUint256, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + position.collateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + MathLib.wMulUp( + borrowAmount, + MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, + ), + 0n, + minSharePrice, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20Transfer", + args: [ + marketTo.loanToken, + generalAdapter1, + maxUint256, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "morphoRepay", + args: [ + marketTo, + maxUint256, + 0n, + maxUint256, + client.account.address, + [], + ], + }, + { + type: "erc20TransferFrom", + args: [aWstEth, maxUint256, aaveV3CoreMigrationAdapter], + }, + { + type: "aaveV3Withdraw", + args: [wstEth, maxUint256, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [wstEth, client.account.address, maxUint256], + }, + ]); + + await sendTransaction( + client, + migrationBundle.requirements.txs[0]!.tx, + ); + await sendTransaction( + client, + migrationBundle.requirements.txs[1]!.tx, + ); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV3CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV3Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV3Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toBeGreaterThan(collateralAmount); + expect(finalPositionTo.collateral).toBeLessThanOrEqual( + collateralAmount + 10n ** 12n, + ); // interest accrued (empirical) + expect(finalPositionTo.borrowAssets).toBeGreaterThan(borrowAmount); + expect(finalPositionTo.borrowAssets).toBeLessThanOrEqual( + borrowAmount + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalCollateralFrom).toBe(0n); + expect(finalDebtFrom).toBe(0n); + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }, + ); }); } }); From 6c7949d50ef34a85b6570b43e0faf6939b557d9b Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Fri, 28 Feb 2025 17:06:57 +0100 Subject: [PATCH 08/12] test(migration-sdk-viem): test aave-v3 borrow migration limited by liquidity --- .../test/e2e/aaveV3/borrow.test.ts | 205 ++++++++++++++++-- 1 file changed, 182 insertions(+), 23 deletions(-) diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts index 51ce6991..14e6da07 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts @@ -895,29 +895,6 @@ describe("Borrow position on AAVE V3", () => { }); expect(migrationBundle.actions).toEqual([ - { - args: [ - { - authorizer: client.account.address, - authorized: generalAdapter1, - isAuthorized: true, - deadline: expect.any(BigInt), - nonce: 0n, - }, - null, - ], - type: "morphoSetAuthorizationWithSig", - }, - { - args: [ - client.account.address, - aWstEth, - maxUint256, - expect.any(BigInt), - null, - ], - type: "permit", - }, { args: [ marketTo, @@ -1048,6 +1025,188 @@ describe("Borrow position on AAVE V3", () => { } }, ); + + testFn( + "should partially migrate user position limited by aave v3 liquidity", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + const liquidity = parseEther("4"); + + const migratedBorrow = parseEther("1.5"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + await client.deal({ + erc20: wstEth, + account: aWstEth, + amount: liquidity, + }); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(1); + + const position = + aaveV3Positions[0]! as MigratableBorrowPosition_AaveV3; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV3); + expect(position.maxWithdraw.limiter).toEqual( + SupplyMigrationLimiter.liquidity, + ); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: position.maxWithdraw.value, + minSharePrice, + }, + true, + ); + + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWstEth, + liquidity, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + liquidity, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV3CoreMigrationAdapter, + ], + }, + { + type: "aaveV3Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [aWstEth, liquidity, aaveV3CoreMigrationAdapter], + }, + { + type: "aaveV3Withdraw", + args: [wstEth, liquidity, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWstEth, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV3CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV3Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV3Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toEqual(liquidity); + expect(finalPositionTo.borrowAssets).approximately( + migratedBorrow, + 2n, + ); + + expect(finalCollateralFrom).toBeGreaterThan( + collateralAmount - liquidity, + ); + expect(finalCollateralFrom).toBeLessThan( + collateralAmount - liquidity + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); + expect(finalDebtFrom).toBeLessThan( + borrowAmount - migratedBorrow + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }, + ); }); } }); From d23e42e8512b7fe06c86f2c7a935d8e63c0dd436 Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Tue, 4 Mar 2025 10:47:32 +0100 Subject: [PATCH 09/12] feat(migration-sdk-viem): implement aave V2 borrow migration --- .../migration-sdk-viem/src/abis/aaveV2.ts | 861 ++++++++++++ packages/migration-sdk-viem/src/config.ts | 6 + .../src/fetchers/aaveV2/aaveV2.fetchers.ts | 497 ++++--- .../src/positions/borrow/aaveV2.borrow.ts | 337 +++++ .../test/e2e/aaveV2/borrow.test.ts | 1164 +++++++++++++++++ 5 files changed, 2693 insertions(+), 172 deletions(-) create mode 100644 packages/migration-sdk-viem/src/positions/borrow/aaveV2.borrow.ts create mode 100644 packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts diff --git a/packages/migration-sdk-viem/src/abis/aaveV2.ts b/packages/migration-sdk-viem/src/abis/aaveV2.ts index 9793bd2c..cbf4c46a 100644 --- a/packages/migration-sdk-viem/src/abis/aaveV2.ts +++ b/packages/migration-sdk-viem/src/abis/aaveV2.ts @@ -1344,3 +1344,864 @@ export const aTokenV2Abi = [ type: "function", }, ] as const; + +export const addressesProviderAbi_v2 = [ + { + inputs: [{ internalType: "string", name: "marketId", type: "string" }], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "bytes32", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + { indexed: false, internalType: "bool", name: "hasProxy", type: "bool" }, + ], + name: "AddressSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "ConfigurationAdminUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "EmergencyAdminUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "LendingPoolCollateralManagerUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "LendingPoolConfiguratorUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "LendingPoolUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "LendingRateOracleUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "string", + name: "newMarketId", + type: "string", + }, + ], + name: "MarketIdSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "PriceOracleUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "bytes32", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "newAddress", + type: "address", + }, + ], + name: "ProxyCreated", + type: "event", + }, + { + inputs: [{ internalType: "bytes32", name: "id", type: "bytes32" }], + name: "getAddress", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getEmergencyAdmin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLendingPool", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLendingPoolCollateralManager", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLendingPoolConfigurator", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLendingRateOracle", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getMarketId", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolAdmin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPriceOracle", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "id", type: "bytes32" }, + { internalType: "address", name: "newAddress", type: "address" }, + ], + name: "setAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "id", type: "bytes32" }, + { + internalType: "address", + name: "implementationAddress", + type: "address", + }, + ], + name: "setAddressAsProxy", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "emergencyAdmin", type: "address" }, + ], + name: "setEmergencyAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "manager", type: "address" }], + name: "setLendingPoolCollateralManager", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "configurator", type: "address" }, + ], + name: "setLendingPoolConfiguratorImpl", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "pool", type: "address" }], + name: "setLendingPoolImpl", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "lendingRateOracle", type: "address" }, + ], + name: "setLendingRateOracle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "string", name: "marketId", type: "string" }], + name: "setMarketId", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "admin", type: "address" }], + name: "setPoolAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "priceOracle", type: "address" }], + name: "setPriceOracle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +export const aaveV2OracleAbi = [ + { + inputs: [ + { internalType: "address[]", name: "assets", type: "address[]" }, + { internalType: "address[]", name: "sources", type: "address[]" }, + { internalType: "address", name: "fallbackOracle", type: "address" }, + { internalType: "address", name: "weth", type: "address" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "asset", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "source", + type: "address", + }, + ], + name: "AssetSourceUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "fallbackOracle", + type: "address", + }, + ], + name: "FallbackOracleUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "weth", type: "address" }, + ], + name: "WethSet", + type: "event", + }, + { + inputs: [], + name: "WETH", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "asset", type: "address" }], + name: "getAssetPrice", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address[]", name: "assets", type: "address[]" }], + name: "getAssetsPrices", + outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getFallbackOracle", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "asset", type: "address" }], + name: "getSourceOfAsset", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address[]", name: "assets", type: "address[]" }, + { internalType: "address[]", name: "sources", type: "address[]" }, + ], + name: "setAssetSources", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "fallbackOracle", type: "address" }, + ], + name: "setFallbackOracle", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +export const variableDebtTokenV2Abi = [ + { + inputs: [ + { internalType: "address", name: "pool", type: "address" }, + { internalType: "address", name: "underlyingAsset", type: "address" }, + { internalType: "string", name: "name", type: "string" }, + { internalType: "string", name: "symbol", type: "string" }, + { + internalType: "address", + name: "incentivesController", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "fromUser", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "toUser", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "asset", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "BorrowAllowanceDelegated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "user", type: "address" }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "index", + type: "uint256", + }, + ], + name: "Burn", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "underlyingAsset", + type: "address", + }, + { indexed: true, internalType: "address", name: "pool", type: "address" }, + { + indexed: false, + internalType: "address", + name: "incentivesController", + type: "address", + }, + { + indexed: false, + internalType: "uint8", + name: "debtTokenDecimals", + type: "uint8", + }, + { + indexed: false, + internalType: "string", + name: "debtTokenName", + type: "string", + }, + { + indexed: false, + internalType: "string", + name: "debtTokenSymbol", + type: "string", + }, + { indexed: false, internalType: "bytes", name: "params", type: "bytes" }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { + indexed: true, + internalType: "address", + name: "onBehalfOf", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "index", + type: "uint256", + }, + ], + name: "Mint", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + inputs: [], + name: "DEBT_TOKEN_REVISION", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "POOL", + outputs: [ + { internalType: "contract ILendingPool", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UNDERLYING_ASSET_ADDRESS", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + ], + name: "allowance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "delegatee", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "approveDelegation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "fromUser", type: "address" }, + { internalType: "address", name: "toUser", type: "address" }, + ], + name: "borrowAllowance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "user", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "uint256", name: "index", type: "uint256" }, + ], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "subtractedValue", type: "uint256" }, + ], + name: "decreaseAllowance", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getIncentivesController", + outputs: [ + { + internalType: "contract IAaveIncentivesController", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "getScaledUserBalanceAndSupply", + outputs: [ + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "addedValue", type: "uint256" }, + ], + name: "increaseAllowance", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint8", name: "decimals", type: "uint8" }, + { internalType: "string", name: "name", type: "string" }, + { internalType: "string", name: "symbol", type: "string" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "user", type: "address" }, + { internalType: "address", name: "onBehalfOf", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "uint256", name: "index", type: "uint256" }, + ], + name: "mint", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "scaledBalanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "scaledTotalSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/packages/migration-sdk-viem/src/config.ts b/packages/migration-sdk-viem/src/config.ts index 816e756b..32746456 100644 --- a/packages/migration-sdk-viem/src/config.ts +++ b/packages/migration-sdk-viem/src/config.ts @@ -2,6 +2,7 @@ import { type Address, ChainId, addresses } from "@morpho-org/blue-sdk"; import type { Abi } from "viem"; import { + addressesProviderAbi_v2, lendingPoolAbi, protocolDataProviderAbi as protocolDataProviderAbi_v2, } from "./abis/aaveV2.js"; @@ -45,6 +46,7 @@ export interface ProtocolMigrationContracts { [MigratableProtocol.aaveV2]: { protocolDataProvider: Contract; lendingPool: Contract; + addressesProvider: Contract; } | null; [MigratableProtocol.aaveV3]: { pool: Contract; @@ -83,6 +85,10 @@ export const migrationAddressesRegistry = { address: "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9", abi: lendingPoolAbi, }, + addressesProvider: { + address: "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5", + abi: addressesProviderAbi_v2, + }, }, [MigratableProtocol.aaveV3]: { pool: { diff --git a/packages/migration-sdk-viem/src/fetchers/aaveV2/aaveV2.fetchers.ts b/packages/migration-sdk-viem/src/fetchers/aaveV2/aaveV2.fetchers.ts index d16b1460..a9f53268 100644 --- a/packages/migration-sdk-viem/src/fetchers/aaveV2/aaveV2.fetchers.ts +++ b/packages/migration-sdk-viem/src/fetchers/aaveV2/aaveV2.fetchers.ts @@ -1,19 +1,29 @@ -import { type Address, MathLib, Token } from "@morpho-org/blue-sdk"; -import { isDefined } from "@morpho-org/morpho-ts"; +import { type Address, MathLib, getChainAddresses } from "@morpho-org/blue-sdk"; +import { isDefined, values } from "@morpho-org/morpho-ts"; import { migrationAddresses } from "../../config.js"; import type { MigratablePosition } from "../../positions/index.js"; import { MigratableSupplyPosition_AaveV2 } from "../../positions/supply/aaveV2.supply.js"; import { + BorrowMigrationLimiter, MigratableProtocol, SupplyMigrationLimiter, } from "../../types/index.js"; -import type { FetchParameters } from "@morpho-org/blue-sdk-viem"; +import { + type FetchParameters, + blueAbi, + fetchToken, +} from "@morpho-org/blue-sdk-viem"; -import { type Client, erc20Abi } from "viem"; +import { type Client, erc20Abi, parseUnits } from "viem"; import { getChainId, readContract } from "viem/actions"; -import { aTokenV2Abi } from "../../abis/aaveV2.js"; +import { + aTokenV2Abi, + aaveV2OracleAbi, + variableDebtTokenV2Abi, +} from "../../abis/aaveV2.js"; +import { MigratableBorrowPosition_AaveV2 } from "../../positions/borrow/aaveV2.borrow.js"; import { rateToApy } from "../../utils/rates.js"; export async function fetchAaveV2Positions( @@ -24,35 +34,47 @@ export async function fetchAaveV2Positions( parameters.chainId ??= await getChainId(client); const chainId = parameters.chainId; + const { + morpho, + bundler3: { generalAdapter1 }, + } = getChainAddresses(chainId); const migrationContracts = migrationAddresses[chainId]?.[MigratableProtocol.aaveV2]; if (!migrationContracts) return []; - const [allATokens, userConfig, reservesList] = await Promise.all([ - readContract(client, { - ...parameters, - abi: migrationContracts.protocolDataProvider.abi, - address: migrationContracts.protocolDataProvider.address, - functionName: "getAllATokens", - args: [], - }), - readContract(client, { - ...parameters, - abi: migrationContracts.lendingPool.abi, - address: migrationContracts.lendingPool.address, - functionName: "getUserConfiguration", - args: [user], - }), - readContract(client, { - ...parameters, - abi: migrationContracts.lendingPool.abi, - address: migrationContracts.lendingPool.address, - functionName: "getReservesList", - args: [], - }), - ]); + const [allATokens, userConfig, reservesList, oracleAddress] = + await Promise.all([ + readContract(client, { + ...parameters, + abi: migrationContracts.protocolDataProvider.abi, + address: migrationContracts.protocolDataProvider.address, + functionName: "getAllATokens", + args: [], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.lendingPool.abi, + address: migrationContracts.lendingPool.address, + functionName: "getUserConfiguration", + args: [user], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.lendingPool.abi, + address: migrationContracts.lendingPool.address, + functionName: "getReservesList", + args: [], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.addressesProvider.abi, + address: migrationContracts.addressesProvider.address, + functionName: "getPriceOracle", + args: [], + }), + ]); /* cf https://docs.aave.com/developers/v/2.0/the-core-protocol/lendingpool#getuserconfiguration */ const orderedUserConfig = userConfig.data @@ -81,161 +103,292 @@ export async function fetchAaveV2Positions( }), ); - const isBorrowing = Object.values(userConfigByToken).some( - ({ isBorrowed }) => isBorrowed, - ); + const positionsData = ( + await Promise.all( + allATokens.map(async ({ tokenAddress }) => { + const [underlyingAddress, totalSupply, nonce, aTokenData] = + await Promise.all([ + readContract(client, { + ...parameters, + abi: aTokenV2Abi, + address: tokenAddress, + functionName: "UNDERLYING_ASSET_ADDRESS", + args: [], + }), + readContract(client, { + ...parameters, + abi: aTokenV2Abi, + address: tokenAddress, + functionName: "balanceOf", + args: [user], + }), + readContract(client, { + ...parameters, + abi: aTokenV2Abi, + address: tokenAddress, + functionName: "_nonces", + args: [user], + }), + fetchToken(tokenAddress, client, parameters), + ]); - const positions = await Promise.all( - allATokens.map(async ({ tokenAddress, symbol }) => { - const [ - underlyingAddress, - totalSupply, - nonce, - aTokenDecimals, - aTokenName, - ] = await Promise.all([ - readContract(client, { - ...parameters, - abi: aTokenV2Abi, - address: tokenAddress, - functionName: "UNDERLYING_ASSET_ADDRESS", - args: [], - }), - readContract(client, { - ...parameters, - abi: aTokenV2Abi, - address: tokenAddress, - functionName: "balanceOf", - args: [user], - }), - readContract(client, { - ...parameters, - abi: aTokenV2Abi, - address: tokenAddress, - functionName: "_nonces", - args: [user], - }), - readContract(client, { - ...parameters, - abi: aTokenV2Abi, - address: tokenAddress, - functionName: "decimals", - args: [], - }), - readContract(client, { - ...parameters, - abi: aTokenV2Abi, - address: tokenAddress, - functionName: "name", - args: [], - }), - ]); - - const aTokenData = new Token({ - address: tokenAddress, - symbol, - decimals: aTokenDecimals, - name: aTokenName, - }); - - if (totalSupply === 0n) return null; - - const userReserveConfig = userConfigByToken[underlyingAddress]; - - if (!userReserveConfig) return null; - - const [ - poolLiquidity, - [, , , , , usageAsCollateralEnabled, , , isActive], - { currentLiquidityRate }, - ] = await Promise.all([ - readContract(client, { - ...parameters, - abi: erc20Abi, - address: underlyingAddress, - functionName: "balanceOf", - args: [tokenAddress], - }), - readContract(client, { - ...parameters, - abi: migrationContracts.protocolDataProvider.abi, - address: migrationContracts.protocolDataProvider.address, - functionName: "getReserveConfigurationData", - args: [underlyingAddress], - }), - readContract(client, { + const userReserveConfig = userConfigByToken[underlyingAddress]; + + if (!userReserveConfig) return; + + const [ + poolLiquidity, + [ + , + , + liquidationThreshold, + , + , + usageAsCollateralEnabled, + , + , + isActive, + ], + { + currentLiquidityRate, + variableDebtTokenAddress, + currentVariableBorrowRate, + }, + underlying, + ] = await Promise.all([ + readContract(client, { + ...parameters, + abi: erc20Abi, + address: underlyingAddress, + functionName: "balanceOf", + args: [tokenAddress], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.protocolDataProvider.abi, + address: migrationContracts.protocolDataProvider.address, + functionName: "getReserveConfigurationData", + args: [underlyingAddress], + }), + readContract(client, { + ...parameters, + abi: migrationContracts.lendingPool.abi, + address: migrationContracts.lendingPool.address, + functionName: "getReserveData", + args: [underlyingAddress], + }), + fetchToken(underlyingAddress, client, parameters), + ]); + + const [totalBorrow, morphoNonce, isBundlerManaging] = await Promise.all( + [ + readContract(client, { + ...parameters, + abi: variableDebtTokenV2Abi, + address: variableDebtTokenAddress, + functionName: "balanceOf", + args: [user], + }), + readContract(client, { + ...parameters, + abi: blueAbi, + address: morpho, + functionName: "nonce", + args: [user], + }), + readContract(client, { + ...parameters, + abi: blueAbi, + address: morpho, + functionName: "isAuthorized", + args: [user, generalAdapter1], + }), + ], + ); + + const ethPrice = await readContract(client, { ...parameters, - abi: migrationContracts.lendingPool.abi, - address: migrationContracts.lendingPool.address, - functionName: "getReserveData", + abi: aaveV2OracleAbi, + address: oracleAddress, + functionName: "getAssetPrice", args: [underlyingAddress], - }), - ]); - - // TODO we only focus on pure suppliers now - // We need to check both `usageAsCollateralEnabled` and `userReserveConfig.isUsedAsCollateral`` - // Because `userReserveConfig.isUsedAsCollateral` is true by default for everyone when `usageAsCollateralEnabled` is false - if ( - userReserveConfig.isUsedAsCollateral && - usageAsCollateralEnabled && - isBorrowing - ) - return null; - - /* MAX */ - const max = (() => { - if (!isActive) - return { value: 0n, limiter: SupplyMigrationLimiter.withdrawPaused }; - - const maxWithdrawFromAvailableLiquidity = poolLiquidity; - const maxWithdrawFromSupplyBalance = totalSupply; - - const maxWithdraw = MathLib.min( - maxWithdrawFromAvailableLiquidity, - maxWithdrawFromSupplyBalance, - ); + }); - if (maxWithdraw === maxWithdrawFromAvailableLiquidity) - return { - value: maxWithdrawFromAvailableLiquidity, - limiter: SupplyMigrationLimiter.liquidity, - }; + // We need to check both `usageAsCollateralEnabled` and `userReserveConfig.isUsedAsCollateral`` + // Because `userReserveConfig.isUsedAsCollateral` is true by default for everyone when `usageAsCollateralEnabled` is false + const isCollateral = + userReserveConfig.isUsedAsCollateral && usageAsCollateralEnabled; - if (maxWithdraw === maxWithdrawFromSupplyBalance) - return { - value: maxWithdrawFromSupplyBalance, - limiter: SupplyMigrationLimiter.position, - }; - })()!; + return { + underlying, + supply: { + isCollateral, + poolLiquidity, + totalSupply, + isActive, + currentLiquidityRate, + aTokenData, + nonce, + ethPrice, + }, + borrow: { + liquidationThreshold, + currentVariableBorrowRate, + totalBorrow, + isActive, + ethPrice, + morphoNonce, + isBundlerManaging, + }, + }; + }), + ) + ).filter(isDefined); - return { - underlyingAddress, - supply: totalSupply, - supplyApy: rateToApy(currentLiquidityRate, "s", 27), - max, - nonce, - aToken: aTokenData, - }; - }), + const isBorrowing = values(userConfigByToken).some( + ({ isBorrowed }) => isBorrowed, ); - return positions - .filter(isDefined) - .flatMap(({ underlyingAddress, supply, supplyApy, max, nonce, aToken }) => { - if (supply > 0n) - return [ - new MigratableSupplyPosition_AaveV2({ + const positions: MigratablePosition[] = positionsData + .map( + ({ + underlying, + supply: { + isCollateral, + isActive, + poolLiquidity, + totalSupply, + currentLiquidityRate, + nonce, + aTokenData, + }, + }) => { + if (isBorrowing && isCollateral) return; + + /* MAX */ + const max = (() => { + if (!isActive) + return { + value: 0n, + limiter: SupplyMigrationLimiter.withdrawPaused, + }; + + const maxWithdrawFromAvailableLiquidity = poolLiquidity; + const maxWithdrawFromSupplyBalance = totalSupply; + + const maxWithdraw = MathLib.min( + maxWithdrawFromAvailableLiquidity, + maxWithdrawFromSupplyBalance, + ); + + if (maxWithdraw === maxWithdrawFromAvailableLiquidity) + return { + value: maxWithdrawFromAvailableLiquidity, + limiter: SupplyMigrationLimiter.liquidity, + }; + + if (maxWithdraw === maxWithdrawFromSupplyBalance) + return { + value: maxWithdrawFromSupplyBalance, + limiter: SupplyMigrationLimiter.position, + }; + })()!; + + if (totalSupply > 0n) + return new MigratableSupplyPosition_AaveV2({ user, - loanToken: underlyingAddress as Address, - supply, - supplyApy, + loanToken: underlying.address, + supply: totalSupply, + supplyApy: rateToApy(currentLiquidityRate, "s", 27), max, nonce, - aToken, + aToken: aTokenData, chainId, - }), - ]; + }); + }, + ) + .filter(isDefined); + + const collateralPositionsData = positionsData.filter( + ({ supply: { isCollateral, totalSupply } }) => + isCollateral && totalSupply > 0n, + ); + const borrowPositionsData = positionsData.filter( + ({ borrow: { totalBorrow } }) => totalBorrow > 0n, + ); + + // We only handle 1-Borrow 1-Collateral positions + if ( + collateralPositionsData.length === 1 && + borrowPositionsData.length === 1 + ) { + const { underlying: collateralToken, supply: collateralData } = + collateralPositionsData[0]!; + const { underlying: loanToken, borrow: loanData } = borrowPositionsData[0]!; + + /* MAX */ + const maxCollateral = (() => { + if (!collateralData.isActive) + return { + value: 0n, + limiter: SupplyMigrationLimiter.withdrawPaused, + }; + + const maxWithdrawFromAvailableLiquidity = collateralData.poolLiquidity; + const maxWithdrawFromSupplyBalance = collateralData.totalSupply; + + const maxWithdraw = MathLib.min( + maxWithdrawFromAvailableLiquidity, + maxWithdrawFromSupplyBalance, + ); + + if (maxWithdraw === maxWithdrawFromAvailableLiquidity) + return { + value: maxWithdrawFromAvailableLiquidity, + limiter: SupplyMigrationLimiter.liquidity, + }; + + if (maxWithdraw === maxWithdrawFromSupplyBalance) + return { + value: maxWithdrawFromSupplyBalance, + limiter: SupplyMigrationLimiter.position, + }; + })()!; + const maxBorrow = (() => { + if (!loanData.isActive) + return { + value: 0n, + limiter: BorrowMigrationLimiter.repayPaused, + }; + + return { + value: loanData.totalBorrow, + limiter: BorrowMigrationLimiter.position, + }; + })()!; + + positions.push( + new MigratableBorrowPosition_AaveV2({ + loanToken, + collateralToken, + collateral: collateralData.totalSupply, + borrow: loanData.totalBorrow, + collateralApy: rateToApy(collateralData.currentLiquidityRate, "s", 27), + borrowApy: rateToApy(loanData.currentVariableBorrowRate, "s", 27), + lltv: loanData.liquidationThreshold * parseUnits("1", 14), // lltv has 4 decimals on aave V2 + aToken: collateralData.aTokenData, + nonce: collateralData.nonce, + chainId, + user, + maxRepay: maxBorrow, + maxWithdraw: maxCollateral, + collateralPriceEth: collateralData.ethPrice, + loanPriceEth: loanData.ethPrice, + morphoNonce: loanData.morphoNonce, + isBundlerManaging: loanData.isBundlerManaging, + }), + ); + } - return []; - }); + return positions; } diff --git a/packages/migration-sdk-viem/src/positions/borrow/aaveV2.borrow.ts b/packages/migration-sdk-viem/src/positions/borrow/aaveV2.borrow.ts new file mode 100644 index 00000000..d6674a25 --- /dev/null +++ b/packages/migration-sdk-viem/src/positions/borrow/aaveV2.borrow.ts @@ -0,0 +1,337 @@ +import { + DEFAULT_SLIPPAGE_TOLERANCE, + MathLib, + type Token, + getChainAddresses, +} from "@morpho-org/blue-sdk"; + +import { + blueAbi, + getAuthorizationTypedData, + getPermitTypedData, +} from "@morpho-org/blue-sdk-viem"; +import type { + Action, + SignatureRequirement, +} from "@morpho-org/bundler-sdk-viem"; +import BundlerAction from "@morpho-org/bundler-sdk-viem/src/BundlerAction.js"; +import { Time } from "@morpho-org/morpho-ts"; +import { + type Account, + type Client, + encodeFunctionData, + maxUint256, + parseUnits, + verifyTypedData, +} from "viem"; +import { signTypedData } from "viem/actions"; +import { aTokenV2Abi } from "../../abis/aaveV2.js"; +import type { + MigrationBundle, + MigrationTransactionRequirement, +} from "../../types/actions.js"; +import { + BorrowMigrationLimiter, + MigratableProtocol, + SupplyMigrationLimiter, +} from "../../types/index.js"; +import { + type IMigratableBorrowPosition, + MigratableBorrowPosition, +} from "./index.js"; + +interface IMigratableBorrowPosition_AaveV2 + extends Omit { + nonce: bigint; + aToken: Token; + collateralPriceEth: bigint; + loanPriceEth: bigint; +} + +export class MigratableBorrowPosition_AaveV2 + extends MigratableBorrowPosition + implements IMigratableBorrowPosition_AaveV2 +{ + private _nonce; + public readonly aToken; + public readonly collateralPriceEth; + public readonly loanPriceEth; + + constructor(config: IMigratableBorrowPosition_AaveV2) { + super({ ...config, protocol: MigratableProtocol.aaveV2 }); + this.aToken = config.aToken; + this._nonce = config.nonce; + this.collateralPriceEth = config.collateralPriceEth; + this.loanPriceEth = config.loanPriceEth; + } + + getLtv({ + withdrawn = 0n, + repaid = 0n, + }: { withdrawn?: bigint; repaid?: bigint } = {}): bigint | null { + const totalCollateralEth = + ((this.collateral - withdrawn) * this.collateralPriceEth) / + parseUnits("1", this.collateralToken.decimals); + + const totalBorrowEth = + ((this.borrow - repaid) * this.loanPriceEth) / + parseUnits("1", this.loanToken.decimals); + + if (totalBorrowEth <= 0n) return null; + if (totalCollateralEth <= 0n) return maxUint256; + + return MathLib.wDivUp(totalBorrowEth, totalCollateralEth); + } + + get nonce() { + return this._nonce; + } + + getMigrationTx( + { + collateralAmount, + borrowAmount, + marketTo, + slippageFrom = DEFAULT_SLIPPAGE_TOLERANCE, + minSharePrice, + }: MigratableBorrowPosition.Args, + supportsSignature = true, + ): MigrationBundle { + if ( + marketTo.collateralToken !== this.collateralToken.address || + marketTo.loanToken !== this.loanToken.address + ) + throw new Error("Invalid market"); + + const signRequirements: SignatureRequirement[] = []; + const txRequirements: MigrationTransactionRequirement[] = []; + const actions: Action[] = []; + + const user = this.user; + const chainId = this.chainId; + + const { + morpho, + bundler3: { generalAdapter1, aaveV2MigrationAdapter }, + } = getChainAddresses(chainId); + if (aaveV2MigrationAdapter == null) + throw new Error("missing aaveV2MigrationAdapter address"); + + const aToken = this.aToken; + + let migratedBorrow = borrowAmount; + let migratedCollateral = collateralAmount; + + const migrateMaxBorrow = + this.maxRepay.limiter === BorrowMigrationLimiter.position && + this.maxRepay.value === migratedBorrow; + if (migrateMaxBorrow) { + migratedBorrow = maxUint256; + } + + const migrateMaxCollateral = + this.maxWithdraw.limiter === SupplyMigrationLimiter.position && + this.maxWithdraw.value === migratedCollateral; + if (migrateMaxCollateral) { + migratedCollateral = maxUint256; + } + + if (supportsSignature) { + const deadline = Time.timestamp() + Time.s.from.d(1n); + const nonce = this._nonce; + + if (migratedBorrow > 0n && !this.isBundlerManaging) { + const authorization = { + authorizer: user, + authorized: generalAdapter1, + isAuthorized: true, + deadline, + nonce: this.morphoNonce, + }; + + const authorizeAction: Action = { + type: "morphoSetAuthorizationWithSig", + args: [authorization, null], + }; + + actions.push(authorizeAction); + + signRequirements.push({ + action: authorizeAction, + async sign(client: Client, account: Account = client.account!) { + let signature = authorizeAction.args[1]; + if (signature != null) return signature; + + const typedData = getAuthorizationTypedData(authorization, chainId); + signature = await signTypedData(client, { + ...typedData, + account, + }); + + await verifyTypedData({ + ...typedData, + address: user, // Verify against the authorization's owner. + signature, + }); + + return (authorizeAction.args[1] = signature); + }, + }); + } + + if (migratedCollateral > 0n) { + const permitAction: Action = { + type: "permit", + args: [user, aToken.address, migratedCollateral, deadline, null], + }; + + actions.push(permitAction); + + signRequirements.push({ + action: permitAction, + async sign(client: Client, account: Account = client.account!) { + let signature = permitAction.args[4]; + if (signature != null) return signature; // action is already signed + + const typedData = getPermitTypedData( + { + erc20: aToken, + owner: user, + spender: generalAdapter1, + allowance: migratedCollateral, + nonce, + deadline, + }, + chainId, + ); + signature = await signTypedData(client, { + ...typedData, + account, + }); + + await verifyTypedData({ + ...typedData, + address: user, // Verify against the permit's owner. + signature, + }); + + return (permitAction.args[4] = signature); + }, + }); + } + } else { + if (migratedBorrow > 0n && !this.isBundlerManaging) { + txRequirements.push({ + type: "morphoSetAuthorization", + args: [generalAdapter1, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [generalAdapter1, true], + }), + }, + }); + } + + if (migratedCollateral > 0n) + txRequirements.push({ + type: "erc20Approve", + args: [aToken.address, generalAdapter1, migratedCollateral], + tx: { + to: aToken.address, + data: encodeFunctionData({ + abi: aTokenV2Abi, + functionName: "approve", + args: [generalAdapter1, migratedCollateral], + }), + }, + }); + } + + const borrowActions: Action[] = + migratedBorrow > 0n + ? [ + { + type: "morphoBorrow", + args: [ + marketTo, + migrateMaxBorrow + ? MathLib.wMulUp(this.borrow, MathLib.WAD + slippageFrom) + : migratedBorrow, + 0n, + minSharePrice, + aaveV2MigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [this.loanToken.address, maxUint256, user, 2n], + }, + ] + : []; + + if (migrateMaxBorrow && slippageFrom > 0n) + borrowActions.push( + { + type: "erc20Transfer", + args: [ + marketTo.loanToken, + generalAdapter1, + maxUint256, + aaveV2MigrationAdapter, + ], + }, + { + type: "morphoRepay", + args: [marketTo, maxUint256, 0n, maxUint256, user, []], + }, + ); + + if (migratedCollateral > 0n) { + const callbackActions = borrowActions.concat( + { + type: "erc20TransferFrom", + args: [aToken.address, migratedCollateral, aaveV2MigrationAdapter], + }, + { + type: "aaveV2Withdraw", + args: [ + this.collateralToken.address, + migratedCollateral, + generalAdapter1, + ], + }, + ); + actions.push( + { + type: "morphoSupplyCollateral", + args: [marketTo, collateralAmount, user, callbackActions], + }, + { + type: "erc20Transfer", + args: [ + migrateMaxCollateral + ? this.collateralToken.address + : this.aToken.address, + user, + maxUint256, + ], + }, + ); + if (migrateMaxCollateral) actions.push(); + } else { + actions.push(...borrowActions); + } + + return { + actions, + requirements: { + signatures: signRequirements, + txs: txRequirements, + }, + tx: () => BundlerAction.encodeBundle(chainId, actions), + }; + } +} diff --git a/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts new file mode 100644 index 00000000..44e757b8 --- /dev/null +++ b/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts @@ -0,0 +1,1164 @@ +import { + BorrowMigrationLimiter, + MigratableProtocol, + SupplyMigrationLimiter, + fetchMigratablePositions, +} from "../../../src/index.js"; + +import { + ChainId, + DEFAULT_SLIPPAGE_TOLERANCE, + MathLib, + addresses, +} from "@morpho-org/blue-sdk"; + +import { blueAbi, fetchAccrualPosition } from "@morpho-org/blue-sdk-viem"; +import { markets } from "@morpho-org/morpho-test"; +import type { ViemTestContext } from "@morpho-org/test/vitest"; +import { + type Address, + encodeFunctionData, + erc20Abi, + maxUint256, + parseEther, + parseUnits, +} from "viem"; +import { readContract, sendTransaction } from "viem/actions"; +import { type TestAPI, describe, expect } from "vitest"; +import { + aTokenV2Abi, + variableDebtTokenV2Abi, +} from "../../../src/abis/aaveV2.js"; +import { MIGRATION_ADDRESSES } from "../../../src/config.js"; +import { MigratableBorrowPosition_AaveV2 } from "../../../src/positions/borrow/aaveV2.borrow.js"; +import { MigratableSupplyPosition_AaveV2 } from "../../../src/positions/supply/aaveV2.supply.js"; +import { test } from "../setup.js"; + +describe("Borrow position on AAVE V2", () => { + const chainId = ChainId.EthMainnet; + const aWstEth: Address = "0x0B925eD163218f6662a35e0f0371Ac234f9E9371"; + const variableDebtToken: Address = + "0xeA51d7853EEFb32b6ee06b1C12E6dcCA88Be0fFE"; + const testFn = test[ChainId.EthMainnet] as TestAPI; + const marketTo = markets[ChainId.EthMainnet].eth_wstEth_2; + + const { lendingPool } = MIGRATION_ADDRESSES[chainId].aaveV2!; + const { + bundler3: { generalAdapter1, aaveV2CoreMigrationAdapter }, + wNative, + usdc, + wstEth, + morpho, + } = addresses[chainId]; + + const writeSupply = async ( + client: ViemTestContext["client"], + market: Address, + amount: bigint, + asCollateral = false, + ) => { + await client.deal({ + erc20: market, + amount: amount, + }); + await client.approve({ + address: market, + args: [lendingPool.address, amount], + }); + await client.writeContract({ + ...lendingPool, + functionName: "deposit", + args: [market, amount, client.account.address, 0], + }); + await client.writeContract({ + ...lendingPool, + functionName: "setUserUseReserveAsCollateral", + args: [market, asCollateral], + }); + + await client.mine({ blocks: 500 }); //accrue some interests + }; + + const writeBorrow = async ( + client: ViemTestContext["client"], + market: Address, + amount: bigint, + ) => { + await client.writeContract({ + ...lendingPool, + functionName: "borrow", + args: [market, amount, 2n, 0, client.account.address], + }); + }; + + describe(`on chain ${chainId}`, () => { + testFn( + "should fetch user position", + async ({ client }: ViemTestContext) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("1"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + + expect(position.protocol).toEqual(MigratableProtocol.aaveV2); + expect(position.user).toEqual(client.account.address); + expect(position.loanToken.address).toEqual(wNative); + expect(position.nonce).toEqual(0n); + expect(position.aToken.address).toEqual(aWstEth); + expect(position.collateral).toBeGreaterThanOrEqual(collateralAmount); //interest accrued + expect(position.borrow).toBeGreaterThanOrEqual(borrowAmount); //interest accrued + expect(position.chainId).toEqual(chainId); + expect(position.collateralToken.address).toEqual(wstEth); + expect(position.loanToken.address).toEqual(wNative); + expect(position.maxRepay.limiter).toEqual( + BorrowMigrationLimiter.position, + ); + expect(position.maxRepay.value).toEqual(position.borrow); + expect(position.maxWithdraw.limiter).toEqual( + SupplyMigrationLimiter.position, + ); + expect(position.maxWithdraw.value).toEqual(position.collateral); + }, + ); + + testFn( + "shouldn't fetch user position if multiple collaterals", + async ({ client }) => { + const collateralAmount1 = parseUnits("1000", 6); + const collateralAmount2 = parseEther("10"); + const borrowAmount = parseEther("1"); + + await writeSupply(client, usdc, collateralAmount1, true); + await writeSupply(client, wstEth, collateralAmount2, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(0); + }, + ); + + testFn( + "should fetch multiple user positions if only one collateral", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const pureSupply = parseUnits("10000", 6); + const borrowAmount = parseEther("1"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeSupply(client, usdc, pureSupply, false); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(2); + expect(aaveV2Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV2, + ); + expect(aaveV2Positions[1]).toBeInstanceOf( + MigratableBorrowPosition_AaveV2, + ); + + const position = aaveV2Positions[1] as MigratableBorrowPosition_AaveV2; + + expect(position.collateralToken.address).toBe(wstEth); + }, + ); + + testFn( + "shouldn't fetch user position if multiple loans", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount1 = parseUnits("1000", 6); + const borrowAmount2 = parseEther("1"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, usdc, borrowAmount1); + await writeBorrow(client, wNative, borrowAmount2); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(0); + }, + ); + + testFn( + "shouldn't fetch user collateral positions if no borrow", + async ({ client }) => { + const collateralAmount = parseEther("10"); + + await writeSupply(client, wstEth, collateralAmount, true); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + expect(aaveV2Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV2, + ); + }, + ); + + testFn( + "should fetch user position with limited liquidity", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("5"); + const liquidity = parseEther("6"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + await client.deal({ + erc20: wstEth, + account: aWstEth, + amount: liquidity, + }); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + + expect(position.maxWithdraw).toEqual({ + limiter: SupplyMigrationLimiter.liquidity, + value: liquidity, + }); + }, + ); + + testFn("should partially migrate user position", async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + const migratedBorrow = borrowAmount / 2n; + const migratedCollateral = collateralAmount / 2n; + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: migratedCollateral, + minSharePrice, + }, + true, + ); + + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWstEth, + migratedCollateral, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + migratedCollateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV2CoreMigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [aWstEth, migratedCollateral, aaveV2CoreMigrationAdapter], + }, + { + type: "aaveV2Withdraw", + args: [wstEth, migratedCollateral, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWstEth, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV2Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV2Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toEqual(migratedCollateral); + expect(finalPositionTo.borrowAssets).approximately(migratedBorrow, 2n); + + expect(finalCollateralFrom).toBeGreaterThan( + collateralAmount - migratedCollateral, + ); + expect(finalCollateralFrom).toBeLessThan( + collateralAmount - migratedCollateral + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); + expect(finalDebtFrom).toBeLessThan( + borrowAmount - migratedBorrow + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }); + + testFn("should fully migrate user position", async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: position.borrow, + collateralAmount: position.collateral, + minSharePrice, + }, + true, + ); + + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWstEth, + maxUint256, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + position.collateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + MathLib.wMulUp( + borrowAmount, + MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, + ), + 0n, + minSharePrice, + aaveV2CoreMigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20Transfer", + args: [ + marketTo.loanToken, + generalAdapter1, + maxUint256, + aaveV2CoreMigrationAdapter, + ], + }, + { + type: "morphoRepay", + args: [ + marketTo, + maxUint256, + 0n, + maxUint256, + client.account.address, + [], + ], + }, + { + type: "erc20TransferFrom", + args: [aWstEth, maxUint256, aaveV2CoreMigrationAdapter], + }, + { + type: "aaveV2Withdraw", + args: [wstEth, maxUint256, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [wstEth, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV2Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV2Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toBeGreaterThan(collateralAmount); + expect(finalPositionTo.collateral).toBeLessThanOrEqual( + collateralAmount + 10n ** 12n, + ); // interest accrued (empirical) + expect(finalPositionTo.borrowAssets).toBeGreaterThan(borrowAmount); + expect(finalPositionTo.borrowAssets).toBeLessThanOrEqual( + borrowAmount + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalCollateralFrom).toBe(0n); + expect(finalDebtFrom).toBe(0n); + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }); + + testFn( + "should partially migrate user position without signature", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + const migratedBorrow = borrowAmount / 2n; + const migratedCollateral = collateralAmount / 2n; + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: migratedCollateral, + minSharePrice, + }, + false, + ); + + expect(migrationBundle.requirements.signatures).toHaveLength(0); + expect(migrationBundle.requirements.txs).toHaveLength(2); + expect(migrationBundle.requirements.txs[0]).toEqual({ + type: "morphoSetAuthorization", + args: [generalAdapter1, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [generalAdapter1, true], + }), + }, + }); + expect(migrationBundle.requirements.txs[1]).toEqual({ + type: "erc20Approve", + args: [aWstEth, generalAdapter1, migratedCollateral], + tx: { + to: aWstEth, + data: encodeFunctionData({ + abi: aTokenV2Abi, + functionName: "approve", + args: [generalAdapter1, migratedCollateral], + }), + }, + }); + + expect(migrationBundle.actions).toEqual([ + { + args: [ + marketTo, + migratedCollateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV2CoreMigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [ + aWstEth, + migratedCollateral, + aaveV2CoreMigrationAdapter, + ], + }, + { + type: "aaveV2Withdraw", + args: [wstEth, migratedCollateral, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWstEth, client.account.address, maxUint256], + }, + ]); + + await sendTransaction(client, migrationBundle.requirements.txs[0]!.tx); + await sendTransaction(client, migrationBundle.requirements.txs[1]!.tx); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV2Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV2Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toEqual(migratedCollateral); + expect(finalPositionTo.borrowAssets).approximately(migratedBorrow, 2n); + + expect(finalCollateralFrom).toBeGreaterThan( + collateralAmount - migratedCollateral, + ); + expect(finalCollateralFrom).toBeLessThan( + collateralAmount - migratedCollateral + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); + expect(finalDebtFrom).toBeLessThan( + borrowAmount - migratedBorrow + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }, + ); + + testFn( + "should fully migrate user position without signature", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: position.borrow, + collateralAmount: position.collateral, + minSharePrice, + }, + false, + ); + + expect(migrationBundle.requirements.signatures).toHaveLength(0); + expect(migrationBundle.requirements.txs).toHaveLength(2); + expect(migrationBundle.requirements.txs[0]).toEqual({ + type: "morphoSetAuthorization", + args: [generalAdapter1, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [generalAdapter1, true], + }), + }, + }); + expect(migrationBundle.requirements.txs[1]).toEqual({ + type: "erc20Approve", + args: [aWstEth, generalAdapter1, maxUint256], + tx: { + to: aWstEth, + data: encodeFunctionData({ + abi: aTokenV2Abi, + functionName: "approve", + args: [generalAdapter1, maxUint256], + }), + }, + }); + + expect(migrationBundle.actions).toEqual([ + { + args: [ + marketTo, + position.collateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + MathLib.wMulUp( + borrowAmount, + MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, + ), + 0n, + minSharePrice, + aaveV2CoreMigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20Transfer", + args: [ + marketTo.loanToken, + generalAdapter1, + maxUint256, + aaveV2CoreMigrationAdapter, + ], + }, + { + type: "morphoRepay", + args: [ + marketTo, + maxUint256, + 0n, + maxUint256, + client.account.address, + [], + ], + }, + { + type: "erc20TransferFrom", + args: [aWstEth, maxUint256, aaveV2CoreMigrationAdapter], + }, + { + type: "aaveV2Withdraw", + args: [wstEth, maxUint256, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [wstEth, client.account.address, maxUint256], + }, + ]); + + await sendTransaction(client, migrationBundle.requirements.txs[0]!.tx); + await sendTransaction(client, migrationBundle.requirements.txs[1]!.tx); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV2Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV2Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toBeGreaterThan(collateralAmount); + expect(finalPositionTo.collateral).toBeLessThanOrEqual( + collateralAmount + 10n ** 12n, + ); // interest accrued (empirical) + expect(finalPositionTo.borrowAssets).toBeGreaterThan(borrowAmount); + expect(finalPositionTo.borrowAssets).toBeLessThanOrEqual( + borrowAmount + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalCollateralFrom).toBe(0n); + expect(finalDebtFrom).toBe(0n); + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }, + ); + + testFn( + "should partially migrate user position limited by aave v3 liquidity", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseEther("3"); + + const liquidity = parseEther("4"); + + const migratedBorrow = parseEther("1.5"); + + await writeSupply(client, wstEth, collateralAmount, true); + await writeBorrow(client, wNative, borrowAmount); + await client.deal({ + erc20: wstEth, + account: aWstEth, + amount: liquidity, + }); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + expect(position.maxWithdraw.limiter).toEqual( + SupplyMigrationLimiter.liquidity, + ); + + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: position.maxWithdraw.value, + minSharePrice, + }, + true, + ); + + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWstEth, + liquidity, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + liquidity, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV2CoreMigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [wNative, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [aWstEth, liquidity, aaveV2CoreMigrationAdapter], + }, + { + type: "aaveV2Withdraw", + args: [wstEth, liquidity, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWstEth, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [wNative, wstEth, aWstEth]; + const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV2Abi, + address: aWstEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV2Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toEqual(liquidity); + expect(finalPositionTo.borrowAssets).approximately(migratedBorrow, 2n); + + expect(finalCollateralFrom).toBeGreaterThan( + collateralAmount - liquidity, + ); + expect(finalCollateralFrom).toBeLessThan( + collateralAmount - liquidity + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); + expect(finalDebtFrom).toBeLessThan( + borrowAmount - migratedBorrow + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }, + ); + }); +}); From 2fbbaab28e2f1825efc624e0f8d830b8abc3ed9f Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Tue, 4 Mar 2025 15:03:07 +0100 Subject: [PATCH 10/12] test(migration-sdk-viem): fix chain in tests --- .../migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts | 10 +++++----- .../migration-sdk-viem/test/e2e/aaveV3/supply.test.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts index 14e6da07..cf75d626 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts @@ -3,13 +3,14 @@ import { MigratableProtocol, SupplyMigrationLimiter, fetchMigratablePositions, + migrationAddressesRegistry, } from "../../../src/index.js"; import { ChainId, DEFAULT_SLIPPAGE_TOLERANCE, MathLib, - addresses, + addressesRegistry, } from "@morpho-org/blue-sdk"; import { blueAbi, fetchAccrualPosition } from "@morpho-org/blue-sdk-viem"; @@ -29,7 +30,6 @@ import { aTokenV3Abi, variableDebtTokenV3Abi, } from "../../../src/abis/aaveV3.js"; -import { MIGRATION_ADDRESSES } from "../../../src/config.js"; import { MigratableBorrowPosition_AaveV3 } from "../../../src/positions/borrow/aaveV3.borrow.js"; import { MigratableSupplyPosition_AaveV3 } from "../../../src/positions/supply/aaveV3.supply.js"; import { test } from "../setup.js"; @@ -38,7 +38,7 @@ const TEST_CONFIGS = [ { chainId: ChainId.EthMainnet, aWstEth: "0x0B925eD163218f6662a35e0f0371Ac234f9E9371", - wstEth: addresses[ChainId.EthMainnet].wstEth, + wstEth: addressesRegistry[ChainId.EthMainnet].wstEth, variableDebtToken: "0xeA51d7853EEFb32b6ee06b1C12E6dcCA88Be0fFE", testFn: test[ChainId.EthMainnet] as TestAPI, marketTo: markets[ChainId.EthMainnet].eth_wstEth_2, @@ -62,13 +62,13 @@ describe("Borrow position on AAVE V3", () => { marketTo, variableDebtToken, } of TEST_CONFIGS) { - const { pool } = MIGRATION_ADDRESSES[chainId].aaveV3; + const { pool } = migrationAddressesRegistry[chainId].aaveV3; const { bundler3: { generalAdapter1, aaveV3CoreMigrationAdapter }, wNative, usdc, morpho, - } = addresses[chainId]; + } = addressesRegistry[chainId]; const writeSupply = async ( client: ViemTestContext["client"], diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/supply.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/supply.test.ts index 9f710c92..633f001c 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3/supply.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/supply.test.ts @@ -2,6 +2,7 @@ import { MigratableProtocol, SupplyMigrationLimiter, fetchMigratablePositions, + migrationAddressesRegistry, } from "../../../src/index.js"; import { ChainId, MathLib, addressesRegistry } from "@morpho-org/blue-sdk"; @@ -13,7 +14,6 @@ import { vaults } from "@morpho-org/morpho-test"; import type { ViemTestContext } from "@morpho-org/test/vitest"; import { sendTransaction } from "viem/actions"; import { type TestAPI, describe, expect } from "vitest"; -import { migrationAddressesRegistry } from "../../../src/config.js"; import { MigratableBorrowPosition_AaveV3 } from "../../../src/positions/borrow/aaveV3.borrow.js"; import { MigratableSupplyPosition_AaveV3 } from "../../../src/positions/supply/aaveV3.supply.js"; import { test } from "../setup.js"; From 5648daac44d284c07360dbe924094b23cca2432e Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Tue, 4 Mar 2025 12:11:09 +0100 Subject: [PATCH 11/12] test(migration-sdk-viem): fix aave v2 borrow migration tests --- .../test/e2e/aaveV2/borrow.test.ts | 1645 +++++++++-------- 1 file changed, 836 insertions(+), 809 deletions(-) diff --git a/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts index 44e757b8..b2bbe348 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts @@ -3,17 +3,20 @@ import { MigratableProtocol, SupplyMigrationLimiter, fetchMigratablePositions, + migrationAddressesRegistry, } from "../../../src/index.js"; import { ChainId, DEFAULT_SLIPPAGE_TOLERANCE, + type MarketParams, MathLib, - addresses, + addressesRegistry, } from "@morpho-org/blue-sdk"; import { blueAbi, fetchAccrualPosition } from "@morpho-org/blue-sdk-viem"; import { markets } from "@morpho-org/morpho-test"; +import { testAccount } from "@morpho-org/test"; import type { ViemTestContext } from "@morpho-org/test/vitest"; import { type Address, @@ -29,257 +32,633 @@ import { aTokenV2Abi, variableDebtTokenV2Abi, } from "../../../src/abis/aaveV2.js"; -import { MIGRATION_ADDRESSES } from "../../../src/config.js"; import { MigratableBorrowPosition_AaveV2 } from "../../../src/positions/borrow/aaveV2.borrow.js"; import { MigratableSupplyPosition_AaveV2 } from "../../../src/positions/supply/aaveV2.supply.js"; import { test } from "../setup.js"; +const chainId = ChainId.EthMainnet; +const aWEth: Address = "0x030bA81f1c18d280636F32af80b9AAd02Cf0854e"; +const variableDebtToken: Address = "0x531842cEbbdD378f8ee36D171d6cC9C4fcf475Ec"; +const testFn = test[chainId] as TestAPI; +const marketTo = markets[chainId].usdt_weth_86; + +const { lendingPool } = migrationAddressesRegistry[chainId].aaveV2!; +const { + bundler3: { generalAdapter1, aaveV2MigrationAdapter }, + usdt, + usdc, + morpho, + wNative, +} = addressesRegistry[chainId]; + +const lp = testAccount(1); + +const addBlueLiquidity = async ( + client: ViemTestContext["client"], + market: MarketParams, + amount: bigint, +) => { + await client.deal({ + account: lp, + amount, + erc20: market.loanToken, + }); + await client.approve({ + account: lp, + address: market.loanToken, + args: [morpho, amount], + }); + await client.writeContract({ + account: lp, + abi: blueAbi, + address: morpho, + functionName: "supply", + args: [market, amount, 0n, lp.address, "0x"], + }); +}; + +const writeSupply = async ( + client: ViemTestContext["client"], + market: Address, + amount: bigint, + asCollateral = false, +) => { + await client.deal({ + erc20: market, + amount: amount, + }); + await client.approve({ + address: market, + args: [lendingPool.address, amount], + }); + await client.writeContract({ + ...lendingPool, + functionName: "deposit", + args: [market, amount, client.account.address, 0], + }); + await client.writeContract({ + ...lendingPool, + functionName: "setUserUseReserveAsCollateral", + args: [market, asCollateral], + }); + + await client.mine({ blocks: 500 }); //accrue some interests +}; + +const writeBorrow = async ( + client: ViemTestContext["client"], + market: Address, + amount: bigint, +) => { + await client.writeContract({ + ...lendingPool, + functionName: "borrow", + args: [market, amount, 2n, 0, client.account.address], + }); +}; + describe("Borrow position on AAVE V2", () => { - const chainId = ChainId.EthMainnet; - const aWstEth: Address = "0x0B925eD163218f6662a35e0f0371Ac234f9E9371"; - const variableDebtToken: Address = - "0xeA51d7853EEFb32b6ee06b1C12E6dcCA88Be0fFE"; - const testFn = test[ChainId.EthMainnet] as TestAPI; - const marketTo = markets[ChainId.EthMainnet].eth_wstEth_2; - - const { lendingPool } = MIGRATION_ADDRESSES[chainId].aaveV2!; - const { - bundler3: { generalAdapter1, aaveV2CoreMigrationAdapter }, - wNative, - usdc, - wstEth, - morpho, - } = addresses[chainId]; - - const writeSupply = async ( - client: ViemTestContext["client"], - market: Address, - amount: bigint, - asCollateral = false, - ) => { - await client.deal({ - erc20: market, - amount: amount, - }); - await client.approve({ - address: market, - args: [lendingPool.address, amount], - }); - await client.writeContract({ - ...lendingPool, - functionName: "deposit", - args: [market, amount, client.account.address, 0], - }); - await client.writeContract({ - ...lendingPool, - functionName: "setUserUseReserveAsCollateral", - args: [market, asCollateral], - }); - - await client.mine({ blocks: 500 }); //accrue some interests - }; - - const writeBorrow = async ( - client: ViemTestContext["client"], - market: Address, - amount: bigint, - ) => { - await client.writeContract({ - ...lendingPool, - functionName: "borrow", - args: [market, amount, 2n, 0, client.account.address], - }); - }; - - describe(`on chain ${chainId}`, () => { - testFn( - "should fetch user position", - async ({ client }: ViemTestContext) => { - const collateralAmount = parseEther("10"); - const borrowAmount = parseEther("1"); - - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, wNative, borrowAmount); - - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); + testFn("should fetch user position", async ({ client }: ViemTestContext) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseUnits("1000", 6); - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(1); - - const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; - expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); - - expect(position.protocol).toEqual(MigratableProtocol.aaveV2); - expect(position.user).toEqual(client.account.address); - expect(position.loanToken.address).toEqual(wNative); - expect(position.nonce).toEqual(0n); - expect(position.aToken.address).toEqual(aWstEth); - expect(position.collateral).toBeGreaterThanOrEqual(collateralAmount); //interest accrued - expect(position.borrow).toBeGreaterThanOrEqual(borrowAmount); //interest accrued - expect(position.chainId).toEqual(chainId); - expect(position.collateralToken.address).toEqual(wstEth); - expect(position.loanToken.address).toEqual(wNative); - expect(position.maxRepay.limiter).toEqual( - BorrowMigrationLimiter.position, - ); - expect(position.maxRepay.value).toEqual(position.borrow); - expect(position.maxWithdraw.limiter).toEqual( - SupplyMigrationLimiter.position, - ); - expect(position.maxWithdraw.value).toEqual(position.collateral); - }, + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdt, borrowAmount); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, ); - testFn( - "shouldn't fetch user position if multiple collaterals", - async ({ client }) => { - const collateralAmount1 = parseUnits("1000", 6); - const collateralAmount2 = parseEther("10"); - const borrowAmount = parseEther("1"); + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + + expect(position.protocol).toEqual(MigratableProtocol.aaveV2); + expect(position.user).toEqual(client.account.address); + expect(position.loanToken.address).toEqual(usdt); + expect(position.nonce).toEqual(0n); + expect(position.aToken.address).toEqual(aWEth); + expect(position.collateral).toBeGreaterThanOrEqual(collateralAmount); //interest accrued + expect(position.borrow).toBeGreaterThanOrEqual(borrowAmount); //interest accrued + expect(position.chainId).toEqual(chainId); + expect(position.collateralToken.address).toEqual(wNative); + expect(position.loanToken.address).toEqual(usdt); + expect(position.maxRepay.limiter).toEqual(BorrowMigrationLimiter.position); + expect(position.maxRepay.value).toEqual(position.borrow); + expect(position.maxWithdraw.limiter).toEqual( + SupplyMigrationLimiter.position, + ); + expect(position.maxWithdraw.value).toEqual(position.collateral); + }); - await writeSupply(client, usdc, collateralAmount1, true); - await writeSupply(client, wstEth, collateralAmount2, true); - await writeBorrow(client, wNative, borrowAmount); + testFn( + "shouldn't fetch user position if multiple collaterals", + async ({ client }) => { + const collateralAmount1 = parseUnits("1000", 6); + const collateralAmount2 = parseEther("10"); + const borrowAmount = parseUnits("1000", 6); - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); + await writeSupply(client, usdc, collateralAmount1, true); + await writeSupply(client, wNative, collateralAmount2, true); + await writeBorrow(client, usdt, borrowAmount); - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(0); - }, - ); + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); - testFn( - "should fetch multiple user positions if only one collateral", - async ({ client }) => { - const collateralAmount = parseEther("10"); - const pureSupply = parseUnits("10000", 6); - const borrowAmount = parseEther("1"); + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(0); + }, + ); - await writeSupply(client, wstEth, collateralAmount, true); - await writeSupply(client, usdc, pureSupply, false); - await writeBorrow(client, wNative, borrowAmount); + testFn( + "should fetch multiple user positions if only one collateral", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const pureSupply = parseUnits("10000", 6); + const borrowAmount = parseUnits("1000", 6); - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); + await writeSupply(client, wNative, collateralAmount, true); + await writeSupply(client, usdc, pureSupply, false); + await writeBorrow(client, usdt, borrowAmount); - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(2); - expect(aaveV2Positions[0]).toBeInstanceOf( - MigratableSupplyPosition_AaveV2, - ); - expect(aaveV2Positions[1]).toBeInstanceOf( - MigratableBorrowPosition_AaveV2, - ); + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); - const position = aaveV2Positions[1] as MigratableBorrowPosition_AaveV2; + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(2); + expect(aaveV2Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV2, + ); + expect(aaveV2Positions[1]).toBeInstanceOf( + MigratableBorrowPosition_AaveV2, + ); - expect(position.collateralToken.address).toBe(wstEth); - }, - ); + const position = aaveV2Positions[1] as MigratableBorrowPosition_AaveV2; - testFn( - "shouldn't fetch user position if multiple loans", - async ({ client }) => { - const collateralAmount = parseEther("10"); - const borrowAmount1 = parseUnits("1000", 6); - const borrowAmount2 = parseEther("1"); + expect(position.collateralToken.address).toBe(wNative); + }, + ); - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, usdc, borrowAmount1); - await writeBorrow(client, wNative, borrowAmount2); + testFn( + "shouldn't fetch user position if multiple loans", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount1 = parseUnits("1000", 6); + const borrowAmount2 = parseUnits("1000", 6); - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdc, borrowAmount1); + await writeBorrow(client, usdt, borrowAmount2); - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(0); - }, + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(0); + }, + ); + + testFn( + "shouldn't fetch user collateral positions if no borrow", + async ({ client }) => { + const collateralAmount = parseEther("10"); + + await writeSupply(client, wNative, collateralAmount, true); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + expect(aaveV2Positions[0]).toBeInstanceOf( + MigratableSupplyPosition_AaveV2, + ); + }, + ); + + testFn( + "should fetch user position with limited liquidity", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseUnits("5000", 6); + const liquidity = parseEther("6"); + + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdt, borrowAmount); + await client.deal({ + erc20: wNative, + account: aWEth, + amount: liquidity, + }); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); + + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + + expect(position.maxWithdraw).toEqual({ + limiter: SupplyMigrationLimiter.liquidity, + value: liquidity, + }); + }, + ); + + testFn("should partially migrate user position", async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseUnits("3000", 6); + + const migratedBorrow = borrowAmount / 2n; + const migratedCollateral = collateralAmount / 2n; + + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdt, borrowAmount); + await addBlueLiquidity(client, marketTo, migratedBorrow); + + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, ); - testFn( - "shouldn't fetch user collateral positions if no borrow", - async ({ client }) => { - const collateralAmount = parseEther("10"); + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); - await writeSupply(client, wstEth, collateralAmount, true); + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(1); - expect(aaveV2Positions[0]).toBeInstanceOf( - MigratableSupplyPosition_AaveV2, - ); + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: migratedCollateral, + minSharePrice, }, + true, ); - testFn( - "should fetch user position with limited liquidity", - async ({ client }) => { - const collateralAmount = parseEther("10"); - const borrowAmount = parseEther("5"); - const liquidity = parseEther("6"); - - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, wNative, borrowAmount); - await client.deal({ - erc20: wstEth, - account: aWstEth, - amount: liquidity, - }); - - const allPositions = await fetchMigratablePositions( + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); + aWEth, + migratedCollateral, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + migratedCollateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV2MigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [usdt, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [aWEth, migratedCollateral, aaveV2MigrationAdapter], + }, + { + type: "aaveV2Withdraw", + args: [wNative, migratedCollateral, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWEth, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [usdt, wNative, aWEth]; + const adapters = [generalAdapter1, aaveV2MigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV2Abi, + address: aWEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV2Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toEqual(migratedCollateral); + expect(finalPositionTo.borrowAssets).approximately(migratedBorrow, 2n); + + expect(finalCollateralFrom).toBeGreaterThan( + collateralAmount - migratedCollateral, + ); + expect(finalCollateralFrom).toBeLessThan( + collateralAmount - migratedCollateral + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); + expect(finalDebtFrom).toBeLessThan( + borrowAmount - migratedBorrow + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }); + + testFn("should fully migrate user position", async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseUnits("3000", 6); + + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdt, borrowAmount); + await addBlueLiquidity( + client, + marketTo, + MathLib.wMulUp(borrowAmount, MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE), + ); - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(1); + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); + + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); - const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; - expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); - expect(position.maxWithdraw).toEqual({ - limiter: SupplyMigrationLimiter.liquidity, - value: liquidity, - }); + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); + + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: position.borrow, + collateralAmount: position.collateral, + minSharePrice, }, + true, ); - testFn("should partially migrate user position", async ({ client }) => { + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWEth, + maxUint256, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + position.collateral, + client.account.address, + [ + { + type: "morphoBorrow", + args: [ + marketTo, + MathLib.wMulUp( + position.borrow, + MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, + ), + 0n, + minSharePrice, + aaveV2MigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [usdt, maxUint256, client.account.address, 2n], + }, + { + type: "erc20Transfer", + args: [ + marketTo.loanToken, + generalAdapter1, + maxUint256, + aaveV2MigrationAdapter, + ], + }, + { + type: "morphoRepay", + args: [ + marketTo, + maxUint256, + 0n, + maxUint256, + client.account.address, + [], + ], + }, + { + type: "erc20TransferFrom", + args: [aWEth, maxUint256, aaveV2MigrationAdapter], + }, + { + type: "aaveV2Withdraw", + args: [wNative, maxUint256, generalAdapter1], + }, + ], + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [wNative, client.account.address, maxUint256], + }, + ]); + + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); + + await sendTransaction(client, migrationBundle.tx()); + + const transferredAssets = [usdt, wNative, aWEth]; + const adapters = [generalAdapter1, aaveV2MigrationAdapter]; + + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV2Abi, + address: aWEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV2Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), + ), + ), + ]); + + expect(finalPositionTo.collateral).toBeGreaterThan(collateralAmount); + expect(finalPositionTo.collateral).toBeLessThanOrEqual( + collateralAmount + 10n ** 12n, + ); // interest accrued (empirical) + expect(finalPositionTo.borrowAssets).toBeGreaterThan(borrowAmount); + expect(finalPositionTo.borrowAssets).toBeLessThanOrEqual( + borrowAmount + 10n ** 12n, + ); // interest accrued (empirical) + + expect(finalCollateralFrom).toBe(0n); + expect(finalDebtFrom).toBe(0n); + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, + ); + } + }); + + testFn( + "should partially migrate user position without signature", + async ({ client }) => { const collateralAmount = parseEther("10"); - const borrowAmount = parseEther("3"); + const borrowAmount = parseUnits("3000", 6); const migratedBorrow = borrowAmount / 2n; const migratedCollateral = collateralAmount / 2n; - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, wNative, borrowAmount); + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdt, borrowAmount); + await addBlueLiquidity(client, marketTo, migratedBorrow); const allPositions = await fetchMigratablePositions( client.account.address, @@ -304,35 +683,37 @@ describe("Borrow position on AAVE V2", () => { collateralAmount: migratedCollateral, minSharePrice, }, - true, + false, ); - expect(migrationBundle.requirements.txs).toHaveLength(0); - expect(migrationBundle.requirements.signatures).toHaveLength(2); - expect(migrationBundle.actions).toEqual([ - { - args: [ - { - authorizer: client.account.address, - authorized: generalAdapter1, - isAuthorized: true, - deadline: expect.any(BigInt), - nonce: 0n, - }, - null, - ], - type: "morphoSetAuthorizationWithSig", + expect(migrationBundle.requirements.signatures).toHaveLength(0); + expect(migrationBundle.requirements.txs).toHaveLength(2); + expect(migrationBundle.requirements.txs[0]).toEqual({ + type: "morphoSetAuthorization", + args: [generalAdapter1, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [generalAdapter1, true], + }), }, - { - args: [ - client.account.address, - aWstEth, - migratedCollateral, - expect.any(BigInt), - null, - ], - type: "permit", + }); + expect(migrationBundle.requirements.txs[1]).toEqual({ + type: "erc20Approve", + args: [aWEth, generalAdapter1, migratedCollateral], + tx: { + to: aWEth, + data: encodeFunctionData({ + abi: aTokenV2Abi, + functionName: "approve", + args: [generalAdapter1, migratedCollateral], + }), }, + }); + + expect(migrationBundle.actions).toEqual([ { args: [ marketTo, @@ -346,20 +727,20 @@ describe("Borrow position on AAVE V2", () => { migratedBorrow, 0n, minSharePrice, - aaveV2CoreMigrationAdapter, + aaveV2MigrationAdapter, ], }, { type: "aaveV2Repay", - args: [wNative, maxUint256, client.account.address, 2n], + args: [usdt, maxUint256, client.account.address, 2n], }, { type: "erc20TransferFrom", - args: [aWstEth, migratedCollateral, aaveV2CoreMigrationAdapter], + args: [aWEth, migratedCollateral, aaveV2MigrationAdapter], }, { type: "aaveV2Withdraw", - args: [wstEth, migratedCollateral, generalAdapter1], + args: [wNative, migratedCollateral, generalAdapter1], }, ], ], @@ -367,17 +748,17 @@ describe("Borrow position on AAVE V2", () => { }, { type: "erc20Transfer", - args: [aWstEth, client.account.address, maxUint256], + args: [aWEth, client.account.address, maxUint256], }, ]); - await migrationBundle.requirements.signatures[0]!.sign(client); - await migrationBundle.requirements.signatures[1]!.sign(client); + await sendTransaction(client, migrationBundle.requirements.txs[0]!.tx); + await sendTransaction(client, migrationBundle.requirements.txs[1]!.tx); await sendTransaction(client, migrationBundle.tx()); - const transferredAssets = [wNative, wstEth, aWstEth]; - const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; + const transferredAssets = [usdt, wNative, aWEth]; + const adapters = [generalAdapter1, aaveV2MigrationAdapter]; const [ finalPositionTo, @@ -388,7 +769,7 @@ describe("Borrow position on AAVE V2", () => { fetchAccrualPosition(client.account.address, marketTo.id, client), readContract(client, { abi: aTokenV2Abi, - address: aWstEth, + address: aWEth, functionName: "balanceOf", args: [client.account.address], }), @@ -435,14 +816,22 @@ describe("Borrow position on AAVE V2", () => { `Adapter ${adapter} shouldn't hold ${asset}.`, ); } - }); + }, + ); - testFn("should fully migrate user position", async ({ client }) => { + testFn( + "should fully migrate user position without signature", + async ({ client }) => { const collateralAmount = parseEther("10"); - const borrowAmount = parseEther("3"); + const borrowAmount = parseUnits("3000", 6); - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, wNative, borrowAmount); + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdt, borrowAmount); + await addBlueLiquidity( + client, + marketTo, + MathLib.wMulUp(borrowAmount, MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE), + ); const allPositions = await fetchMigratablePositions( client.account.address, @@ -467,35 +856,37 @@ describe("Borrow position on AAVE V2", () => { collateralAmount: position.collateral, minSharePrice, }, - true, + false, ); - expect(migrationBundle.requirements.txs).toHaveLength(0); - expect(migrationBundle.requirements.signatures).toHaveLength(2); - expect(migrationBundle.actions).toEqual([ - { - args: [ - { - authorizer: client.account.address, - authorized: generalAdapter1, - isAuthorized: true, - deadline: expect.any(BigInt), - nonce: 0n, - }, - null, - ], - type: "morphoSetAuthorizationWithSig", + expect(migrationBundle.requirements.signatures).toHaveLength(0); + expect(migrationBundle.requirements.txs).toHaveLength(2); + expect(migrationBundle.requirements.txs[0]).toEqual({ + type: "morphoSetAuthorization", + args: [generalAdapter1, true], + tx: { + to: morpho, + data: encodeFunctionData({ + abi: blueAbi, + functionName: "setAuthorization", + args: [generalAdapter1, true], + }), }, - { - args: [ - client.account.address, - aWstEth, - maxUint256, - expect.any(BigInt), - null, - ], - type: "permit", + }); + expect(migrationBundle.requirements.txs[1]).toEqual({ + type: "erc20Approve", + args: [aWEth, generalAdapter1, maxUint256], + tx: { + to: aWEth, + data: encodeFunctionData({ + abi: aTokenV2Abi, + functionName: "approve", + args: [generalAdapter1, maxUint256], + }), }, + }); + + expect(migrationBundle.actions).toEqual([ { args: [ marketTo, @@ -507,17 +898,17 @@ describe("Borrow position on AAVE V2", () => { args: [ marketTo, MathLib.wMulUp( - borrowAmount, + position.borrow, MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, ), 0n, minSharePrice, - aaveV2CoreMigrationAdapter, + aaveV2MigrationAdapter, ], }, { type: "aaveV2Repay", - args: [wNative, maxUint256, client.account.address, 2n], + args: [usdt, maxUint256, client.account.address, 2n], }, { type: "erc20Transfer", @@ -525,7 +916,7 @@ describe("Borrow position on AAVE V2", () => { marketTo.loanToken, generalAdapter1, maxUint256, - aaveV2CoreMigrationAdapter, + aaveV2MigrationAdapter, ], }, { @@ -541,11 +932,11 @@ describe("Borrow position on AAVE V2", () => { }, { type: "erc20TransferFrom", - args: [aWstEth, maxUint256, aaveV2CoreMigrationAdapter], + args: [aWEth, maxUint256, aaveV2MigrationAdapter], }, { type: "aaveV2Withdraw", - args: [wstEth, maxUint256, generalAdapter1], + args: [wNative, maxUint256, generalAdapter1], }, ], ], @@ -553,17 +944,17 @@ describe("Borrow position on AAVE V2", () => { }, { type: "erc20Transfer", - args: [wstEth, client.account.address, maxUint256], + args: [wNative, client.account.address, maxUint256], }, ]); - await migrationBundle.requirements.signatures[0]!.sign(client); - await migrationBundle.requirements.signatures[1]!.sign(client); + await sendTransaction(client, migrationBundle.requirements.txs[0]!.tx); + await sendTransaction(client, migrationBundle.requirements.txs[1]!.tx); await sendTransaction(client, migrationBundle.tx()); - const transferredAssets = [wNative, wstEth, aWstEth]; - const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; + const transferredAssets = [usdt, wNative, aWEth]; + const adapters = [generalAdapter1, aaveV2MigrationAdapter]; const [ finalPositionTo, @@ -574,7 +965,7 @@ describe("Borrow position on AAVE V2", () => { fetchAccrualPosition(client.account.address, marketTo.id, client), readContract(client, { abi: aTokenV2Abi, - address: aWstEth, + address: aWEth, functionName: "balanceOf", args: [client.account.address], }), @@ -618,547 +1009,183 @@ describe("Borrow position on AAVE V2", () => { `Adapter ${adapter} shouldn't hold ${asset}.`, ); } - }); + }, + ); - testFn( - "should partially migrate user position without signature", - async ({ client }) => { - const collateralAmount = parseEther("10"); - const borrowAmount = parseEther("3"); - - const migratedBorrow = borrowAmount / 2n; - const migratedCollateral = collateralAmount / 2n; - - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, wNative, borrowAmount); - - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); - - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(1); - - const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; - expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); - - // initial share price is 10^-6 because of virtual shares - const minSharePrice = parseUnits("1", 21); - - const migrationBundle = position.getMigrationTx( - { - marketTo, - borrowAmount: migratedBorrow, - collateralAmount: migratedCollateral, - minSharePrice, - }, - false, - ); - - expect(migrationBundle.requirements.signatures).toHaveLength(0); - expect(migrationBundle.requirements.txs).toHaveLength(2); - expect(migrationBundle.requirements.txs[0]).toEqual({ - type: "morphoSetAuthorization", - args: [generalAdapter1, true], - tx: { - to: morpho, - data: encodeFunctionData({ - abi: blueAbi, - functionName: "setAuthorization", - args: [generalAdapter1, true], - }), - }, - }); - expect(migrationBundle.requirements.txs[1]).toEqual({ - type: "erc20Approve", - args: [aWstEth, generalAdapter1, migratedCollateral], - tx: { - to: aWstEth, - data: encodeFunctionData({ - abi: aTokenV2Abi, - functionName: "approve", - args: [generalAdapter1, migratedCollateral], - }), - }, - }); - - expect(migrationBundle.actions).toEqual([ - { - args: [ - marketTo, - migratedCollateral, - client.account.address, - [ - { - type: "morphoBorrow", - args: [ - marketTo, - migratedBorrow, - 0n, - minSharePrice, - aaveV2CoreMigrationAdapter, - ], - }, - { - type: "aaveV2Repay", - args: [wNative, maxUint256, client.account.address, 2n], - }, - { - type: "erc20TransferFrom", - args: [ - aWstEth, - migratedCollateral, - aaveV2CoreMigrationAdapter, - ], - }, - { - type: "aaveV2Withdraw", - args: [wstEth, migratedCollateral, generalAdapter1], - }, - ], - ], - type: "morphoSupplyCollateral", - }, - { - type: "erc20Transfer", - args: [aWstEth, client.account.address, maxUint256], - }, - ]); - - await sendTransaction(client, migrationBundle.requirements.txs[0]!.tx); - await sendTransaction(client, migrationBundle.requirements.txs[1]!.tx); - - await sendTransaction(client, migrationBundle.tx()); - - const transferredAssets = [wNative, wstEth, aWstEth]; - const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; - - const [ - finalPositionTo, - finalCollateralFrom, - finalDebtFrom, - adaptersBalances, - ] = await Promise.all([ - fetchAccrualPosition(client.account.address, marketTo.id, client), - readContract(client, { - abi: aTokenV2Abi, - address: aWstEth, - functionName: "balanceOf", - args: [client.account.address], - }), - readContract(client, { - abi: variableDebtTokenV2Abi, - address: variableDebtToken, - functionName: "balanceOf", - args: [client.account.address], - }), - Promise.all( - transferredAssets.flatMap((asset) => - adapters.map(async (adapter) => ({ - balance: await readContract(client, { - abi: erc20Abi, - address: asset, - functionName: "balanceOf", - args: [adapter], - }), - asset, - adapter, - })), - ), - ), - ]); - - expect(finalPositionTo.collateral).toEqual(migratedCollateral); - expect(finalPositionTo.borrowAssets).approximately(migratedBorrow, 2n); - - expect(finalCollateralFrom).toBeGreaterThan( - collateralAmount - migratedCollateral, - ); - expect(finalCollateralFrom).toBeLessThan( - collateralAmount - migratedCollateral + 10n ** 12n, - ); // interest accrued (empirical) - - expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); - expect(finalDebtFrom).toBeLessThan( - borrowAmount - migratedBorrow + 10n ** 12n, - ); // interest accrued (empirical) - - for (const { balance, asset, adapter } of adaptersBalances) { - expect(balance).to.equal( - 0n, - `Adapter ${adapter} shouldn't hold ${asset}.`, - ); - } - }, - ); - - testFn( - "should fully migrate user position without signature", - async ({ client }) => { - const collateralAmount = parseEther("10"); - const borrowAmount = parseEther("3"); - - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, wNative, borrowAmount); - - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); - - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(1); - - const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; - expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); - - // initial share price is 10^-6 because of virtual shares - const minSharePrice = parseUnits("1", 21); - - const migrationBundle = position.getMigrationTx( - { - marketTo, - borrowAmount: position.borrow, - collateralAmount: position.collateral, - minSharePrice, - }, - false, - ); - - expect(migrationBundle.requirements.signatures).toHaveLength(0); - expect(migrationBundle.requirements.txs).toHaveLength(2); - expect(migrationBundle.requirements.txs[0]).toEqual({ - type: "morphoSetAuthorization", - args: [generalAdapter1, true], - tx: { - to: morpho, - data: encodeFunctionData({ - abi: blueAbi, - functionName: "setAuthorization", - args: [generalAdapter1, true], - }), - }, - }); - expect(migrationBundle.requirements.txs[1]).toEqual({ - type: "erc20Approve", - args: [aWstEth, generalAdapter1, maxUint256], - tx: { - to: aWstEth, - data: encodeFunctionData({ - abi: aTokenV2Abi, - functionName: "approve", - args: [generalAdapter1, maxUint256], - }), - }, - }); - - expect(migrationBundle.actions).toEqual([ - { - args: [ - marketTo, - position.collateral, - client.account.address, - [ - { - type: "morphoBorrow", - args: [ - marketTo, - MathLib.wMulUp( - borrowAmount, - MathLib.WAD + DEFAULT_SLIPPAGE_TOLERANCE, - ), - 0n, - minSharePrice, - aaveV2CoreMigrationAdapter, - ], - }, - { - type: "aaveV2Repay", - args: [wNative, maxUint256, client.account.address, 2n], - }, - { - type: "erc20Transfer", - args: [ - marketTo.loanToken, - generalAdapter1, - maxUint256, - aaveV2CoreMigrationAdapter, - ], - }, - { - type: "morphoRepay", - args: [ - marketTo, - maxUint256, - 0n, - maxUint256, - client.account.address, - [], - ], - }, - { - type: "erc20TransferFrom", - args: [aWstEth, maxUint256, aaveV2CoreMigrationAdapter], - }, - { - type: "aaveV2Withdraw", - args: [wstEth, maxUint256, generalAdapter1], - }, - ], - ], - type: "morphoSupplyCollateral", - }, - { - type: "erc20Transfer", - args: [wstEth, client.account.address, maxUint256], - }, - ]); - - await sendTransaction(client, migrationBundle.requirements.txs[0]!.tx); - await sendTransaction(client, migrationBundle.requirements.txs[1]!.tx); - - await sendTransaction(client, migrationBundle.tx()); - - const transferredAssets = [wNative, wstEth, aWstEth]; - const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; - - const [ - finalPositionTo, - finalCollateralFrom, - finalDebtFrom, - adaptersBalances, - ] = await Promise.all([ - fetchAccrualPosition(client.account.address, marketTo.id, client), - readContract(client, { - abi: aTokenV2Abi, - address: aWstEth, - functionName: "balanceOf", - args: [client.account.address], - }), - readContract(client, { - abi: variableDebtTokenV2Abi, - address: variableDebtToken, - functionName: "balanceOf", - args: [client.account.address], - }), - Promise.all( - transferredAssets.flatMap((asset) => - adapters.map(async (adapter) => ({ - balance: await readContract(client, { - abi: erc20Abi, - address: asset, - functionName: "balanceOf", - args: [adapter], - }), - asset, - adapter, - })), - ), - ), - ]); - - expect(finalPositionTo.collateral).toBeGreaterThan(collateralAmount); - expect(finalPositionTo.collateral).toBeLessThanOrEqual( - collateralAmount + 10n ** 12n, - ); // interest accrued (empirical) - expect(finalPositionTo.borrowAssets).toBeGreaterThan(borrowAmount); - expect(finalPositionTo.borrowAssets).toBeLessThanOrEqual( - borrowAmount + 10n ** 12n, - ); // interest accrued (empirical) - - expect(finalCollateralFrom).toBe(0n); - expect(finalDebtFrom).toBe(0n); - - for (const { balance, asset, adapter } of adaptersBalances) { - expect(balance).to.equal( - 0n, - `Adapter ${adapter} shouldn't hold ${asset}.`, - ); - } - }, - ); + testFn( + "should partially migrate user position limited by aave v3 liquidity", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseUnits("3000", 6); - testFn( - "should partially migrate user position limited by aave v3 liquidity", - async ({ client }) => { - const collateralAmount = parseEther("10"); - const borrowAmount = parseEther("3"); + const liquidity = parseEther("4"); - const liquidity = parseEther("4"); + const migratedBorrow = parseUnits("1500", 6); - const migratedBorrow = parseEther("1.5"); + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdt, borrowAmount); + await client.deal({ + erc20: wNative, + account: aWEth, + amount: liquidity, + }); + await addBlueLiquidity(client, marketTo, migratedBorrow); - await writeSupply(client, wstEth, collateralAmount, true); - await writeBorrow(client, wNative, borrowAmount); - await client.deal({ - erc20: wstEth, - account: aWstEth, - amount: liquidity, - }); + const allPositions = await fetchMigratablePositions( + client.account.address, + client, + { protocols: [MigratableProtocol.aaveV2] }, + ); - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); + const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; + expect(aaveV2Positions).toBeDefined(); + expect(aaveV2Positions).toHaveLength(1); - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(1); + const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; + expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); + expect(position.maxWithdraw.limiter).toEqual( + SupplyMigrationLimiter.liquidity, + ); - const position = aaveV2Positions[0]! as MigratableBorrowPosition_AaveV2; - expect(position).toBeInstanceOf(MigratableBorrowPosition_AaveV2); - expect(position.maxWithdraw.limiter).toEqual( - SupplyMigrationLimiter.liquidity, - ); + // initial share price is 10^-6 because of virtual shares + const minSharePrice = parseUnits("1", 21); - // initial share price is 10^-6 because of virtual shares - const minSharePrice = parseUnits("1", 21); + const migrationBundle = position.getMigrationTx( + { + marketTo, + borrowAmount: migratedBorrow, + collateralAmount: position.maxWithdraw.value, + minSharePrice, + }, + true, + ); - const migrationBundle = position.getMigrationTx( - { + expect(migrationBundle.requirements.txs).toHaveLength(0); + expect(migrationBundle.requirements.signatures).toHaveLength(2); + expect(migrationBundle.actions).toEqual([ + { + args: [ + { + authorizer: client.account.address, + authorized: generalAdapter1, + isAuthorized: true, + deadline: expect.any(BigInt), + nonce: 0n, + }, + null, + ], + type: "morphoSetAuthorizationWithSig", + }, + { + args: [ + client.account.address, + aWEth, + liquidity, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ marketTo, - borrowAmount: migratedBorrow, - collateralAmount: position.maxWithdraw.value, - minSharePrice, - }, - true, - ); - - expect(migrationBundle.requirements.txs).toHaveLength(0); - expect(migrationBundle.requirements.signatures).toHaveLength(2); - expect(migrationBundle.actions).toEqual([ - { - args: [ + liquidity, + client.account.address, + [ { - authorizer: client.account.address, - authorized: generalAdapter1, - isAuthorized: true, - deadline: expect.any(BigInt), - nonce: 0n, + type: "morphoBorrow", + args: [ + marketTo, + migratedBorrow, + 0n, + minSharePrice, + aaveV2MigrationAdapter, + ], + }, + { + type: "aaveV2Repay", + args: [usdt, maxUint256, client.account.address, 2n], + }, + { + type: "erc20TransferFrom", + args: [aWEth, liquidity, aaveV2MigrationAdapter], + }, + { + type: "aaveV2Withdraw", + args: [wNative, liquidity, generalAdapter1], }, - null, - ], - type: "morphoSetAuthorizationWithSig", - }, - { - args: [ - client.account.address, - aWstEth, - liquidity, - expect.any(BigInt), - null, - ], - type: "permit", - }, - { - args: [ - marketTo, - liquidity, - client.account.address, - [ - { - type: "morphoBorrow", - args: [ - marketTo, - migratedBorrow, - 0n, - minSharePrice, - aaveV2CoreMigrationAdapter, - ], - }, - { - type: "aaveV2Repay", - args: [wNative, maxUint256, client.account.address, 2n], - }, - { - type: "erc20TransferFrom", - args: [aWstEth, liquidity, aaveV2CoreMigrationAdapter], - }, - { - type: "aaveV2Withdraw", - args: [wstEth, liquidity, generalAdapter1], - }, - ], ], - type: "morphoSupplyCollateral", - }, - { - type: "erc20Transfer", - args: [aWstEth, client.account.address, maxUint256], - }, - ]); + ], + type: "morphoSupplyCollateral", + }, + { + type: "erc20Transfer", + args: [aWEth, client.account.address, maxUint256], + }, + ]); - await migrationBundle.requirements.signatures[0]!.sign(client); - await migrationBundle.requirements.signatures[1]!.sign(client); + await migrationBundle.requirements.signatures[0]!.sign(client); + await migrationBundle.requirements.signatures[1]!.sign(client); - await sendTransaction(client, migrationBundle.tx()); + await sendTransaction(client, migrationBundle.tx()); - const transferredAssets = [wNative, wstEth, aWstEth]; - const adapters = [generalAdapter1, aaveV2CoreMigrationAdapter]; + const transferredAssets = [usdt, wNative, aWEth]; + const adapters = [generalAdapter1, aaveV2MigrationAdapter]; - const [ - finalPositionTo, - finalCollateralFrom, - finalDebtFrom, - adaptersBalances, - ] = await Promise.all([ - fetchAccrualPosition(client.account.address, marketTo.id, client), - readContract(client, { - abi: aTokenV2Abi, - address: aWstEth, - functionName: "balanceOf", - args: [client.account.address], - }), - readContract(client, { - abi: variableDebtTokenV2Abi, - address: variableDebtToken, - functionName: "balanceOf", - args: [client.account.address], - }), - Promise.all( - transferredAssets.flatMap((asset) => - adapters.map(async (adapter) => ({ - balance: await readContract(client, { - abi: erc20Abi, - address: asset, - functionName: "balanceOf", - args: [adapter], - }), - asset, - adapter, - })), - ), + const [ + finalPositionTo, + finalCollateralFrom, + finalDebtFrom, + adaptersBalances, + ] = await Promise.all([ + fetchAccrualPosition(client.account.address, marketTo.id, client), + readContract(client, { + abi: aTokenV2Abi, + address: aWEth, + functionName: "balanceOf", + args: [client.account.address], + }), + readContract(client, { + abi: variableDebtTokenV2Abi, + address: variableDebtToken, + functionName: "balanceOf", + args: [client.account.address], + }), + Promise.all( + transferredAssets.flatMap((asset) => + adapters.map(async (adapter) => ({ + balance: await readContract(client, { + abi: erc20Abi, + address: asset, + functionName: "balanceOf", + args: [adapter], + }), + asset, + adapter, + })), ), - ]); + ), + ]); - expect(finalPositionTo.collateral).toEqual(liquidity); - expect(finalPositionTo.borrowAssets).approximately(migratedBorrow, 2n); + expect(finalPositionTo.collateral).toEqual(liquidity); + expect(finalPositionTo.borrowAssets).approximately(migratedBorrow, 2n); + + expect(finalCollateralFrom).toBeGreaterThan(collateralAmount - liquidity); + expect(finalCollateralFrom).toBeLessThan( + collateralAmount - liquidity + 10n ** 12n, + ); // interest accrued (empirical) - expect(finalCollateralFrom).toBeGreaterThan( - collateralAmount - liquidity, + expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); + expect(finalDebtFrom).toBeLessThan( + borrowAmount - migratedBorrow + 10n ** 12n, + ); // interest accrued (empirical) + + for (const { balance, asset, adapter } of adaptersBalances) { + expect(balance).to.equal( + 0n, + `Adapter ${adapter} shouldn't hold ${asset}.`, ); - expect(finalCollateralFrom).toBeLessThan( - collateralAmount - liquidity + 10n ** 12n, - ); // interest accrued (empirical) - - expect(finalDebtFrom).toBeGreaterThan(borrowAmount - migratedBorrow); - expect(finalDebtFrom).toBeLessThan( - borrowAmount - migratedBorrow + 10n ** 12n, - ); // interest accrued (empirical) - - for (const { balance, asset, adapter } of adaptersBalances) { - expect(balance).to.equal( - 0n, - `Adapter ${adapter} shouldn't hold ${asset}.`, - ); - } - }, - ); - }); + } + }, + ); }); From 6b40c156fbb03db13db594083c27577816a3912a Mon Sep 17 00:00:00 2001 From: Oumar Fall Date: Tue, 4 Mar 2025 15:20:11 +0100 Subject: [PATCH 12/12] test(migration-sdk-viem): clean tests --- .../test/e2e/aaveV2/borrow.test.ts | 22 --------------- .../test/e2e/aaveV2/supply.test.ts | 22 ++++++++++----- .../test/e2e/aaveV3/borrow.test.ts | 27 ++----------------- .../test/e2e/aaveV3Optimizer/supply.test.ts | 16 ++++++++--- .../test/e2e/compoundV2/supply.test.ts | 5 ++-- .../test/e2e/compoundV3/supply.test.ts | 18 ++++++++++--- 6 files changed, 46 insertions(+), 64 deletions(-) diff --git a/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts index b2bbe348..bb02a9a5 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts @@ -235,28 +235,6 @@ describe("Borrow position on AAVE V2", () => { }, ); - testFn( - "shouldn't fetch user collateral positions if no borrow", - async ({ client }) => { - const collateralAmount = parseEther("10"); - - await writeSupply(client, wNative, collateralAmount, true); - - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV2] }, - ); - - const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; - expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(1); - expect(aaveV2Positions[0]).toBeInstanceOf( - MigratableSupplyPosition_AaveV2, - ); - }, - ); - testFn( "should fetch user position with limited liquidity", async ({ client }) => { diff --git a/packages/migration-sdk-viem/test/e2e/aaveV2/supply.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV2/supply.test.ts index a5037046..7c080106 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV2/supply.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV2/supply.test.ts @@ -13,6 +13,7 @@ import type { AnvilTestClient } from "@morpho-org/test"; import { sendTransaction } from "viem/actions"; import { describe, expect } from "vitest"; import { migrationAddressesRegistry } from "../../../src/config.js"; +import { MigratableBorrowPosition_AaveV2 } from "../../../src/positions/borrow/aaveV2.borrow.js"; import { test } from "../setup.js"; const aWeth = "0x030bA81f1c18d280636F32af80b9AAd02Cf0854e"; @@ -141,7 +142,10 @@ describe("Supply position on AAVE V2", () => { const aaveV2Positions = allPositions[MigratableProtocol.aaveV2]!; expect(aaveV2Positions).toBeDefined(); - expect(aaveV2Positions).toHaveLength(0); + expect(aaveV2Positions).toHaveLength(1); + expect(aaveV2Positions[0]).toBeInstanceOf( + MigratableBorrowPosition_AaveV2, + ); }, ); @@ -193,7 +197,9 @@ describe("Supply position on AAVE V2", () => { expect(aaveV2Positions).toBeDefined(); expect(aaveV2Positions).toHaveLength(1); - const migrationBundle = aaveV2Positions[0]!.getMigrationTx( + const position = aaveV2Positions[0]! as MigratableSupplyPosition_AaveV2; + + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: migratedAmount, @@ -275,9 +281,9 @@ describe("Supply position on AAVE V2", () => { expect(aaveV2Positions).toBeDefined(); expect(aaveV2Positions).toHaveLength(1); - const position = aaveV2Positions[0]!; + const position = aaveV2Positions[0]! as MigratableSupplyPosition_AaveV2; - const migrationBundle = aaveV2Positions[0]!.getMigrationTx( + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: position.supply, @@ -359,7 +365,9 @@ describe("Supply position on AAVE V2", () => { expect(aaveV2Positions).toBeDefined(); expect(aaveV2Positions).toHaveLength(1); - const migrationBundle = aaveV2Positions[0]!.getMigrationTx( + const position = aaveV2Positions[0]! as MigratableSupplyPosition_AaveV2; + + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: migratedAmount, @@ -431,9 +439,9 @@ describe("Supply position on AAVE V2", () => { expect(aaveV2Positions).toBeDefined(); expect(aaveV2Positions).toHaveLength(1); - const position = aaveV2Positions[0]!; + const position = aaveV2Positions[0]! as MigratableSupplyPosition_AaveV2; - const migrationBundle = aaveV2Positions[0]!.getMigrationTx( + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: position.supply, diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts index cf75d626..00902e11 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts @@ -38,7 +38,6 @@ const TEST_CONFIGS = [ { chainId: ChainId.EthMainnet, aWstEth: "0x0B925eD163218f6662a35e0f0371Ac234f9E9371", - wstEth: addressesRegistry[ChainId.EthMainnet].wstEth, variableDebtToken: "0xeA51d7853EEFb32b6ee06b1C12E6dcCA88Be0fFE", testFn: test[ChainId.EthMainnet] as TestAPI, marketTo: markets[ChainId.EthMainnet].eth_wstEth_2, @@ -46,7 +45,6 @@ const TEST_CONFIGS = [ { chainId: ChainId.BaseMainnet, aWstEth: "0x99CBC45ea5bb7eF3a5BC08FB1B7E56bB2442Ef0D", - wstEth: "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", variableDebtToken: "0x24e6e0795b3c7c71D965fCc4f371803d1c1DcA1E", testFn: test[ChainId.BaseMainnet] as TestAPI, marketTo: markets[ChainId.BaseMainnet].eth_wstEth, @@ -57,11 +55,12 @@ describe("Borrow position on AAVE V3", () => { for (const { chainId, aWstEth, - wstEth, testFn, marketTo, variableDebtToken, } of TEST_CONFIGS) { + const wstEth = marketTo.collateralToken; + const { pool } = migrationAddressesRegistry[chainId].aaveV3; const { bundler3: { generalAdapter1, aaveV3CoreMigrationAdapter }, @@ -235,28 +234,6 @@ describe("Borrow position on AAVE V3", () => { }, ); - testFn( - "shouldn't fetch user collateral positions if no borrow", - async ({ client }) => { - const collateralAmount = parseEther("10"); - - await writeSupply(client, wstEth, collateralAmount, true); - - const allPositions = await fetchMigratablePositions( - client.account.address, - client, - { protocols: [MigratableProtocol.aaveV3] }, - ); - - const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; - expect(aaveV3Positions).toBeDefined(); - expect(aaveV3Positions).toHaveLength(1); - expect(aaveV3Positions[0]).toBeInstanceOf( - MigratableSupplyPosition_AaveV3, - ); - }, - ); - testFn( "should fetch user position with limited liquidity", async ({ client }) => { diff --git a/packages/migration-sdk-viem/test/e2e/aaveV3Optimizer/supply.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV3Optimizer/supply.test.ts index 782a6db0..693f66ba 100644 --- a/packages/migration-sdk-viem/test/e2e/aaveV3Optimizer/supply.test.ts +++ b/packages/migration-sdk-viem/test/e2e/aaveV3Optimizer/supply.test.ts @@ -218,7 +218,10 @@ describe("Supply position on Morpho AAVE V3", () => { expect(aaveV3Positions).toBeDefined(); expect(aaveV3Positions).toHaveLength(1); - const migrationBundle = aaveV3Positions[0]!.getMigrationTx( + const position = + aaveV3Positions[0]! as MigratableSupplyPosition_AaveV3Optimizer; + + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: migratedAmount, @@ -310,7 +313,8 @@ describe("Supply position on Morpho AAVE V3", () => { expect(aaveV3Positions).toBeDefined(); expect(aaveV3Positions).toHaveLength(1); - const position = aaveV3Positions[0]!; + const position = + aaveV3Positions[0]! as MigratableSupplyPosition_AaveV3Optimizer; const migrationBundle = position.getMigrationTx( { vault: mmWeth, @@ -404,7 +408,10 @@ describe("Supply position on Morpho AAVE V3", () => { expect(aaveV3Positions).toBeDefined(); expect(aaveV3Positions).toHaveLength(1); - const migrationBundle = aaveV3Positions[0]!.getMigrationTx( + const position = + aaveV3Positions[0]! as MigratableSupplyPosition_AaveV3Optimizer; + + const migrationBundle = position.getMigrationTx( { vault: mmWeth, amount: migratedAmount, @@ -482,7 +489,8 @@ describe("Supply position on Morpho AAVE V3", () => { expect(aaveV3Positions).toBeDefined(); expect(aaveV3Positions).toHaveLength(1); - const position = aaveV3Positions[0]!; + const position = + aaveV3Positions[0]! as MigratableSupplyPosition_AaveV3Optimizer; const migrationBundle = position.getMigrationTx( { diff --git a/packages/migration-sdk-viem/test/e2e/compoundV2/supply.test.ts b/packages/migration-sdk-viem/test/e2e/compoundV2/supply.test.ts index 0e27a816..488a94ae 100644 --- a/packages/migration-sdk-viem/test/e2e/compoundV2/supply.test.ts +++ b/packages/migration-sdk-viem/test/e2e/compoundV2/supply.test.ts @@ -385,7 +385,9 @@ describe("Supply position on COMPOUND V2", () => { expect(compoundV2Positions).toBeDefined(); expect(compoundV2Positions).toHaveLength(1); - const position = compoundV2Positions[0]!; + const position = + compoundV2Positions[0]! as MigratableSupplyPosition_CompoundV2; + const migrationBundle = position.getMigrationTx( { vault, @@ -393,7 +395,6 @@ describe("Supply position on COMPOUND V2", () => { maxSharePrice: 2n * MathLib.RAY, }, chainId, - false, ); expect(migrationBundle.requirements.txs).toHaveLength(1); diff --git a/packages/migration-sdk-viem/test/e2e/compoundV3/supply.test.ts b/packages/migration-sdk-viem/test/e2e/compoundV3/supply.test.ts index 9a39b30a..17d2cf10 100644 --- a/packages/migration-sdk-viem/test/e2e/compoundV3/supply.test.ts +++ b/packages/migration-sdk-viem/test/e2e/compoundV3/supply.test.ts @@ -178,7 +178,10 @@ describe("Supply position on COMPOUND V3", () => { expect(compoundV3Positions).toBeDefined(); expect(compoundV3Positions).toHaveLength(1); - const migrationBundle = compoundV3Positions[0]!.getMigrationTx( + const position = + compoundV3Positions[0]! as MigratableSupplyPosition_CompoundV3; + + const migrationBundle = position.getMigrationTx( { vault, amount: migratedAmount, @@ -262,7 +265,9 @@ describe("Supply position on COMPOUND V3", () => { expect(compoundV3Positions).toBeDefined(); expect(compoundV3Positions).toHaveLength(1); - const position = compoundV3Positions[0]!; + const position = + compoundV3Positions[0]! as MigratableSupplyPosition_CompoundV3; + const migrationBundle = position.getMigrationTx( { vault, @@ -347,7 +352,10 @@ describe("Supply position on COMPOUND V3", () => { expect(compoundV3Positions).toBeDefined(); expect(compoundV3Positions).toHaveLength(1); - const migrationBundle = compoundV3Positions[0]!.getMigrationTx( + const position = + compoundV3Positions[0]! as MigratableSupplyPosition_CompoundV3; + + const migrationBundle = position.getMigrationTx( { vault, amount: migratedAmount, @@ -425,7 +433,9 @@ describe("Supply position on COMPOUND V3", () => { expect(compoundV3Positions).toBeDefined(); expect(compoundV3Positions).toHaveLength(1); - const position = compoundV3Positions[0]!; + const position = + compoundV3Positions[0]! as MigratableSupplyPosition_CompoundV3; + const migrationBundle = position.getMigrationTx( { vault,