diff --git a/packages/bundler-sdk-viem/src/BundlerAction.ts b/packages/bundler-sdk-viem/src/BundlerAction.ts index e7e0b960..ae8badd1 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 04154d9c..85dbea41 100644 --- a/packages/bundler-sdk-viem/src/types/actions.ts +++ b/packages/bundler-sdk-viem/src/types/actions.ts @@ -38,7 +38,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 */ 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/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..32746456 100644 --- a/packages/migration-sdk-viem/src/config.ts +++ b/packages/migration-sdk-viem/src/config.ts @@ -2,10 +2,12 @@ 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"; import { + addressesProviderAbi as addressesProviderAbi_v3, poolAbi, protocolDataProviderAbi as protocolDataProviderAbi_v3, } from "./abis/aaveV3.js"; @@ -44,10 +46,12 @@ export interface ProtocolMigrationContracts { [MigratableProtocol.aaveV2]: { protocolDataProvider: Contract; lendingPool: Contract; + addressesProvider: Contract; } | null; [MigratableProtocol.aaveV3]: { pool: Contract; protocolDataProvider: Contract; + addressesProvider: Contract; } | null; [MigratableProtocol.compoundV3]: Record< string, @@ -81,6 +85,10 @@ export const migrationAddressesRegistry = { address: "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9", abi: lendingPoolAbi, }, + addressesProvider: { + address: "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5", + abi: addressesProviderAbi_v2, + }, }, [MigratableProtocol.aaveV3]: { pool: { @@ -91,6 +99,10 @@ export const migrationAddressesRegistry = { address: "0x7B4EB56E7CD4b454BA8ff71E4518426369a138a3", abi: protocolDataProviderAbi_v3, }, + addressesProvider: { + address: "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e", + abi: addressesProviderAbi_v3, + }, }, [MigratableProtocol.compoundV3]: { usdc: { @@ -127,6 +139,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/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/fetchers/aaveV3/aaveV3.fetchers.ts b/packages/migration-sdk-viem/src/fetchers/aaveV3/aaveV3.fetchers.ts index 0f6d79b2..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,17 +1,30 @@ -import { type Address, MathLib, Token } 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 } 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 { 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( @@ -22,35 +35,47 @@ 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]; 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 +103,309 @@ 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, 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, - 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, + morphoNonce, + isBundlerManaging, + }, + }; + }), + ) + ).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, + morphoNonce: loanData.morphoNonce, + isBundlerManaging: loanData.isBundlerManaging, + }), + ); + } - 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 new file mode 100644 index 00000000..a7b202ed --- /dev/null +++ b/packages/migration-sdk-viem/src/positions/borrow/MigratableBorrowPosition.ts @@ -0,0 +1,130 @@ +import type { + Address, + ChainId, + MarketParams, + Token, +} from "@morpho-org/blue-sdk"; + +import type { MigrationBundle } from "../../types/actions.js"; +import type { + BorrowMigrationLimiter, + MigratableProtocol, + SupplyMigrationLimiter, +} 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 market to migrate to. */ + marketTo: MarketParams; + /** Slippage tolerance for the current position (optional). */ + slippageFrom?: bigint; + /** The maximum amount of borrow shares mint (protects the sender from unexpected slippage). */ + minSharePrice: 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 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. */ + 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. */ + maxWithdraw: { value: bigint; limiter: SupplyMigrationLimiter }; + /** The maximum borrow migration limit and its corresponding limiter. */ + 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; +} + +/** + * 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 chainId; + public readonly collateralToken; + public readonly collateral; + public readonly collateralApy; + public readonly maxRepay; + public readonly maxWithdraw; + public readonly lltv; + public readonly isBundlerManaging; + public readonly morphoNonce; + + /** + * 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.maxWithdraw = config.maxWithdraw; + this.chainId = config.chainId; + this.collateralToken = config.collateralToken; + this.collateral = config.collateral; + 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 }): + | bigint + | null; + + /** + * 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, + supportsSignature: boolean, + ): MigrationBundle; +} 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/src/positions/borrow/aaveV3.borrow.ts b/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts new file mode 100644 index 00000000..3fe3f75c --- /dev/null +++ b/packages/migration-sdk-viem/src/positions/borrow/aaveV3.borrow.ts @@ -0,0 +1,341 @@ +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 { 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, +} 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( + { + 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 (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( + { + 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/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/actions.ts b/packages/migration-sdk-viem/src/types/actions.ts index 9494070c..0acea47e 100644 --- a/packages/migration-sdk-viem/src/types/actions.ts +++ b/packages/migration-sdk-viem/src/types/actions.ts @@ -10,6 +10,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/src/types/positions.ts b/packages/migration-sdk-viem/src/types/positions.ts index c087f328..c3622579 100644 --- a/packages/migration-sdk-viem/src/types/positions.ts +++ b/packages/migration-sdk-viem/src/types/positions.ts @@ -6,4 +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/aaveV2/borrow.test.ts b/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts new file mode 100644 index 00000000..bb02a9a5 --- /dev/null +++ b/packages/migration-sdk-viem/test/e2e/aaveV2/borrow.test.ts @@ -0,0 +1,1169 @@ +import { + BorrowMigrationLimiter, + MigratableProtocol, + SupplyMigrationLimiter, + fetchMigratablePositions, + migrationAddressesRegistry, +} from "../../../src/index.js"; + +import { + ChainId, + DEFAULT_SLIPPAGE_TOLERANCE, + type MarketParams, + MathLib, + 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, + 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 { 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", () => { + testFn("should fetch user position", async ({ client }: ViemTestContext) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseUnits("1000", 6); + + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdt, 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(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); + }); + + 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); + + await writeSupply(client, usdc, collateralAmount1, true); + await writeSupply(client, wNative, collateralAmount2, true); + await writeBorrow(client, usdt, 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 = parseUnits("1000", 6); + + await writeSupply(client, wNative, collateralAmount, true); + await writeSupply(client, usdc, pureSupply, false); + await writeBorrow(client, usdt, 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(wNative); + }, + ); + + 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); + + await writeSupply(client, wNative, collateralAmount, true); + await writeBorrow(client, usdc, borrowAmount1); + await writeBorrow(client, usdt, 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( + "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] }, + ); + + 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, + 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 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, + 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 = 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] }, + ); + + 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: [aWEth, generalAdapter1, migratedCollateral], + tx: { + to: aWEth, + 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, + 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 sendTransaction(client, migrationBundle.requirements.txs[0]!.tx); + await sendTransaction(client, migrationBundle.requirements.txs[1]!.tx); + + 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 without signature", + 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 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: [aWEth, generalAdapter1, maxUint256], + tx: { + to: aWEth, + 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( + 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 sendTransaction(client, migrationBundle.requirements.txs[0]!.tx); + await sendTransaction(client, migrationBundle.requirements.txs[1]!.tx); + + 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 limited by aave v3 liquidity", + async ({ client }) => { + const collateralAmount = parseEther("10"); + const borrowAmount = parseUnits("3000", 6); + + const liquidity = parseEther("4"); + + const migratedBorrow = parseUnits("1500", 6); + + 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); + + 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, + aWEth, + liquidity, + expect.any(BigInt), + null, + ], + type: "permit", + }, + { + args: [ + marketTo, + liquidity, + client.account.address, + [ + { + 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], + }, + ], + ], + 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(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}.`, + ); + } + }, + ); +}); 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 new file mode 100644 index 00000000..00902e11 --- /dev/null +++ b/packages/migration-sdk-viem/test/e2e/aaveV3/borrow.test.ts @@ -0,0 +1,1189 @@ +import { + BorrowMigrationLimiter, + MigratableProtocol, + SupplyMigrationLimiter, + fetchMigratablePositions, + migrationAddressesRegistry, +} from "../../../src/index.js"; + +import { + ChainId, + DEFAULT_SLIPPAGE_TOLERANCE, + MathLib, + addressesRegistry, +} 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 { + aTokenV3Abi, + variableDebtTokenV3Abi, +} from "../../../src/abis/aaveV3.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, + aWstEth: "0x0B925eD163218f6662a35e0f0371Ac234f9E9371", + variableDebtToken: "0xeA51d7853EEFb32b6ee06b1C12E6dcCA88Be0fFE", + testFn: test[ChainId.EthMainnet] as TestAPI, + marketTo: markets[ChainId.EthMainnet].eth_wstEth_2, + }, + { + chainId: ChainId.BaseMainnet, + aWstEth: "0x99CBC45ea5bb7eF3a5BC08FB1B7E56bB2442Ef0D", + variableDebtToken: "0x24e6e0795b3c7c71D965fCc4f371803d1c1DcA1E", + testFn: test[ChainId.BaseMainnet] as TestAPI, + marketTo: markets[ChainId.BaseMainnet].eth_wstEth, + }, +] as const; + +describe("Borrow position on AAVE V3", () => { + for (const { + chainId, + aWstEth, + testFn, + marketTo, + variableDebtToken, + } of TEST_CONFIGS) { + const wstEth = marketTo.collateralToken; + + const { pool } = migrationAddressesRegistry[chainId].aaveV3; + const { + bundler3: { generalAdapter1, aaveV3CoreMigrationAdapter }, + wNative, + usdc, + morpho, + } = addressesRegistry[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 = 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.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(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.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 = 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.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(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.aaveV3] }, + ); + + const aaveV3Positions = allPositions[MigratableProtocol.aaveV3]!; + expect(aaveV3Positions).toBeDefined(); + expect(aaveV3Positions).toHaveLength(0); + }, + ); + + 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.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, + }); + }, + ); + + 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, + 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}.`, + ); + } + }); + + 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.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, + [ + { + 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 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).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.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: [ + 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}.`, + ); + } + }, + ); + + 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}.`, + ); + } + }, + ); + }); + } +}); 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..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,27 +14,21 @@ 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"; -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, 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, 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 = {}) =>