From cd34bb69cc4089455d226219ee13dc3b373d4353 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Thu, 23 Jan 2025 11:55:25 +0100 Subject: [PATCH 1/2] idle WIP --- src/swapService/config/mainnet.ts | 11 + .../strategies/balmySDK/customSourceList.ts | 2 + .../balmySDK/idleAATrancheQuoteSource.ts | 201 ++++ .../strategies/strategyBalmySDK.ts | 1 + .../strategies/strategyERC4626Wrapper.ts | 22 +- src/swapService/strategies/strategyIdle.ts | 934 +++++++++++++++++ .../strategies/strategyIdleCDOTranche.ts | 935 ++++++++++++++++++ 7 files changed, 2099 insertions(+), 7 deletions(-) create mode 100644 src/swapService/strategies/balmySDK/idleAATrancheQuoteSource.ts create mode 100644 src/swapService/strategies/strategyIdle.ts create mode 100644 src/swapService/strategies/strategyIdleCDOTranche.ts diff --git a/src/swapService/config/mainnet.ts b/src/swapService/config/mainnet.ts index fc36668..7f2643d 100644 --- a/src/swapService/config/mainnet.ts +++ b/src/swapService/config/mainnet.ts @@ -15,6 +15,8 @@ const PT_WSTUSR1740182579 = "0xd0097149aa4cc0d0e1fc99b8bd73fc17dc32c1e9" const PT_WSTUSR_27MAR2025_MAINNET = "0xA8c8861b5ccF8CCe0ade6811CD2A7A7d3222B0B8" const USD0PLUSPLUS_MAINNET = "0x35d8949372d46b7a3d5a56006ae77b215fc69bc0" const YNETHX_MAINNET = "0x657d9aba1dbb59e53f9f3ecaa878447dcfc96dcb" +const IDLEAATRANCHEFASANARA_MAINNET = + "0x45054c6753b4Bce40C5d54418DabC20b070F85bE" const mainnetRoutingConfig: ChainRoutingConfig = [ // WRAPPERS @@ -37,6 +39,15 @@ const mainnetRoutingConfig: ChainRoutingConfig = [ excludeTokensInOrOut: [PT_WSTUSR_27MAR2025_MAINNET], }, }, + { + strategy: StrategyBalmySDK.name(), + config: { + sourcesFilter: { + includeSources: ["idle-tranche"], + }, + }, + match: { tokensInOrOut: [IDLEAATRANCHEFASANARA_MAINNET] }, + }, { strategy: StrategyBalmySDK.name(), config: { diff --git a/src/swapService/strategies/balmySDK/customSourceList.ts b/src/swapService/strategies/balmySDK/customSourceList.ts index 8ffaed1..6e3d03d 100644 --- a/src/swapService/strategies/balmySDK/customSourceList.ts +++ b/src/swapService/strategies/balmySDK/customSourceList.ts @@ -1,5 +1,6 @@ import type { IFetchService, IProviderService } from "@balmy/sdk" import { LocalSourceList } from "@balmy/sdk/dist/services/quotes/source-lists/local-source-list" +import { CustomIdleAATrancheQuoteSource } from "./idleAATrancheQuoteSource" import { CustomLiFiQuoteSource } from "./lifiQuoteSource" import { CustomNeptuneQuoteSource } from "./neptuneQuoteSource" import { CustomOdosQuoteSource } from "./odosQuoteSource" @@ -19,6 +20,7 @@ const customSources = { "open-ocean": new CustomOpenOceanQuoteSource(), neptune: new CustomNeptuneQuoteSource(), odos: new CustomOdosQuoteSource(), + "idle-tranche": new CustomIdleAATrancheQuoteSource(), } export class CustomSourceList extends LocalSourceList { constructor({ providerService, fetchService }: ConstructorParameters) { diff --git a/src/swapService/strategies/balmySDK/idleAATrancheQuoteSource.ts b/src/swapService/strategies/balmySDK/idleAATrancheQuoteSource.ts new file mode 100644 index 0000000..a3f7082 --- /dev/null +++ b/src/swapService/strategies/balmySDK/idleAATrancheQuoteSource.ts @@ -0,0 +1,201 @@ +import { Chains } from "@balmy/sdk" +import type { + BuildTxParams, + IQuoteSource, + QuoteParams, + QuoteSourceMetadata, + SourceQuoteResponse, + SourceQuoteTransaction, +} from "@balmy/sdk/dist/services/quotes/quote-sources/types" +import { + addQuoteSlippage, + calculateAllowanceTarget, +} from "@balmy/sdk/dist/services/quotes/quote-sources/utils" +import { + type Address, + type PublicClient, + encodeFunctionData, + isAddressEqual, +} from "viem" + +const assets: { + cdo: Address + aaTranche: Address + aaTrancheVault: Address + token: Address + tokenDecimals: bigint + swapHandler: Address + priceOne: bigint +}[] = [ + { + // IdleCDO AA Tranche - idle_Fasanara + cdo: "0xf6223C567F21E33e859ED7A045773526E9E3c2D5", + aaTranche: "0x45054c6753b4Bce40C5d54418DabC20b070F85bE", + aaTrancheVault: "0xd820C8129a853a04dC7e42C64aE62509f531eE5A", + token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + swapHandler: "0xA24689b6Ab48eCcF7038c70eBC39f9ed4217aFE3", + tokenDecimals: 6n, + priceOne: 1000000n, + }, +] + +// Supported networks: https://docs.1inch.io/docs/aggregation-protocol/introduction/#supported-networkschains +export const IDLEAATRANCHE_METADATA: QuoteSourceMetadata = + { + name: "IdleCDO", + supports: { + chains: [Chains.ETHEREUM.chainId], + swapAndTransfer: true, + buyOrders: false, + }, + logoURI: "", + } + +type IdleAATrancheSupport = { buyOrders: false; swapAndTransfer: true } +type IdleAATrancheConfig = object +type IdleAATrancheData = { tx: SourceQuoteTransaction } +export class CustomIdleAATrancheQuoteSource + implements + IQuoteSource +{ + getMetadata() { + return IDLEAATRANCHE_METADATA + } + + async quote( + params: QuoteParams, + ): Promise> { + const asset = assets.find( + (a) => + a.aaTranche === params.request.sellToken || + a.aaTranche === params.request.buyToken, + ) + if (!asset) throw new Error("Asset not found") + + const viemClient = params.components.providerService.getViemPublicClient({ + chainId: params.request.chainId, + }) as PublicClient + const virtualPrice = await fetchVirtualPrice( + viemClient, + asset.cdo, + asset.aaTranche, + ) + + const to = asset.swapHandler + let amountOut + let data + if (isAddressEqual(params.request.buyToken as Address, asset.aaTranche)) { + amountOut = + (params.request.order.sellAmount * + asset.priceOne * + 10n ** (18n - asset.tokenDecimals)) / + virtualPrice + data = encodeSwapExactTokensForAATranche(params.request.order.sellAmount) + } else { + amountOut = + (params.request.order.sellAmount * + virtualPrice * + 10n ** (18n - asset.tokenDecimals)) / + asset.priceOne + data = encodeSwapExactAATrancheForTokens(params.request.order.sellAmount) + } + + const quote = { + sellAmount: params.request.order.sellAmount, + buyAmount: BigInt(amountOut), + estimatedGas: undefined, + allowanceTarget: calculateAllowanceTarget(params.request.sellToken, to), + customData: { + tx: { + to, + calldata: data, + value: 0n, + }, + }, + } + + return addQuoteSlippage( + quote, + params.request.order.type, + params.request.config.slippagePercentage, + ) + } + + async buildTx({ + request, + }: BuildTxParams< + IdleAATrancheConfig, + IdleAATrancheData + >): Promise { + return request.customData.tx + } + + isConfigAndContextValidForQuoting( + config: Partial | undefined, + ): config is IdleAATrancheConfig { + return true + } + + isConfigAndContextValidForTxBuilding( + config: Partial | undefined, + ): config is IdleAATrancheConfig { + return true + } +} + +function encodeSwapExactTokensForAATranche(amount: bigint) { + const abiItem = { + inputs: [{ name: "amountIn", type: "uint256" }], + name: "swapExactTokensForAATranche", + stateMutability: "nonpayable", + type: "function", + } + + const functionData = encodeFunctionData({ + abi: [abiItem], + args: [amount], + }) + + return functionData +} + +function encodeSwapExactAATrancheForTokens(amount: bigint) { + const abiItem = { + inputs: [{ name: "amountIn", type: "uint256" }], + name: "swapExactAATrancheForTokens", + stateMutability: "nonpayable", + type: "function", + } + + const functionData = encodeFunctionData({ + abi: [abiItem], + args: [amount], + }) + + return functionData +} + +export async function fetchVirtualPrice( + client: PublicClient, + cdo: Address, + tranche: Address, +) { + const abiItem = { + name: "virtualPrice", + inputs: [{ name: "_tranche", type: "address" }], + outputs: [{ name: "_virtualPrice", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: cdo, + abi: [abiItem], + functionName: "virtualPrice", + args: [tranche], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} diff --git a/src/swapService/strategies/strategyBalmySDK.ts b/src/swapService/strategies/strategyBalmySDK.ts index d4d920f..aba2766 100644 --- a/src/swapService/strategies/strategyBalmySDK.ts +++ b/src/swapService/strategies/strategyBalmySDK.ts @@ -505,6 +505,7 @@ export class StrategyBalmySDK { ? getAddress(sdkQuote.source.allowanceTarget) : undefined + console.log("allowanceTarget: ", allowanceTarget) return { swapParams, amountIn: sdkQuote.sellAmount.amount, diff --git a/src/swapService/strategies/strategyERC4626Wrapper.ts b/src/swapService/strategies/strategyERC4626Wrapper.ts index 68bddc1..5cfaadd 100644 --- a/src/swapService/strategies/strategyERC4626Wrapper.ts +++ b/src/swapService/strategies/strategyERC4626Wrapper.ts @@ -230,10 +230,11 @@ export class StrategyERC4626Wrapper { ) const vaultData = this.getSupportedVault(swapParams.tokenIn.addressInfo) - + const tokenIn = findToken(swapParams.chainId, vaultData.asset) + if (!tokenIn) throw new Error("Inner token not found") const innerSwapParams = { ...swapParams, - tokenIn: findToken(swapParams.chainId, vaultData.asset), + tokenIn, amount: redeemAmountOut, } @@ -325,10 +326,11 @@ export class StrategyERC4626Wrapper { swapParams: SwapParams, ): Promise { const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) - + const tokenOut = findToken(swapParams.chainId, vaultData.asset) + if (!tokenOut) throw new Error("Inner token not found") const innerSwapParams = { ...swapParams, - tokenOut: findToken(swapParams.chainId, vaultData.asset), + tokenOut, receiver: swapParams.from, } @@ -436,9 +438,11 @@ export class StrategyERC4626Wrapper { ): Promise { // TODO expects dust out - add to dust list const vaultData = this.getSupportedVault(swapParams.tokenIn.addressInfo) + const tokenIn = findToken(swapParams.chainId, vaultData.asset) + if (!tokenIn) throw new Error("Inner token not found") const innerSwapParams = { ...swapParams, - tokenIn: findToken(swapParams.chainId, vaultData.asset), + tokenIn, vaultIn: vaultData.assetDustEVault, onlyFixedInputExactOut: true, // eliminate dust in the intermediate asset (vault underlying) } @@ -548,9 +552,11 @@ export class StrategyERC4626Wrapper { const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) const mintAmount = adjustForInterest(swapParams.amount) + const tokenIn = findToken(swapParams.chainId, vaultData.asset) + if (!tokenIn) throw new Error("Inner token in not found") const mintSwapParams = { ...swapParams, - tokenIn: findToken(swapParams.chainId, vaultData.asset), + tokenIn, vaultIn: vaultData.assetDustEVault, } @@ -565,10 +571,12 @@ export class StrategyERC4626Wrapper { swapParams.from, ) + const tokenOut = findToken(swapParams.chainId, vaultData.asset) + if (!tokenOut) throw new Error("Inner token not found") const innerSwapParams = { ...swapParams, amount: mintAmountIn, - tokenOut: findToken(swapParams.chainId, vaultData.asset), + tokenOut, receiver: swapParams.from, onlyFixedInputExactOut: true, // this option will overswap, which should cover growing exchange rate } diff --git a/src/swapService/strategies/strategyIdle.ts b/src/swapService/strategies/strategyIdle.ts new file mode 100644 index 0000000..68bddc1 --- /dev/null +++ b/src/swapService/strategies/strategyIdle.ts @@ -0,0 +1,934 @@ +import { viemClients } from "@/common/utils/viemClients" +import { + type Address, + encodeAbiParameters, + encodeFunctionData, + isAddressEqual, + parseAbiParameters, + publicActions, +} from "viem" +import { type SwapApiResponse, SwapperMode } from "../interface" +import { runPipeline } from "../runner" +import type { StrategyResult, SwapParams } from "../types" +import { + SWAPPER_HANDLER_GENERIC, + adjustForInterest, + applySlippage, + buildApiResponseSwap, + buildApiResponseVerifyDebtMax, + buildApiResponseVerifySkimMin, + encodeDepositMulticallItem, + encodeSwapMulticallItem, + findToken, + isExactInRepay, + matchParams, +} from "../utils" + +const defaultConfig: { + supportedVaults: Array<{ + chainId: number + vault: Address + asset: Address + assetDustEVault: Address + protocol: string + }> +} = { + supportedVaults: [ + { + chainId: 1, + protocol: "wstUSR", + vault: "0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055", + asset: "0x66a1E37c9b0eAddca17d3662D6c05F4DECf3e110", + assetDustEVault: "0x3a8992754e2ef51d8f90620d2766278af5c59b90", + }, + { + chainId: 1, + protocol: "wUSDL", + vault: "0x7751E2F4b8ae93EF6B79d86419d42FE3295A4559", + asset: "0xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD", + assetDustEVault: "0x0Fc9cdb39317354a98a1Afa6497a969ff3a6BA9C", + }, + { + chainId: 1, + protocol: "ynETHX", + vault: "0x657d9aba1dbb59e53f9f3ecaa878447dcfc96dcb", + asset: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + assetDustEVault: "0xb3b36220fA7d12f7055dab5c9FD18E860e9a6bF8", + }, + // { + // chainId: 1, + // protocol: "sUSDS", + // vault: "0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD", + // asset: "0xdc035d45d973e3ec169d2276ddab16f1e407384f", + // assetDustEVault: "0x98238Ee86f2c571AD06B0913bef21793dA745F57", + // }, + ], +} + +// Wrapper which adds an ERC4626 deposit or withdraw in front or at the back of a trade +export class StrategyERC4626Wrapper { + static name() { + return "erc4626_wrapper" + } + readonly match + readonly config + + constructor(match = {}, config = defaultConfig) { + this.match = match + this.config = config + } + + async supports(swapParams: SwapParams) { + return ( + !isExactInRepay(swapParams) && + this.config.supportedVaults.some( + (v) => + v.chainId === swapParams.chainId && + (isAddressEqual(v.vault, swapParams.tokenIn.addressInfo) || + isAddressEqual(v.vault, swapParams.tokenOut.addressInfo)), + ) + ) + } + + async findSwap(swapParams: SwapParams): Promise { + const result: StrategyResult = { + strategy: StrategyERC4626Wrapper.name(), + supports: await this.supports(swapParams), + match: matchParams(swapParams, this.match), + } + + if (!result.supports || !result.match) return result + + try { + switch (swapParams.swapperMode) { + case SwapperMode.EXACT_IN: { + if (this.isSupportedVault(swapParams.tokenIn.addressInfo)) { + if ( + this.isSupportedVaultUnderlying({ + vault: swapParams.tokenIn.addressInfo, + underlying: swapParams.tokenOut.addressInfo, + }) + ) { + result.response = + await this.exactInFromVaultToUnderlying(swapParams) + } else { + result.response = await this.exactInFromVaultToAny(swapParams) + } + } else { + if ( + this.isSupportedVaultUnderlying({ + vault: swapParams.tokenOut.addressInfo, + underlying: swapParams.tokenIn.addressInfo, + }) + ) { + result.response = + await this.exactInFromUnderlyingToVault(swapParams) + } else { + result.response = await this.exactInFromAnyToVault(swapParams) + } + } + break + } + case SwapperMode.TARGET_DEBT: { + if (this.isSupportedVault(swapParams.tokenIn.addressInfo)) { + if ( + this.isSupportedVaultUnderlying({ + vault: swapParams.tokenIn.addressInfo, + underlying: swapParams.tokenOut.addressInfo, + }) + ) { + result.response = + await this.targetDebtFromVaultToUnderlying(swapParams) + } else { + result.response = await this.targetDebtFromVaultToAny(swapParams) //test + } + } else { + if ( + this.isSupportedVaultUnderlying({ + vault: swapParams.tokenOut.addressInfo, + underlying: swapParams.tokenIn.addressInfo, + }) + ) { + result.response = + await this.targetDebtFromUnderlyingToVault(swapParams) + } else { + result.response = await this.targetDebtFromAnyToVault(swapParams) + } + } + break + } + // case SwapperMode.EXACT_OUT: + default: { + result.error = "Unsupported swap mode" + } + } + } catch (error) { + result.error = error + } + + return result + } + + async exactInFromVaultToUnderlying( + swapParams: SwapParams, + ): Promise { + const { + swapMulticallItem: redeemMulticallItem, + amountOut: redeemAmountOut, + } = await encodeRedeem( + swapParams, + swapParams.tokenIn.addressInfo, + swapParams.amount, + swapParams.receiver, + ) + + const multicallItems = [redeemMulticallItem] + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifySkimMin( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + redeemAmountOut, + swapParams.deadline, + ) + + return { + amountIn: String(swapParams.amount), + amountInMax: String(swapParams.amount), + amountOut: String(redeemAmountOut), + amountOutMin: String(redeemAmountOut), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: 0, + route: [ + { + providerName: this.getSupportedVault(swapParams.tokenIn.addressInfo) + .protocol, + }, + ], + swap, + verify, + } + } + + async exactInFromVaultToAny( + swapParams: SwapParams, + ): Promise { + const { + swapMulticallItem: redeemMulticallItem, + amountOut: redeemAmountOut, + } = await encodeRedeem( + swapParams, + swapParams.tokenIn.addressInfo, + swapParams.amount, + swapParams.from, + ) + + const vaultData = this.getSupportedVault(swapParams.tokenIn.addressInfo) + + const innerSwapParams = { + ...swapParams, + tokenIn: findToken(swapParams.chainId, vaultData.asset), + amount: redeemAmountOut, + } + + const innerSwap = await runPipeline(innerSwapParams) + + const intermediateDustDepositMulticallItem = encodeDepositMulticallItem( + vaultData.asset, + vaultData.assetDustEVault, + 5n, // avoid zero shares + swapParams.accountOut, + ) + + const multicallItems = [ + redeemMulticallItem, + ...innerSwap.swap.multicallItems, + intermediateDustDepositMulticallItem, + ] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + const verify = innerSwap.verify + + return { + amountIn: String(swapParams.amount), + amountInMax: String(swapParams.amount), + amountOut: innerSwap.amountOut, + amountOutMin: innerSwap.amountOutMin, + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [{ providerName: vaultData.protocol }, ...innerSwap.route], + swap, + verify, + } + } + + async exactInFromUnderlyingToVault( + swapParams: SwapParams, + ): Promise { + const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) + + const amountOut = await fetchPreviewDeposit( + swapParams.chainId, + vaultData.vault, + swapParams.amount, + ) + const swapperDepositMulticallItem = encodeDepositMulticallItem( + vaultData.asset, + vaultData.vault, + 0n, + swapParams.receiver, + ) + + const multicallItems = [swapperDepositMulticallItem] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const amountOutMin = applySlippage(amountOut, swapParams.slippage) // vault (tokenOut) can have growing exchange rate + const verify = buildApiResponseVerifySkimMin( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + amountOutMin, + swapParams.deadline, + ) + + return { + amountIn: String(swapParams.amount), + amountInMax: String(swapParams.amount), + amountOut: String(amountOut), + amountOutMin: String(amountOutMin), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [{ providerName: vaultData.protocol }], + swap, + verify, + } + } + + async exactInFromAnyToVault( + swapParams: SwapParams, + ): Promise { + const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) + + const innerSwapParams = { + ...swapParams, + tokenOut: findToken(swapParams.chainId, vaultData.asset), + receiver: swapParams.from, + } + + const innerSwap = await runPipeline(innerSwapParams) + const amountOut = await fetchPreviewDeposit( + swapParams.chainId, + vaultData.vault, + BigInt(innerSwap.amountOut), + ) + const amountOutMin = await fetchPreviewDeposit( + swapParams.chainId, + vaultData.vault, + BigInt(innerSwap.amountOutMin), + ) + + // Swapper.deposit will deposit all of available balance into the wrapper, and move the wrapper straight to receiver, where it can be skimmed + const swapperDepositMulticallItem = encodeDepositMulticallItem( + vaultData.asset, + vaultData.vault, + 0n, + swapParams.receiver, + ) + + const multicallItems = [ + ...innerSwap.swap.multicallItems, + swapperDepositMulticallItem, + ] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + const verify = buildApiResponseVerifySkimMin( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + amountOutMin, + swapParams.deadline, + ) + + return { + amountIn: String(swapParams.amount), + amountInMax: String(swapParams.amount), + amountOut: String(amountOut), + amountOutMin: String(amountOutMin), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [{ providerName: vaultData.protocol }, ...innerSwap.route], + swap, + verify, + } + } + + async targetDebtFromVaultToUnderlying( + swapParams: SwapParams, + ): Promise { + // TODO expects dust - add to dust list + const vaultData = this.getSupportedVault(swapParams.tokenIn.addressInfo) + const withdrawAmount = adjustForInterest(swapParams.amount) + + const { + swapMulticallItem: withdrawMulticallItem, + amountIn, + amountOut, + } = await encodeWithdraw( + swapParams, + vaultData.vault, + withdrawAmount, + swapParams.from, + ) + + const multicallItems = [withdrawMulticallItem] + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifyDebtMax( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + swapParams.targetDebt, + swapParams.deadline, + ) + + return { + amountIn: String(amountIn), // adjusted for accruing debt + amountInMax: String(amountIn), + amountOut: String(amountOut), + amountOutMin: String(amountOut), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: 0, + route: [{ providerName: vaultData.protocol }], + swap, + verify, + } + } + + async targetDebtFromVaultToAny( + swapParams: SwapParams, + ): Promise { + // TODO expects dust out - add to dust list + const vaultData = this.getSupportedVault(swapParams.tokenIn.addressInfo) + const innerSwapParams = { + ...swapParams, + tokenIn: findToken(swapParams.chainId, vaultData.asset), + vaultIn: vaultData.assetDustEVault, + onlyFixedInputExactOut: true, // eliminate dust in the intermediate asset (vault underlying) + } + + const innerQuote = await runPipeline(innerSwapParams) + + const withdrawSwapParams = { + ...swapParams, + swapperMode: SwapperMode.EXACT_IN, // change to exact in, otherwise multicall item will be target debt and will attempt a repay + } + const { + swapMulticallItem: withdrawMulticallItem, + amountIn: withdrawAmountIn, + } = await encodeWithdraw( + withdrawSwapParams, + vaultData.vault, + BigInt(innerQuote.amountIn), + swapParams.from, + ) + + // repay or exact out will return unused input, which is the intermediate asset + const multicallItems = [ + withdrawMulticallItem, + ...innerQuote.swap.multicallItems, + ] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifyDebtMax( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + swapParams.targetDebt, + swapParams.deadline, + ) + + return { + amountIn: String(withdrawAmountIn), + amountInMax: String(withdrawAmountIn), + amountOut: String(innerQuote.amountOut), + amountOutMin: String(innerQuote.amountOutMin), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [{ providerName: vaultData.protocol }, ...innerQuote.route], + swap, + verify, + } + } + + async targetDebtFromUnderlyingToVault( + swapParams: SwapParams, + ): Promise { + const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) + + const mintAmount = adjustForInterest(swapParams.amount) + + const { + swapMulticallItem: mintMulticallItem, + amountIn, + amountOut, + } = await encodeMint( + swapParams, + vaultData.vault, + mintAmount, + swapParams.from, + ) + + // mint is encoded in target debt mode, so repay will happen automatically + const multicallItems = [mintMulticallItem] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifyDebtMax( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + swapParams.targetDebt, + swapParams.deadline, + ) + + return { + amountIn: String(amountIn), + amountInMax: String(adjustForInterest(amountIn)), // compensate for intrinsic interest accrued in the vault (tokenIn) + amountOut: String(amountOut), + amountOutMin: String(amountOut), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: 0, + route: [{ providerName: vaultData.protocol }], + swap, + verify, + } + } + + async targetDebtFromAnyToVault( + swapParams: SwapParams, + ): Promise { + const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) + + const mintAmount = adjustForInterest(swapParams.amount) + const mintSwapParams = { + ...swapParams, + tokenIn: findToken(swapParams.chainId, vaultData.asset), + vaultIn: vaultData.assetDustEVault, + } + + const { + swapMulticallItem: mintMulticallItem, + amountIn: mintAmountIn, + amountOut, + } = await encodeMint( + mintSwapParams, + vaultData.vault, + mintAmount, + swapParams.from, + ) + + const innerSwapParams = { + ...swapParams, + amount: mintAmountIn, + tokenOut: findToken(swapParams.chainId, vaultData.asset), + receiver: swapParams.from, + onlyFixedInputExactOut: true, // this option will overswap, which should cover growing exchange rate + } + + const innerQuote = await runPipeline(innerSwapParams) + + // re-encode inner swap from target debt to exact out so that repay is not executed before mint TODO fix with exact out support in all strategies + const innerSwapItems = innerQuote.swap.multicallItems.map((item) => { + if (item.functionName !== "swap") return item + + const newItem = encodeSwapMulticallItem({ + ...item.args[0], + mode: BigInt(SwapperMode.EXACT_OUT), + }) + + return newItem + }) + + // repay is done through mint item, which will return unused input, which is the intermediate asset + const multicallItems = [...innerSwapItems, mintMulticallItem] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifyDebtMax( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + swapParams.targetDebt, + swapParams.deadline, + ) + + return { + amountIn: String(innerQuote.amountIn), + amountInMax: String(innerQuote.amountInMax), + amountOut: String(amountOut), + amountOutMin: String(amountOut), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [...innerQuote.route, { providerName: vaultData.protocol }], + swap, + verify, + } + } + + isSupportedVault(vault: Address) { + return this.config.supportedVaults.some((v) => + isAddressEqual(v.vault, vault), + ) + } + + isSupportedVaultUnderlying({ + vault, + underlying, + }: { vault: Address; underlying: Address }) { + const asset = this.config.supportedVaults.find((v) => + isAddressEqual(v.vault, vault), + )?.asset + return !!asset && isAddressEqual(asset, underlying) + } + + getSupportedVault(vault: Address) { + const supportedVault = this.config.supportedVaults.find((v) => + isAddressEqual(v.vault, vault), + ) + if (!supportedVault) throw new Error("Vault not supported") + + return supportedVault + } +} + +export async function encodeRedeem( + swapParams: SwapParams, + vault: Address, + amountIn: bigint, + receiver: Address, +) { + const amountOut = await fetchPreviewRedeem( + swapParams.chainId, + vault, + amountIn, + ) + + const abiItem = { + inputs: [ + { name: "amount", type: "uint256" }, + { name: "receiver", type: "address" }, + { name: "owner", type: "address" }, + ], + name: "redeem", + stateMutability: "nonpayable", + type: "function", + } + + const redeemData = encodeFunctionData({ + abi: [abiItem], + args: [amountIn, receiver, swapParams.from], + }) + + const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ + vault, + redeemData, + ]) + + const swapperAmountOut = + swapParams.swapperMode === SwapperMode.EXACT_IN + ? 0n //ignored + : swapParams.swapperMode === SwapperMode.EXACT_OUT + ? amountOut + : swapParams.targetDebt + + const swapMulticallItem = encodeSwapMulticallItem({ + handler: SWAPPER_HANDLER_GENERIC, + mode: BigInt(swapParams.swapperMode), + account: swapParams.accountOut, + tokenIn: swapParams.tokenIn.addressInfo, + tokenOut: swapParams.tokenOut.addressInfo, + vaultIn: swapParams.vaultIn, + accountIn: swapParams.accountIn, + receiver: swapParams.receiver, + amountOut: swapperAmountOut, + data: swapData, + }) + + return { + amountIn, + amountOut, + swapMulticallItem, + } +} + +export async function encodeWithdraw( + swapParams: SwapParams, + vault: Address, + amountOut: bigint, + receiver: Address, +) { + const amountIn = await fetchPreviewWithdraw( + swapParams.chainId, + vault, + amountOut, + ) + + const abiItem = { + inputs: [ + { name: "amount", type: "uint256" }, + { name: "receiver", type: "address" }, + { name: "owner", type: "address" }, + ], + name: "withdraw", + stateMutability: "nonpayable", + type: "function", + } + + const withdrawData = encodeFunctionData({ + abi: [abiItem], + args: [amountOut, receiver, swapParams.from], + }) + + const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ + vault, + withdrawData, + ]) + + const swapperAmountOut = + swapParams.swapperMode === SwapperMode.EXACT_IN + ? 0n //ignored + : swapParams.swapperMode === SwapperMode.EXACT_OUT + ? amountOut + : swapParams.targetDebt + + const swapMulticallItem = encodeSwapMulticallItem({ + handler: SWAPPER_HANDLER_GENERIC, + mode: BigInt(swapParams.swapperMode), + account: swapParams.accountOut, + tokenIn: swapParams.tokenIn.addressInfo, + tokenOut: swapParams.tokenOut.addressInfo, + vaultIn: swapParams.vaultIn, + accountIn: swapParams.accountIn, + receiver: swapParams.receiver, + amountOut: swapperAmountOut, + data: swapData, + }) + + return { + amountIn, + amountOut, + swapMulticallItem, + } +} + +export async function encodeMint( + swapParams: SwapParams, + vault: Address, + amountOut: bigint, + receiver: Address, +) { + const amountIn = await fetchPreviewMint(swapParams.chainId, vault, amountOut) + + const abiItem = { + inputs: [ + { name: "amount", type: "uint256" }, + { name: "receiver", type: "address" }, + ], + name: "mint", + stateMutability: "nonpayable", + type: "function", + } + + const mintData = encodeFunctionData({ + abi: [abiItem], + args: [amountOut, receiver], + }) + + const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ + vault, + mintData, + ]) + + const swapperAmountOut = + swapParams.swapperMode === SwapperMode.EXACT_IN + ? 0n //ignored + : swapParams.swapperMode === SwapperMode.EXACT_OUT + ? amountOut + : swapParams.targetDebt + + const swapMulticallItem = encodeSwapMulticallItem({ + handler: SWAPPER_HANDLER_GENERIC, + mode: BigInt(swapParams.swapperMode), + account: swapParams.accountOut, + tokenIn: swapParams.tokenIn.addressInfo, + tokenOut: swapParams.tokenOut.addressInfo, + vaultIn: swapParams.vaultIn, + accountIn: swapParams.accountIn, + receiver: swapParams.receiver, + amountOut: swapperAmountOut, + data: swapData, + }) + + return { + amountIn, + amountOut, + swapMulticallItem, + } +} + +export async function fetchPreviewRedeem( + chainId: number, + vault: Address, + amount: bigint, +) { + const client = getViemClient(chainId) + + const abiItem = { + name: "previewRedeem", + inputs: [{ name: "shares", type: "uint256" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: vault, + abi: [abiItem], + functionName: "previewRedeem", + args: [amount], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} + +export async function fetchPreviewWithdraw( + chainId: number, + vault: Address, + amount: bigint, +) { + const client = getViemClient(chainId) + + const abiItem = { + name: "previewWithdraw", + inputs: [{ name: "assets", type: "uint256" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: vault, + abi: [abiItem], + functionName: "previewWithdraw", + args: [amount], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} + +export async function fetchPreviewDeposit( + chainId: number, + vault: Address, + amount: bigint, +) { + const client = getViemClient(chainId) + + const abiItem = { + name: "previewDeposit", + inputs: [{ name: "assets", type: "uint256" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: vault, + abi: [abiItem], + functionName: "previewDeposit", + args: [amount], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} + +export async function fetchPreviewMint( + chainId: number, + vault: Address, + amount: bigint, +) { + const client = getViemClient(chainId) + + const abiItem = { + name: "previewMint", + inputs: [{ name: "shares", type: "uint256" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: vault, + abi: [abiItem], + functionName: "previewMint", + args: [amount], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} + +const getViemClient = (chainId: number) => { + if (!viemClients[chainId]) + throw new Error(`No client found for chainId ${chainId}`) + return viemClients[chainId].extend(publicActions) +} diff --git a/src/swapService/strategies/strategyIdleCDOTranche.ts b/src/swapService/strategies/strategyIdleCDOTranche.ts new file mode 100644 index 0000000..aed7a41 --- /dev/null +++ b/src/swapService/strategies/strategyIdleCDOTranche.ts @@ -0,0 +1,935 @@ +import { viemClients } from "@/common/utils/viemClients" +import { + type Address, + encodeAbiParameters, + encodeFunctionData, + isAddressEqual, + parseAbiParameters, + publicActions, +} from "viem" +import { type SwapApiResponse, SwapperMode } from "../interface" +import { runPipeline } from "../runner" +import type { StrategyResult, SwapParams } from "../types" +import { + SWAPPER_HANDLER_GENERIC, + adjustForInterest, + applySlippage, + buildApiResponseSwap, + buildApiResponseVerifyDebtMax, + buildApiResponseVerifySkimMin, + encodeDepositMulticallItem, + encodeSwapMulticallItem, + findToken, + isExactInRepay, + matchParams, +} from "../utils" + +const defaultConfig: { + supportedTranches: Array<{ + chainId: number + swapHandler: Address + cdo: Address + aaTranche: Address + aaTrancheVault: Address + underlying: Address + underlyingDustVault: Address + underlyingDecimals: bigint + priceOne: bigint + }> +} = { + supportedTranches: [ + { + // IdleCDO AA Tranche - idle_Fasanara + chainId: 1, + swapHandler: "0xA24689b6Ab48eCcF7038c70eBC39f9ed4217aFE3", + cdo: "0xf6223C567F21E33e859ED7A045773526E9E3c2D5", + aaTranche: "0x45054c6753b4Bce40C5d54418DabC20b070F85bE", + aaTrancheVault: "0xd820C8129a853a04dC7e42C64aE62509f531eE5A", + underlying: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + underlyingDustVault: "0xb93d4928f39fbcd6c89a7dfbf0a867e6344561be", // eUSDC-1 escrow + underlyingDecimals: 6n, + priceOne: 1000000n, + }, + ], +} + +const PROTOCOL = { providerName: "IdleCDO" } + +// Wrapper which adds an ERC4626 deposit or withdraw in front or at the back of a trade +export class StrategyERC4626Wrapper { + static name() { + return "erc4626_wrapper" + } + readonly match + readonly config + + constructor(match = {}, config = defaultConfig) { + this.match = match + this.config = config + } + + async supports(swapParams: SwapParams) { + return ( + !isExactInRepay(swapParams) && + this.config.supportedTranches.some( + (v) => + v.chainId === swapParams.chainId && + (isAddressEqual(v.aaTranche, swapParams.tokenIn.addressInfo) || + isAddressEqual(v.aaTranche, swapParams.tokenOut.addressInfo)), + ) + ) + } + + async findSwap(swapParams: SwapParams): Promise { + const result: StrategyResult = { + strategy: StrategyERC4626Wrapper.name(), + supports: await this.supports(swapParams), + match: matchParams(swapParams, this.match), + } + + if (!result.supports || !result.match) return result + + try { + switch (swapParams.swapperMode) { + case SwapperMode.EXACT_IN: { + if (this.isSupportedTranche(swapParams.tokenIn.addressInfo)) { + if ( + this.isSupportedTrancheUnderlying({ + aaTranche: swapParams.tokenIn.addressInfo, + underlying: swapParams.tokenOut.addressInfo, + }) + ) { + result.response = + await this.exactInFromAssetToUnderlying(swapParams) + } else { + result.response = await this.exactInFromAssetToAny(swapParams) + } + } else { + if ( + this.isSupportedTrancheUnderlying({ + aaTranche: swapParams.tokenOut.addressInfo, + underlying: swapParams.tokenIn.addressInfo, + }) + ) { + result.response = + await this.exactInFromUnderlyingToAsset(swapParams) + } else { + result.response = await this.exactInFromAnyToAsset(swapParams) + } + } + break + } + case SwapperMode.TARGET_DEBT: { + if (this.isSupportedTranche(swapParams.tokenIn.addressInfo)) { + if ( + this.isSupportedTrancheUnderlying({ + aaTranche: swapParams.tokenIn.addressInfo, + underlying: swapParams.tokenOut.addressInfo, + }) + ) { + result.response = + await this.targetDebtFromVaultToUnderlying(swapParams) + } else { + result.response = await this.targetDebtFromVaultToAny(swapParams) //test + } + } else { + if ( + this.isSupportedTrancheUnderlying({ + aaTranche: swapParams.tokenOut.addressInfo, + underlying: swapParams.tokenIn.addressInfo, + }) + ) { + result.response = + await this.targetDebtFromUnderlyingToVault(swapParams) + } else { + result.response = await this.targetDebtFromAnyToVault(swapParams) + } + } + break + } + // case SwapperMode.EXACT_OUT: + default: { + result.error = "Unsupported swap mode" + } + } + } catch (error) { + result.error = error + } + + return result + } + + async exactInFromAssetToUnderlying( + swapParams: SwapParams, + ): Promise { + const { + swapMulticallItem: redeemMulticallItem, + amountOut: redeemAmountOut, + } = await encodeRedeem( + swapParams, + swapParams.tokenIn.addressInfo, + swapParams.amount, + swapParams.receiver, + ) + + const multicallItems = [redeemMulticallItem] + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifySkimMin( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + redeemAmountOut, + swapParams.deadline, + ) + + return { + amountIn: String(swapParams.amount), + amountInMax: String(swapParams.amount), + amountOut: String(redeemAmountOut), + amountOutMin: String(redeemAmountOut), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: 0, + route: [PROTOCOL], + swap, + verify, + } + } + + async exactInFromAssetToAny( + swapParams: SwapParams, + ): Promise { + const { + swapMulticallItem: redeemMulticallItem, + amountOut: redeemAmountOut, + } = await encodeRedeem( + swapParams, + swapParams.tokenIn.addressInfo, + swapParams.amount, + swapParams.from, + ) + + const trancheData = this.getSupportedTranche(swapParams.tokenIn.addressInfo) + const tokenIn = findToken(swapParams.chainId, trancheData.underlying) + if (!tokenIn) throw new Error("Inner token not found") + const innerSwapParams = { + ...swapParams, + tokenIn, + amount: redeemAmountOut, + } + + const innerSwap = await runPipeline(innerSwapParams) + + const intermediateDustDepositMulticallItem = encodeDepositMulticallItem( + trancheData.underlying, + trancheData.underlyingDustVault, + 5n, // avoid zero shares + swapParams.accountOut, + ) + + const multicallItems = [ + redeemMulticallItem, + ...innerSwap.swap.multicallItems, + intermediateDustDepositMulticallItem, + ] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + const verify = innerSwap.verify + + return { + amountIn: String(swapParams.amount), + amountInMax: String(swapParams.amount), + amountOut: innerSwap.amountOut, + amountOutMin: innerSwap.amountOutMin, + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [PROTOCOL, ...innerSwap.route], + swap, + verify, + } + } + + async exactInFromUnderlyingToAsset( + swapParams: SwapParams, + ): Promise { + const trancheData = this.getSupportedTranche( + swapParams.tokenOut.addressInfo, + ) + + const amountOut = await fetchPreviewDeposit( + swapParams.chainId, + trancheData.aaTranche, + swapParams.amount, + ) + const swapperDepositMulticallItem = encodeDepositMulticallItem( + trancheData.underlying, + trancheData.aaTranche, + 0n, + swapParams.receiver, + ) + + const multicallItems = [swapperDepositMulticallItem] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const amountOutMin = applySlippage(amountOut, swapParams.slippage) // vault (tokenOut) can have growing exchange rate + const verify = buildApiResponseVerifySkimMin( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + amountOutMin, + swapParams.deadline, + ) + + return { + amountIn: String(swapParams.amount), + amountInMax: String(swapParams.amount), + amountOut: String(amountOut), + amountOutMin: String(amountOutMin), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [PROTOCOL], + swap, + verify, + } + } + + async exactInFromAnyToAsset( + swapParams: SwapParams, + ): Promise { + const trancheData = this.getSupportedTranche( + swapParams.tokenOut.addressInfo, + ) + const tokenOut = findToken(swapParams.chainId, trancheData.underlying) + if (!tokenOut) throw new Error("Inner token not found") + const innerSwapParams = { + ...swapParams, + tokenOut, + receiver: swapParams.from, + } + + const innerSwap = await runPipeline(innerSwapParams) + const amountOut = await fetchPreviewDeposit( + swapParams.chainId, + trancheData.aaTranche, + BigInt(innerSwap.amountOut), + ) + const amountOutMin = await fetchPreviewDeposit( + swapParams.chainId, + trancheData.aaTranche, + BigInt(innerSwap.amountOutMin), + ) + + // Swapper.deposit will deposit all of available balance into the wrapper, and move the wrapper straight to receiver, where it can be skimmed + const swapperDepositMulticallItem = encodeDepositMulticallItem( + trancheData.underlying, + trancheData.aaTranche, + 0n, + swapParams.receiver, + ) + + const multicallItems = [ + ...innerSwap.swap.multicallItems, + swapperDepositMulticallItem, + ] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + const verify = buildApiResponseVerifySkimMin( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + amountOutMin, + swapParams.deadline, + ) + + return { + amountIn: String(swapParams.amount), + amountInMax: String(swapParams.amount), + amountOut: String(amountOut), + amountOutMin: String(amountOutMin), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [PROTOCOL, ...innerSwap.route], + swap, + verify, + } + } + + async targetDebtFromVaultToUnderlying( + swapParams: SwapParams, + ): Promise { + // TODO expects dust - add to dust list + const trancheData = this.getSupportedTranche(swapParams.tokenIn.addressInfo) + const withdrawAmount = adjustForInterest(swapParams.amount) + + const { + swapMulticallItem: withdrawMulticallItem, + amountIn, + amountOut, + } = await encodeWithdraw( + swapParams, + trancheData.aaTranche, + withdrawAmount, + swapParams.from, + ) + + const multicallItems = [withdrawMulticallItem] + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifyDebtMax( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + swapParams.targetDebt, + swapParams.deadline, + ) + + return { + amountIn: String(amountIn), // adjusted for accruing debt + amountInMax: String(amountIn), + amountOut: String(amountOut), + amountOutMin: String(amountOut), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: 0, + route: [PROTOCOL], + swap, + verify, + } + } + + async targetDebtFromVaultToAny( + swapParams: SwapParams, + ): Promise { + // TODO expects dust out - add to dust list + const trancheData = this.getSupportedTranche(swapParams.tokenIn.addressInfo) + const tokenIn = findToken(swapParams.chainId, trancheData.underlying) + if (!tokenIn) throw new Error("Inner token not found") + const innerSwapParams = { + ...swapParams, + tokenIn, + vaultIn: trancheData.underlyingDustVault, + onlyFixedInputExactOut: true, // eliminate dust in the intermediate asset (vault underlying) + } + + const innerQuote = await runPipeline(innerSwapParams) + + const withdrawSwapParams = { + ...swapParams, + swapperMode: SwapperMode.EXACT_IN, // change to exact in, otherwise multicall item will be target debt and will attempt a repay + } + const { + swapMulticallItem: withdrawMulticallItem, + amountIn: withdrawAmountIn, + } = await encodeWithdraw( + withdrawSwapParams, + trancheData.aaTranche, + BigInt(innerQuote.amountIn), + swapParams.from, + ) + + // repay or exact out will return unused input, which is the intermediate asset + const multicallItems = [ + withdrawMulticallItem, + ...innerQuote.swap.multicallItems, + ] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifyDebtMax( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + swapParams.targetDebt, + swapParams.deadline, + ) + + return { + amountIn: String(withdrawAmountIn), + amountInMax: String(withdrawAmountIn), + amountOut: String(innerQuote.amountOut), + amountOutMin: String(innerQuote.amountOutMin), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [PROTOCOL, ...innerQuote.route], + swap, + verify, + } + } + + async targetDebtFromUnderlyingToVault( + swapParams: SwapParams, + ): Promise { + const trancheData = this.getSupportedTranche( + swapParams.tokenOut.addressInfo, + ) + + const mintAmount = adjustForInterest(swapParams.amount) + + const { + swapMulticallItem: mintMulticallItem, + amountIn, + amountOut, + } = await encodeMint( + swapParams, + trancheData.aaTranche, + mintAmount, + swapParams.from, + ) + + // mint is encoded in target debt mode, so repay will happen automatically + const multicallItems = [mintMulticallItem] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifyDebtMax( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + swapParams.targetDebt, + swapParams.deadline, + ) + + return { + amountIn: String(amountIn), + amountInMax: String(adjustForInterest(amountIn)), // compensate for intrinsic interest accrued in the vault (tokenIn) + amountOut: String(amountOut), + amountOutMin: String(amountOut), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: 0, + route: [PROTOCOL], + swap, + verify, + } + } + + async targetDebtFromAnyToVault( + swapParams: SwapParams, + ): Promise { + const trancheData = this.getSupportedTranche( + swapParams.tokenOut.addressInfo, + ) + + const mintAmount = adjustForInterest(swapParams.amount) + const tokenIn = findToken(swapParams.chainId, trancheData.underlying) + if (!tokenIn) throw new Error("Inner token in not found") + const mintSwapParams = { + ...swapParams, + tokenIn, + vaultIn: trancheData.underlyingDustVault, + } + + const { + swapMulticallItem: mintMulticallItem, + amountIn: mintAmountIn, + amountOut, + } = await encodeMint( + mintSwapParams, + trancheData.aaTranche, + mintAmount, + swapParams.from, + ) + + const tokenOut = findToken(swapParams.chainId, trancheData.underlying) + if (!tokenOut) throw new Error("Inner token not found") + const innerSwapParams = { + ...swapParams, + amount: mintAmountIn, + tokenOut, + receiver: swapParams.from, + onlyFixedInputExactOut: true, // this option will overswap, which should cover growing exchange rate + } + + const innerQuote = await runPipeline(innerSwapParams) + + // re-encode inner swap from target debt to exact out so that repay is not executed before mint TODO fix with exact out support in all strategies + const innerSwapItems = innerQuote.swap.multicallItems.map((item) => { + if (item.functionName !== "swap") return item + + const newItem = encodeSwapMulticallItem({ + ...item.args[0], + mode: BigInt(SwapperMode.EXACT_OUT), + }) + + return newItem + }) + + // repay is done through mint item, which will return unused input, which is the intermediate asset + const multicallItems = [...innerSwapItems, mintMulticallItem] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const verify = buildApiResponseVerifyDebtMax( + swapParams.chainId, + swapParams.receiver, + swapParams.accountOut, + swapParams.targetDebt, + swapParams.deadline, + ) + + return { + amountIn: String(innerQuote.amountIn), + amountInMax: String(innerQuote.amountInMax), + amountOut: String(amountOut), + amountOutMin: String(amountOut), + vaultIn: swapParams.vaultIn, + receiver: swapParams.receiver, + accountIn: swapParams.accountIn, + accountOut: swapParams.accountOut, + tokenIn: swapParams.tokenIn, + tokenOut: swapParams.tokenOut, + slippage: swapParams.slippage, + route: [...innerQuote.route, PROTOCOL], + swap, + verify, + } + } + + isSupportedTranche(asset: Address) { + return this.config.supportedTranches.some((v) => + isAddressEqual(v.aaTranche, asset), + ) + } + + isSupportedTrancheUnderlying({ + aaTranche, + underlying, + }: { aaTranche: Address; underlying: Address }) { + const asset = this.config.supportedTranches.find((v) => + isAddressEqual(v.aaTranche, aaTranche), + )?.underlying + return !!asset && isAddressEqual(asset, underlying) + } + + getSupportedTranche(aaTranche: Address) { + const supportedTranche = this.config.supportedTranches.find((v) => + isAddressEqual(v.aaTranche, aaTranche), + ) + if (!supportedTranche) throw new Error("Tranche not supported") + + return supportedTranche + } +} + +export async function encodeRedeem( + swapParams: SwapParams, + vault: Address, + amountIn: bigint, + receiver: Address, +) { + const amountOut = await fetchPreviewRedeem( + swapParams.chainId, + vault, + amountIn, + ) + + const abiItem = { + inputs: [ + { name: "amount", type: "uint256" }, + { name: "receiver", type: "address" }, + { name: "owner", type: "address" }, + ], + name: "redeem", + stateMutability: "nonpayable", + type: "function", + } + + const redeemData = encodeFunctionData({ + abi: [abiItem], + args: [amountIn, receiver, swapParams.from], + }) + + const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ + vault, + redeemData, + ]) + + const swapperAmountOut = + swapParams.swapperMode === SwapperMode.EXACT_IN + ? 0n //ignored + : swapParams.swapperMode === SwapperMode.EXACT_OUT + ? amountOut + : swapParams.targetDebt + + const swapMulticallItem = encodeSwapMulticallItem({ + handler: SWAPPER_HANDLER_GENERIC, + mode: BigInt(swapParams.swapperMode), + account: swapParams.accountOut, + tokenIn: swapParams.tokenIn.addressInfo, + tokenOut: swapParams.tokenOut.addressInfo, + vaultIn: swapParams.vaultIn, + accountIn: swapParams.accountIn, + receiver: swapParams.receiver, + amountOut: swapperAmountOut, + data: swapData, + }) + + return { + amountIn, + amountOut, + swapMulticallItem, + } +} + +export async function encodeWithdraw( + swapParams: SwapParams, + vault: Address, + amountOut: bigint, + receiver: Address, +) { + const amountIn = await fetchPreviewWithdraw( + swapParams.chainId, + vault, + amountOut, + ) + + const abiItem = { + inputs: [ + { name: "amount", type: "uint256" }, + { name: "receiver", type: "address" }, + { name: "owner", type: "address" }, + ], + name: "withdraw", + stateMutability: "nonpayable", + type: "function", + } + + const withdrawData = encodeFunctionData({ + abi: [abiItem], + args: [amountOut, receiver, swapParams.from], + }) + + const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ + vault, + withdrawData, + ]) + + const swapperAmountOut = + swapParams.swapperMode === SwapperMode.EXACT_IN + ? 0n //ignored + : swapParams.swapperMode === SwapperMode.EXACT_OUT + ? amountOut + : swapParams.targetDebt + + const swapMulticallItem = encodeSwapMulticallItem({ + handler: SWAPPER_HANDLER_GENERIC, + mode: BigInt(swapParams.swapperMode), + account: swapParams.accountOut, + tokenIn: swapParams.tokenIn.addressInfo, + tokenOut: swapParams.tokenOut.addressInfo, + vaultIn: swapParams.vaultIn, + accountIn: swapParams.accountIn, + receiver: swapParams.receiver, + amountOut: swapperAmountOut, + data: swapData, + }) + + return { + amountIn, + amountOut, + swapMulticallItem, + } +} + +export async function encodeMint( + swapParams: SwapParams, + vault: Address, + amountOut: bigint, + receiver: Address, +) { + const amountIn = await fetchPreviewMint(swapParams.chainId, vault, amountOut) + + const abiItem = { + inputs: [ + { name: "amount", type: "uint256" }, + { name: "receiver", type: "address" }, + ], + name: "mint", + stateMutability: "nonpayable", + type: "function", + } + + const mintData = encodeFunctionData({ + abi: [abiItem], + args: [amountOut, receiver], + }) + + const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ + vault, + mintData, + ]) + + const swapperAmountOut = + swapParams.swapperMode === SwapperMode.EXACT_IN + ? 0n //ignored + : swapParams.swapperMode === SwapperMode.EXACT_OUT + ? amountOut + : swapParams.targetDebt + + const swapMulticallItem = encodeSwapMulticallItem({ + handler: SWAPPER_HANDLER_GENERIC, + mode: BigInt(swapParams.swapperMode), + account: swapParams.accountOut, + tokenIn: swapParams.tokenIn.addressInfo, + tokenOut: swapParams.tokenOut.addressInfo, + vaultIn: swapParams.vaultIn, + accountIn: swapParams.accountIn, + receiver: swapParams.receiver, + amountOut: swapperAmountOut, + data: swapData, + }) + + return { + amountIn, + amountOut, + swapMulticallItem, + } +} + +export async function fetchPreviewRedeem( + chainId: number, + vault: Address, + amount: bigint, +) { + const client = getViemClient(chainId) + + const abiItem = { + name: "previewRedeem", + inputs: [{ name: "shares", type: "uint256" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: vault, + abi: [abiItem], + functionName: "previewRedeem", + args: [amount], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} + +export async function fetchPreviewWithdraw( + chainId: number, + vault: Address, + amount: bigint, +) { + const client = getViemClient(chainId) + + const abiItem = { + name: "previewWithdraw", + inputs: [{ name: "assets", type: "uint256" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: vault, + abi: [abiItem], + functionName: "previewWithdraw", + args: [amount], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} + +export async function fetchPreviewDeposit( + chainId: number, + vault: Address, + amount: bigint, +) { + const client = getViemClient(chainId) + + const abiItem = { + name: "previewDeposit", + inputs: [{ name: "assets", type: "uint256" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: vault, + abi: [abiItem], + functionName: "previewDeposit", + args: [amount], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} + +export async function fetchPreviewMint( + chainId: number, + vault: Address, + amount: bigint, +) { + const client = getViemClient(chainId) + + const abiItem = { + name: "previewMint", + inputs: [{ name: "shares", type: "uint256" }], + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + } + + const query = { + address: vault, + abi: [abiItem], + functionName: "previewMint", + args: [amount], + } as const + + const data = (await client.readContract(query)) as bigint + + return data +} + +const getViemClient = (chainId: number) => { + if (!viemClients[chainId]) + throw new Error(`No client found for chainId ${chainId}`) + return viemClients[chainId].extend(publicActions) +} From 3ed34422e81e422146ac4550e7f914f72c10c967 Mon Sep 17 00:00:00 2001 From: dglowinski Date: Fri, 24 Jan 2025 10:34:20 +0100 Subject: [PATCH 2/2] finish idle strategy --- src/swapService/config/mainnet.ts | 14 +- .../strategies/balmySDK/customSourceList.ts | 2 - .../balmySDK/idleAATrancheQuoteSource.ts | 201 ---- src/swapService/strategies/index.ts | 3 + src/swapService/strategies/strategyIdle.ts | 934 ------------------ .../strategies/strategyIdleCDOTranche.ts | 788 +++------------ 6 files changed, 123 insertions(+), 1819 deletions(-) delete mode 100644 src/swapService/strategies/balmySDK/idleAATrancheQuoteSource.ts delete mode 100644 src/swapService/strategies/strategyIdle.ts diff --git a/src/swapService/config/mainnet.ts b/src/swapService/config/mainnet.ts index 7f2643d..e48b976 100644 --- a/src/swapService/config/mainnet.ts +++ b/src/swapService/config/mainnet.ts @@ -3,6 +3,7 @@ import { StrategyBalmySDK, StrategyCombinedUniswap, StrategyERC4626Wrapper, + StrategyIdleCDOTranche, StrategyMidas, StrategyRepayWrapper, } from "../strategies" @@ -32,6 +33,10 @@ const mainnetRoutingConfig: ChainRoutingConfig = [ strategy: StrategyMidas.name(), match: {}, // supports function will match mTokens }, + { + strategy: StrategyIdleCDOTranche.name(), + match: { tokensInOrOut: [IDLEAATRANCHEFASANARA_MAINNET] }, + }, { strategy: StrategyERC4626Wrapper.name(), match: { @@ -39,15 +44,6 @@ const mainnetRoutingConfig: ChainRoutingConfig = [ excludeTokensInOrOut: [PT_WSTUSR_27MAR2025_MAINNET], }, }, - { - strategy: StrategyBalmySDK.name(), - config: { - sourcesFilter: { - includeSources: ["idle-tranche"], - }, - }, - match: { tokensInOrOut: [IDLEAATRANCHEFASANARA_MAINNET] }, - }, { strategy: StrategyBalmySDK.name(), config: { diff --git a/src/swapService/strategies/balmySDK/customSourceList.ts b/src/swapService/strategies/balmySDK/customSourceList.ts index 6e3d03d..8ffaed1 100644 --- a/src/swapService/strategies/balmySDK/customSourceList.ts +++ b/src/swapService/strategies/balmySDK/customSourceList.ts @@ -1,6 +1,5 @@ import type { IFetchService, IProviderService } from "@balmy/sdk" import { LocalSourceList } from "@balmy/sdk/dist/services/quotes/source-lists/local-source-list" -import { CustomIdleAATrancheQuoteSource } from "./idleAATrancheQuoteSource" import { CustomLiFiQuoteSource } from "./lifiQuoteSource" import { CustomNeptuneQuoteSource } from "./neptuneQuoteSource" import { CustomOdosQuoteSource } from "./odosQuoteSource" @@ -20,7 +19,6 @@ const customSources = { "open-ocean": new CustomOpenOceanQuoteSource(), neptune: new CustomNeptuneQuoteSource(), odos: new CustomOdosQuoteSource(), - "idle-tranche": new CustomIdleAATrancheQuoteSource(), } export class CustomSourceList extends LocalSourceList { constructor({ providerService, fetchService }: ConstructorParameters) { diff --git a/src/swapService/strategies/balmySDK/idleAATrancheQuoteSource.ts b/src/swapService/strategies/balmySDK/idleAATrancheQuoteSource.ts deleted file mode 100644 index a3f7082..0000000 --- a/src/swapService/strategies/balmySDK/idleAATrancheQuoteSource.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { Chains } from "@balmy/sdk" -import type { - BuildTxParams, - IQuoteSource, - QuoteParams, - QuoteSourceMetadata, - SourceQuoteResponse, - SourceQuoteTransaction, -} from "@balmy/sdk/dist/services/quotes/quote-sources/types" -import { - addQuoteSlippage, - calculateAllowanceTarget, -} from "@balmy/sdk/dist/services/quotes/quote-sources/utils" -import { - type Address, - type PublicClient, - encodeFunctionData, - isAddressEqual, -} from "viem" - -const assets: { - cdo: Address - aaTranche: Address - aaTrancheVault: Address - token: Address - tokenDecimals: bigint - swapHandler: Address - priceOne: bigint -}[] = [ - { - // IdleCDO AA Tranche - idle_Fasanara - cdo: "0xf6223C567F21E33e859ED7A045773526E9E3c2D5", - aaTranche: "0x45054c6753b4Bce40C5d54418DabC20b070F85bE", - aaTrancheVault: "0xd820C8129a853a04dC7e42C64aE62509f531eE5A", - token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - swapHandler: "0xA24689b6Ab48eCcF7038c70eBC39f9ed4217aFE3", - tokenDecimals: 6n, - priceOne: 1000000n, - }, -] - -// Supported networks: https://docs.1inch.io/docs/aggregation-protocol/introduction/#supported-networkschains -export const IDLEAATRANCHE_METADATA: QuoteSourceMetadata = - { - name: "IdleCDO", - supports: { - chains: [Chains.ETHEREUM.chainId], - swapAndTransfer: true, - buyOrders: false, - }, - logoURI: "", - } - -type IdleAATrancheSupport = { buyOrders: false; swapAndTransfer: true } -type IdleAATrancheConfig = object -type IdleAATrancheData = { tx: SourceQuoteTransaction } -export class CustomIdleAATrancheQuoteSource - implements - IQuoteSource -{ - getMetadata() { - return IDLEAATRANCHE_METADATA - } - - async quote( - params: QuoteParams, - ): Promise> { - const asset = assets.find( - (a) => - a.aaTranche === params.request.sellToken || - a.aaTranche === params.request.buyToken, - ) - if (!asset) throw new Error("Asset not found") - - const viemClient = params.components.providerService.getViemPublicClient({ - chainId: params.request.chainId, - }) as PublicClient - const virtualPrice = await fetchVirtualPrice( - viemClient, - asset.cdo, - asset.aaTranche, - ) - - const to = asset.swapHandler - let amountOut - let data - if (isAddressEqual(params.request.buyToken as Address, asset.aaTranche)) { - amountOut = - (params.request.order.sellAmount * - asset.priceOne * - 10n ** (18n - asset.tokenDecimals)) / - virtualPrice - data = encodeSwapExactTokensForAATranche(params.request.order.sellAmount) - } else { - amountOut = - (params.request.order.sellAmount * - virtualPrice * - 10n ** (18n - asset.tokenDecimals)) / - asset.priceOne - data = encodeSwapExactAATrancheForTokens(params.request.order.sellAmount) - } - - const quote = { - sellAmount: params.request.order.sellAmount, - buyAmount: BigInt(amountOut), - estimatedGas: undefined, - allowanceTarget: calculateAllowanceTarget(params.request.sellToken, to), - customData: { - tx: { - to, - calldata: data, - value: 0n, - }, - }, - } - - return addQuoteSlippage( - quote, - params.request.order.type, - params.request.config.slippagePercentage, - ) - } - - async buildTx({ - request, - }: BuildTxParams< - IdleAATrancheConfig, - IdleAATrancheData - >): Promise { - return request.customData.tx - } - - isConfigAndContextValidForQuoting( - config: Partial | undefined, - ): config is IdleAATrancheConfig { - return true - } - - isConfigAndContextValidForTxBuilding( - config: Partial | undefined, - ): config is IdleAATrancheConfig { - return true - } -} - -function encodeSwapExactTokensForAATranche(amount: bigint) { - const abiItem = { - inputs: [{ name: "amountIn", type: "uint256" }], - name: "swapExactTokensForAATranche", - stateMutability: "nonpayable", - type: "function", - } - - const functionData = encodeFunctionData({ - abi: [abiItem], - args: [amount], - }) - - return functionData -} - -function encodeSwapExactAATrancheForTokens(amount: bigint) { - const abiItem = { - inputs: [{ name: "amountIn", type: "uint256" }], - name: "swapExactAATrancheForTokens", - stateMutability: "nonpayable", - type: "function", - } - - const functionData = encodeFunctionData({ - abi: [abiItem], - args: [amount], - }) - - return functionData -} - -export async function fetchVirtualPrice( - client: PublicClient, - cdo: Address, - tranche: Address, -) { - const abiItem = { - name: "virtualPrice", - inputs: [{ name: "_tranche", type: "address" }], - outputs: [{ name: "_virtualPrice", type: "uint256" }], - stateMutability: "view", - type: "function", - } - - const query = { - address: cdo, - abi: [abiItem], - functionName: "virtualPrice", - args: [tranche], - } as const - - const data = (await client.readContract(query)) as bigint - - return data -} diff --git a/src/swapService/strategies/index.ts b/src/swapService/strategies/index.ts index 5255ba6..04a231d 100644 --- a/src/swapService/strategies/index.ts +++ b/src/swapService/strategies/index.ts @@ -1,6 +1,7 @@ import { StrategyBalmySDK } from "./strategyBalmySDK" import { StrategyCombinedUniswap } from "./strategyCombinedUniswap" import { StrategyERC4626Wrapper } from "./strategyERC4626Wrapper" +import { StrategyIdleCDOTranche } from "./strategyIdleCDOTranche" import { StrategyMidas } from "./strategyMidas" import { StrategyRepayWrapper } from "./strategyRepayWrapper" @@ -10,6 +11,7 @@ export { StrategyRepayWrapper, StrategyBalmySDK, StrategyERC4626Wrapper, + StrategyIdleCDOTranche, } export const strategies = { @@ -18,4 +20,5 @@ export const strategies = { [StrategyRepayWrapper.name()]: StrategyRepayWrapper, [StrategyBalmySDK.name()]: StrategyBalmySDK, [StrategyERC4626Wrapper.name()]: StrategyERC4626Wrapper, + [StrategyIdleCDOTranche.name()]: StrategyIdleCDOTranche, } diff --git a/src/swapService/strategies/strategyIdle.ts b/src/swapService/strategies/strategyIdle.ts deleted file mode 100644 index 68bddc1..0000000 --- a/src/swapService/strategies/strategyIdle.ts +++ /dev/null @@ -1,934 +0,0 @@ -import { viemClients } from "@/common/utils/viemClients" -import { - type Address, - encodeAbiParameters, - encodeFunctionData, - isAddressEqual, - parseAbiParameters, - publicActions, -} from "viem" -import { type SwapApiResponse, SwapperMode } from "../interface" -import { runPipeline } from "../runner" -import type { StrategyResult, SwapParams } from "../types" -import { - SWAPPER_HANDLER_GENERIC, - adjustForInterest, - applySlippage, - buildApiResponseSwap, - buildApiResponseVerifyDebtMax, - buildApiResponseVerifySkimMin, - encodeDepositMulticallItem, - encodeSwapMulticallItem, - findToken, - isExactInRepay, - matchParams, -} from "../utils" - -const defaultConfig: { - supportedVaults: Array<{ - chainId: number - vault: Address - asset: Address - assetDustEVault: Address - protocol: string - }> -} = { - supportedVaults: [ - { - chainId: 1, - protocol: "wstUSR", - vault: "0x1202f5c7b4b9e47a1a484e8b270be34dbbc75055", - asset: "0x66a1E37c9b0eAddca17d3662D6c05F4DECf3e110", - assetDustEVault: "0x3a8992754e2ef51d8f90620d2766278af5c59b90", - }, - { - chainId: 1, - protocol: "wUSDL", - vault: "0x7751E2F4b8ae93EF6B79d86419d42FE3295A4559", - asset: "0xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD", - assetDustEVault: "0x0Fc9cdb39317354a98a1Afa6497a969ff3a6BA9C", - }, - { - chainId: 1, - protocol: "ynETHX", - vault: "0x657d9aba1dbb59e53f9f3ecaa878447dcfc96dcb", - asset: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - assetDustEVault: "0xb3b36220fA7d12f7055dab5c9FD18E860e9a6bF8", - }, - // { - // chainId: 1, - // protocol: "sUSDS", - // vault: "0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD", - // asset: "0xdc035d45d973e3ec169d2276ddab16f1e407384f", - // assetDustEVault: "0x98238Ee86f2c571AD06B0913bef21793dA745F57", - // }, - ], -} - -// Wrapper which adds an ERC4626 deposit or withdraw in front or at the back of a trade -export class StrategyERC4626Wrapper { - static name() { - return "erc4626_wrapper" - } - readonly match - readonly config - - constructor(match = {}, config = defaultConfig) { - this.match = match - this.config = config - } - - async supports(swapParams: SwapParams) { - return ( - !isExactInRepay(swapParams) && - this.config.supportedVaults.some( - (v) => - v.chainId === swapParams.chainId && - (isAddressEqual(v.vault, swapParams.tokenIn.addressInfo) || - isAddressEqual(v.vault, swapParams.tokenOut.addressInfo)), - ) - ) - } - - async findSwap(swapParams: SwapParams): Promise { - const result: StrategyResult = { - strategy: StrategyERC4626Wrapper.name(), - supports: await this.supports(swapParams), - match: matchParams(swapParams, this.match), - } - - if (!result.supports || !result.match) return result - - try { - switch (swapParams.swapperMode) { - case SwapperMode.EXACT_IN: { - if (this.isSupportedVault(swapParams.tokenIn.addressInfo)) { - if ( - this.isSupportedVaultUnderlying({ - vault: swapParams.tokenIn.addressInfo, - underlying: swapParams.tokenOut.addressInfo, - }) - ) { - result.response = - await this.exactInFromVaultToUnderlying(swapParams) - } else { - result.response = await this.exactInFromVaultToAny(swapParams) - } - } else { - if ( - this.isSupportedVaultUnderlying({ - vault: swapParams.tokenOut.addressInfo, - underlying: swapParams.tokenIn.addressInfo, - }) - ) { - result.response = - await this.exactInFromUnderlyingToVault(swapParams) - } else { - result.response = await this.exactInFromAnyToVault(swapParams) - } - } - break - } - case SwapperMode.TARGET_DEBT: { - if (this.isSupportedVault(swapParams.tokenIn.addressInfo)) { - if ( - this.isSupportedVaultUnderlying({ - vault: swapParams.tokenIn.addressInfo, - underlying: swapParams.tokenOut.addressInfo, - }) - ) { - result.response = - await this.targetDebtFromVaultToUnderlying(swapParams) - } else { - result.response = await this.targetDebtFromVaultToAny(swapParams) //test - } - } else { - if ( - this.isSupportedVaultUnderlying({ - vault: swapParams.tokenOut.addressInfo, - underlying: swapParams.tokenIn.addressInfo, - }) - ) { - result.response = - await this.targetDebtFromUnderlyingToVault(swapParams) - } else { - result.response = await this.targetDebtFromAnyToVault(swapParams) - } - } - break - } - // case SwapperMode.EXACT_OUT: - default: { - result.error = "Unsupported swap mode" - } - } - } catch (error) { - result.error = error - } - - return result - } - - async exactInFromVaultToUnderlying( - swapParams: SwapParams, - ): Promise { - const { - swapMulticallItem: redeemMulticallItem, - amountOut: redeemAmountOut, - } = await encodeRedeem( - swapParams, - swapParams.tokenIn.addressInfo, - swapParams.amount, - swapParams.receiver, - ) - - const multicallItems = [redeemMulticallItem] - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifySkimMin( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - redeemAmountOut, - swapParams.deadline, - ) - - return { - amountIn: String(swapParams.amount), - amountInMax: String(swapParams.amount), - amountOut: String(redeemAmountOut), - amountOutMin: String(redeemAmountOut), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: 0, - route: [ - { - providerName: this.getSupportedVault(swapParams.tokenIn.addressInfo) - .protocol, - }, - ], - swap, - verify, - } - } - - async exactInFromVaultToAny( - swapParams: SwapParams, - ): Promise { - const { - swapMulticallItem: redeemMulticallItem, - amountOut: redeemAmountOut, - } = await encodeRedeem( - swapParams, - swapParams.tokenIn.addressInfo, - swapParams.amount, - swapParams.from, - ) - - const vaultData = this.getSupportedVault(swapParams.tokenIn.addressInfo) - - const innerSwapParams = { - ...swapParams, - tokenIn: findToken(swapParams.chainId, vaultData.asset), - amount: redeemAmountOut, - } - - const innerSwap = await runPipeline(innerSwapParams) - - const intermediateDustDepositMulticallItem = encodeDepositMulticallItem( - vaultData.asset, - vaultData.assetDustEVault, - 5n, // avoid zero shares - swapParams.accountOut, - ) - - const multicallItems = [ - redeemMulticallItem, - ...innerSwap.swap.multicallItems, - intermediateDustDepositMulticallItem, - ] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - const verify = innerSwap.verify - - return { - amountIn: String(swapParams.amount), - amountInMax: String(swapParams.amount), - amountOut: innerSwap.amountOut, - amountOutMin: innerSwap.amountOutMin, - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, - route: [{ providerName: vaultData.protocol }, ...innerSwap.route], - swap, - verify, - } - } - - async exactInFromUnderlyingToVault( - swapParams: SwapParams, - ): Promise { - const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) - - const amountOut = await fetchPreviewDeposit( - swapParams.chainId, - vaultData.vault, - swapParams.amount, - ) - const swapperDepositMulticallItem = encodeDepositMulticallItem( - vaultData.asset, - vaultData.vault, - 0n, - swapParams.receiver, - ) - - const multicallItems = [swapperDepositMulticallItem] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const amountOutMin = applySlippage(amountOut, swapParams.slippage) // vault (tokenOut) can have growing exchange rate - const verify = buildApiResponseVerifySkimMin( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - amountOutMin, - swapParams.deadline, - ) - - return { - amountIn: String(swapParams.amount), - amountInMax: String(swapParams.amount), - amountOut: String(amountOut), - amountOutMin: String(amountOutMin), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, - route: [{ providerName: vaultData.protocol }], - swap, - verify, - } - } - - async exactInFromAnyToVault( - swapParams: SwapParams, - ): Promise { - const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) - - const innerSwapParams = { - ...swapParams, - tokenOut: findToken(swapParams.chainId, vaultData.asset), - receiver: swapParams.from, - } - - const innerSwap = await runPipeline(innerSwapParams) - const amountOut = await fetchPreviewDeposit( - swapParams.chainId, - vaultData.vault, - BigInt(innerSwap.amountOut), - ) - const amountOutMin = await fetchPreviewDeposit( - swapParams.chainId, - vaultData.vault, - BigInt(innerSwap.amountOutMin), - ) - - // Swapper.deposit will deposit all of available balance into the wrapper, and move the wrapper straight to receiver, where it can be skimmed - const swapperDepositMulticallItem = encodeDepositMulticallItem( - vaultData.asset, - vaultData.vault, - 0n, - swapParams.receiver, - ) - - const multicallItems = [ - ...innerSwap.swap.multicallItems, - swapperDepositMulticallItem, - ] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - const verify = buildApiResponseVerifySkimMin( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - amountOutMin, - swapParams.deadline, - ) - - return { - amountIn: String(swapParams.amount), - amountInMax: String(swapParams.amount), - amountOut: String(amountOut), - amountOutMin: String(amountOutMin), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, - route: [{ providerName: vaultData.protocol }, ...innerSwap.route], - swap, - verify, - } - } - - async targetDebtFromVaultToUnderlying( - swapParams: SwapParams, - ): Promise { - // TODO expects dust - add to dust list - const vaultData = this.getSupportedVault(swapParams.tokenIn.addressInfo) - const withdrawAmount = adjustForInterest(swapParams.amount) - - const { - swapMulticallItem: withdrawMulticallItem, - amountIn, - amountOut, - } = await encodeWithdraw( - swapParams, - vaultData.vault, - withdrawAmount, - swapParams.from, - ) - - const multicallItems = [withdrawMulticallItem] - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifyDebtMax( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - swapParams.targetDebt, - swapParams.deadline, - ) - - return { - amountIn: String(amountIn), // adjusted for accruing debt - amountInMax: String(amountIn), - amountOut: String(amountOut), - amountOutMin: String(amountOut), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: 0, - route: [{ providerName: vaultData.protocol }], - swap, - verify, - } - } - - async targetDebtFromVaultToAny( - swapParams: SwapParams, - ): Promise { - // TODO expects dust out - add to dust list - const vaultData = this.getSupportedVault(swapParams.tokenIn.addressInfo) - const innerSwapParams = { - ...swapParams, - tokenIn: findToken(swapParams.chainId, vaultData.asset), - vaultIn: vaultData.assetDustEVault, - onlyFixedInputExactOut: true, // eliminate dust in the intermediate asset (vault underlying) - } - - const innerQuote = await runPipeline(innerSwapParams) - - const withdrawSwapParams = { - ...swapParams, - swapperMode: SwapperMode.EXACT_IN, // change to exact in, otherwise multicall item will be target debt and will attempt a repay - } - const { - swapMulticallItem: withdrawMulticallItem, - amountIn: withdrawAmountIn, - } = await encodeWithdraw( - withdrawSwapParams, - vaultData.vault, - BigInt(innerQuote.amountIn), - swapParams.from, - ) - - // repay or exact out will return unused input, which is the intermediate asset - const multicallItems = [ - withdrawMulticallItem, - ...innerQuote.swap.multicallItems, - ] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifyDebtMax( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - swapParams.targetDebt, - swapParams.deadline, - ) - - return { - amountIn: String(withdrawAmountIn), - amountInMax: String(withdrawAmountIn), - amountOut: String(innerQuote.amountOut), - amountOutMin: String(innerQuote.amountOutMin), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, - route: [{ providerName: vaultData.protocol }, ...innerQuote.route], - swap, - verify, - } - } - - async targetDebtFromUnderlyingToVault( - swapParams: SwapParams, - ): Promise { - const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) - - const mintAmount = adjustForInterest(swapParams.amount) - - const { - swapMulticallItem: mintMulticallItem, - amountIn, - amountOut, - } = await encodeMint( - swapParams, - vaultData.vault, - mintAmount, - swapParams.from, - ) - - // mint is encoded in target debt mode, so repay will happen automatically - const multicallItems = [mintMulticallItem] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifyDebtMax( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - swapParams.targetDebt, - swapParams.deadline, - ) - - return { - amountIn: String(amountIn), - amountInMax: String(adjustForInterest(amountIn)), // compensate for intrinsic interest accrued in the vault (tokenIn) - amountOut: String(amountOut), - amountOutMin: String(amountOut), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: 0, - route: [{ providerName: vaultData.protocol }], - swap, - verify, - } - } - - async targetDebtFromAnyToVault( - swapParams: SwapParams, - ): Promise { - const vaultData = this.getSupportedVault(swapParams.tokenOut.addressInfo) - - const mintAmount = adjustForInterest(swapParams.amount) - const mintSwapParams = { - ...swapParams, - tokenIn: findToken(swapParams.chainId, vaultData.asset), - vaultIn: vaultData.assetDustEVault, - } - - const { - swapMulticallItem: mintMulticallItem, - amountIn: mintAmountIn, - amountOut, - } = await encodeMint( - mintSwapParams, - vaultData.vault, - mintAmount, - swapParams.from, - ) - - const innerSwapParams = { - ...swapParams, - amount: mintAmountIn, - tokenOut: findToken(swapParams.chainId, vaultData.asset), - receiver: swapParams.from, - onlyFixedInputExactOut: true, // this option will overswap, which should cover growing exchange rate - } - - const innerQuote = await runPipeline(innerSwapParams) - - // re-encode inner swap from target debt to exact out so that repay is not executed before mint TODO fix with exact out support in all strategies - const innerSwapItems = innerQuote.swap.multicallItems.map((item) => { - if (item.functionName !== "swap") return item - - const newItem = encodeSwapMulticallItem({ - ...item.args[0], - mode: BigInt(SwapperMode.EXACT_OUT), - }) - - return newItem - }) - - // repay is done through mint item, which will return unused input, which is the intermediate asset - const multicallItems = [...innerSwapItems, mintMulticallItem] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifyDebtMax( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - swapParams.targetDebt, - swapParams.deadline, - ) - - return { - amountIn: String(innerQuote.amountIn), - amountInMax: String(innerQuote.amountInMax), - amountOut: String(amountOut), - amountOutMin: String(amountOut), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, - route: [...innerQuote.route, { providerName: vaultData.protocol }], - swap, - verify, - } - } - - isSupportedVault(vault: Address) { - return this.config.supportedVaults.some((v) => - isAddressEqual(v.vault, vault), - ) - } - - isSupportedVaultUnderlying({ - vault, - underlying, - }: { vault: Address; underlying: Address }) { - const asset = this.config.supportedVaults.find((v) => - isAddressEqual(v.vault, vault), - )?.asset - return !!asset && isAddressEqual(asset, underlying) - } - - getSupportedVault(vault: Address) { - const supportedVault = this.config.supportedVaults.find((v) => - isAddressEqual(v.vault, vault), - ) - if (!supportedVault) throw new Error("Vault not supported") - - return supportedVault - } -} - -export async function encodeRedeem( - swapParams: SwapParams, - vault: Address, - amountIn: bigint, - receiver: Address, -) { - const amountOut = await fetchPreviewRedeem( - swapParams.chainId, - vault, - amountIn, - ) - - const abiItem = { - inputs: [ - { name: "amount", type: "uint256" }, - { name: "receiver", type: "address" }, - { name: "owner", type: "address" }, - ], - name: "redeem", - stateMutability: "nonpayable", - type: "function", - } - - const redeemData = encodeFunctionData({ - abi: [abiItem], - args: [amountIn, receiver, swapParams.from], - }) - - const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ - vault, - redeemData, - ]) - - const swapperAmountOut = - swapParams.swapperMode === SwapperMode.EXACT_IN - ? 0n //ignored - : swapParams.swapperMode === SwapperMode.EXACT_OUT - ? amountOut - : swapParams.targetDebt - - const swapMulticallItem = encodeSwapMulticallItem({ - handler: SWAPPER_HANDLER_GENERIC, - mode: BigInt(swapParams.swapperMode), - account: swapParams.accountOut, - tokenIn: swapParams.tokenIn.addressInfo, - tokenOut: swapParams.tokenOut.addressInfo, - vaultIn: swapParams.vaultIn, - accountIn: swapParams.accountIn, - receiver: swapParams.receiver, - amountOut: swapperAmountOut, - data: swapData, - }) - - return { - amountIn, - amountOut, - swapMulticallItem, - } -} - -export async function encodeWithdraw( - swapParams: SwapParams, - vault: Address, - amountOut: bigint, - receiver: Address, -) { - const amountIn = await fetchPreviewWithdraw( - swapParams.chainId, - vault, - amountOut, - ) - - const abiItem = { - inputs: [ - { name: "amount", type: "uint256" }, - { name: "receiver", type: "address" }, - { name: "owner", type: "address" }, - ], - name: "withdraw", - stateMutability: "nonpayable", - type: "function", - } - - const withdrawData = encodeFunctionData({ - abi: [abiItem], - args: [amountOut, receiver, swapParams.from], - }) - - const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ - vault, - withdrawData, - ]) - - const swapperAmountOut = - swapParams.swapperMode === SwapperMode.EXACT_IN - ? 0n //ignored - : swapParams.swapperMode === SwapperMode.EXACT_OUT - ? amountOut - : swapParams.targetDebt - - const swapMulticallItem = encodeSwapMulticallItem({ - handler: SWAPPER_HANDLER_GENERIC, - mode: BigInt(swapParams.swapperMode), - account: swapParams.accountOut, - tokenIn: swapParams.tokenIn.addressInfo, - tokenOut: swapParams.tokenOut.addressInfo, - vaultIn: swapParams.vaultIn, - accountIn: swapParams.accountIn, - receiver: swapParams.receiver, - amountOut: swapperAmountOut, - data: swapData, - }) - - return { - amountIn, - amountOut, - swapMulticallItem, - } -} - -export async function encodeMint( - swapParams: SwapParams, - vault: Address, - amountOut: bigint, - receiver: Address, -) { - const amountIn = await fetchPreviewMint(swapParams.chainId, vault, amountOut) - - const abiItem = { - inputs: [ - { name: "amount", type: "uint256" }, - { name: "receiver", type: "address" }, - ], - name: "mint", - stateMutability: "nonpayable", - type: "function", - } - - const mintData = encodeFunctionData({ - abi: [abiItem], - args: [amountOut, receiver], - }) - - const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ - vault, - mintData, - ]) - - const swapperAmountOut = - swapParams.swapperMode === SwapperMode.EXACT_IN - ? 0n //ignored - : swapParams.swapperMode === SwapperMode.EXACT_OUT - ? amountOut - : swapParams.targetDebt - - const swapMulticallItem = encodeSwapMulticallItem({ - handler: SWAPPER_HANDLER_GENERIC, - mode: BigInt(swapParams.swapperMode), - account: swapParams.accountOut, - tokenIn: swapParams.tokenIn.addressInfo, - tokenOut: swapParams.tokenOut.addressInfo, - vaultIn: swapParams.vaultIn, - accountIn: swapParams.accountIn, - receiver: swapParams.receiver, - amountOut: swapperAmountOut, - data: swapData, - }) - - return { - amountIn, - amountOut, - swapMulticallItem, - } -} - -export async function fetchPreviewRedeem( - chainId: number, - vault: Address, - amount: bigint, -) { - const client = getViemClient(chainId) - - const abiItem = { - name: "previewRedeem", - inputs: [{ name: "shares", type: "uint256" }], - outputs: [{ name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - } - - const query = { - address: vault, - abi: [abiItem], - functionName: "previewRedeem", - args: [amount], - } as const - - const data = (await client.readContract(query)) as bigint - - return data -} - -export async function fetchPreviewWithdraw( - chainId: number, - vault: Address, - amount: bigint, -) { - const client = getViemClient(chainId) - - const abiItem = { - name: "previewWithdraw", - inputs: [{ name: "assets", type: "uint256" }], - outputs: [{ name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - } - - const query = { - address: vault, - abi: [abiItem], - functionName: "previewWithdraw", - args: [amount], - } as const - - const data = (await client.readContract(query)) as bigint - - return data -} - -export async function fetchPreviewDeposit( - chainId: number, - vault: Address, - amount: bigint, -) { - const client = getViemClient(chainId) - - const abiItem = { - name: "previewDeposit", - inputs: [{ name: "assets", type: "uint256" }], - outputs: [{ name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - } - - const query = { - address: vault, - abi: [abiItem], - functionName: "previewDeposit", - args: [amount], - } as const - - const data = (await client.readContract(query)) as bigint - - return data -} - -export async function fetchPreviewMint( - chainId: number, - vault: Address, - amount: bigint, -) { - const client = getViemClient(chainId) - - const abiItem = { - name: "previewMint", - inputs: [{ name: "shares", type: "uint256" }], - outputs: [{ name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - } - - const query = { - address: vault, - abi: [abiItem], - functionName: "previewMint", - args: [amount], - } as const - - const data = (await client.readContract(query)) as bigint - - return data -} - -const getViemClient = (chainId: number) => { - if (!viemClients[chainId]) - throw new Error(`No client found for chainId ${chainId}`) - return viemClients[chainId].extend(publicActions) -} diff --git a/src/swapService/strategies/strategyIdleCDOTranche.ts b/src/swapService/strategies/strategyIdleCDOTranche.ts index aed7a41..f6fd466 100644 --- a/src/swapService/strategies/strategyIdleCDOTranche.ts +++ b/src/swapService/strategies/strategyIdleCDOTranche.ts @@ -4,6 +4,7 @@ import { encodeAbiParameters, encodeFunctionData, isAddressEqual, + maxUint256, parseAbiParameters, publicActions, } from "viem" @@ -12,12 +13,9 @@ import { runPipeline } from "../runner" import type { StrategyResult, SwapParams } from "../types" import { SWAPPER_HANDLER_GENERIC, - adjustForInterest, applySlippage, buildApiResponseSwap, - buildApiResponseVerifyDebtMax, buildApiResponseVerifySkimMin, - encodeDepositMulticallItem, encodeSwapMulticallItem, findToken, isExactInRepay, @@ -32,7 +30,6 @@ const defaultConfig: { aaTranche: Address aaTrancheVault: Address underlying: Address - underlyingDustVault: Address underlyingDecimals: bigint priceOne: bigint }> @@ -46,17 +43,16 @@ const defaultConfig: { aaTranche: "0x45054c6753b4Bce40C5d54418DabC20b070F85bE", aaTrancheVault: "0xd820C8129a853a04dC7e42C64aE62509f531eE5A", underlying: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - underlyingDustVault: "0xb93d4928f39fbcd6c89a7dfbf0a867e6344561be", // eUSDC-1 escrow underlyingDecimals: 6n, priceOne: 1000000n, }, ], } -const PROTOCOL = { providerName: "IdleCDO" } +const PROTOCOL = { providerName: "Idle" } -// Wrapper which adds an ERC4626 deposit or withdraw in front or at the back of a trade -export class StrategyERC4626Wrapper { +// Strategy uses a special SwapHandler contract, which deposits into IdleCDO tranches +export class StrategyIdleCDOTranche { static name() { return "erc4626_wrapper" } @@ -73,16 +69,18 @@ export class StrategyERC4626Wrapper { !isExactInRepay(swapParams) && this.config.supportedTranches.some( (v) => + swapParams.swapperMode === SwapperMode.EXACT_IN && v.chainId === swapParams.chainId && - (isAddressEqual(v.aaTranche, swapParams.tokenIn.addressInfo) || - isAddressEqual(v.aaTranche, swapParams.tokenOut.addressInfo)), + // only deposits into the tranche are possible atomically + isAddressEqual(v.aaTranche, swapParams.tokenOut.addressInfo) && + swapParams.receiver === v.aaTrancheVault, ) ) } async findSwap(swapParams: SwapParams): Promise { const result: StrategyResult = { - strategy: StrategyERC4626Wrapper.name(), + strategy: StrategyIdleCDOTranche.name(), supports: await this.supports(swapParams), match: matchParams(swapParams, this.match), } @@ -93,57 +91,18 @@ export class StrategyERC4626Wrapper { switch (swapParams.swapperMode) { case SwapperMode.EXACT_IN: { if (this.isSupportedTranche(swapParams.tokenIn.addressInfo)) { - if ( - this.isSupportedTrancheUnderlying({ - aaTranche: swapParams.tokenIn.addressInfo, - underlying: swapParams.tokenOut.addressInfo, - }) - ) { - result.response = - await this.exactInFromAssetToUnderlying(swapParams) - } else { - result.response = await this.exactInFromAssetToAny(swapParams) - } - } else { - if ( - this.isSupportedTrancheUnderlying({ - aaTranche: swapParams.tokenOut.addressInfo, - underlying: swapParams.tokenIn.addressInfo, - }) - ) { - result.response = - await this.exactInFromUnderlyingToAsset(swapParams) - } else { - result.response = await this.exactInFromAnyToAsset(swapParams) - } + throw new Error("Not supported") } - break - } - case SwapperMode.TARGET_DEBT: { - if (this.isSupportedTranche(swapParams.tokenIn.addressInfo)) { - if ( - this.isSupportedTrancheUnderlying({ - aaTranche: swapParams.tokenIn.addressInfo, - underlying: swapParams.tokenOut.addressInfo, - }) - ) { - result.response = - await this.targetDebtFromVaultToUnderlying(swapParams) - } else { - result.response = await this.targetDebtFromVaultToAny(swapParams) //test - } + if ( + this.isSupportedTrancheUnderlying({ + aaTranche: swapParams.tokenOut.addressInfo, + underlying: swapParams.tokenIn.addressInfo, + }) + ) { + result.response = + await this.exactInFromUnderlyingToTranche(swapParams) } else { - if ( - this.isSupportedTrancheUnderlying({ - aaTranche: swapParams.tokenOut.addressInfo, - underlying: swapParams.tokenIn.addressInfo, - }) - ) { - result.response = - await this.targetDebtFromUnderlyingToVault(swapParams) - } else { - result.response = await this.targetDebtFromAnyToVault(swapParams) - } + result.response = await this.exactInFromAnyToTranche(swapParams) } break } @@ -159,130 +118,30 @@ export class StrategyERC4626Wrapper { return result } - async exactInFromAssetToUnderlying( - swapParams: SwapParams, - ): Promise { - const { - swapMulticallItem: redeemMulticallItem, - amountOut: redeemAmountOut, - } = await encodeRedeem( - swapParams, - swapParams.tokenIn.addressInfo, - swapParams.amount, - swapParams.receiver, - ) - - const multicallItems = [redeemMulticallItem] - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifySkimMin( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - redeemAmountOut, - swapParams.deadline, - ) - - return { - amountIn: String(swapParams.amount), - amountInMax: String(swapParams.amount), - amountOut: String(redeemAmountOut), - amountOutMin: String(redeemAmountOut), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: 0, - route: [PROTOCOL], - swap, - verify, - } - } - - async exactInFromAssetToAny( - swapParams: SwapParams, - ): Promise { - const { - swapMulticallItem: redeemMulticallItem, - amountOut: redeemAmountOut, - } = await encodeRedeem( - swapParams, - swapParams.tokenIn.addressInfo, - swapParams.amount, - swapParams.from, - ) - - const trancheData = this.getSupportedTranche(swapParams.tokenIn.addressInfo) - const tokenIn = findToken(swapParams.chainId, trancheData.underlying) - if (!tokenIn) throw new Error("Inner token not found") - const innerSwapParams = { - ...swapParams, - tokenIn, - amount: redeemAmountOut, - } - - const innerSwap = await runPipeline(innerSwapParams) - - const intermediateDustDepositMulticallItem = encodeDepositMulticallItem( - trancheData.underlying, - trancheData.underlyingDustVault, - 5n, // avoid zero shares - swapParams.accountOut, - ) - - const multicallItems = [ - redeemMulticallItem, - ...innerSwap.swap.multicallItems, - intermediateDustDepositMulticallItem, - ] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - const verify = innerSwap.verify - - return { - amountIn: String(swapParams.amount), - amountInMax: String(swapParams.amount), - amountOut: innerSwap.amountOut, - amountOutMin: innerSwap.amountOutMin, - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, - route: [PROTOCOL, ...innerSwap.route], - swap, - verify, - } - } - - async exactInFromUnderlyingToAsset( + async exactInFromUnderlyingToTranche( swapParams: SwapParams, ): Promise { const trancheData = this.getSupportedTranche( swapParams.tokenOut.addressInfo, ) - const amountOut = await fetchPreviewDeposit( + const amountOut = await this.getDepositAmountOut( swapParams.chainId, trancheData.aaTranche, swapParams.amount, ) - const swapperDepositMulticallItem = encodeDepositMulticallItem( - trancheData.underlying, + + const swapHandlerMulticallItem = this.encodeSwapToTrancheSwapMulticallItem( trancheData.aaTranche, - 0n, - swapParams.receiver, + swapParams, + swapParams.amount, ) - const multicallItems = [swapperDepositMulticallItem] + const multicallItems = [swapHandlerMulticallItem] const swap = buildApiResponseSwap(swapParams.from, multicallItems) - const amountOutMin = applySlippage(amountOut, swapParams.slippage) // vault (tokenOut) can have growing exchange rate + const amountOutMin = amountOut // tranche price should not decrease under normal circumstances const verify = buildApiResponseVerifySkimMin( swapParams.chainId, swapParams.receiver, @@ -302,14 +161,14 @@ export class StrategyERC4626Wrapper { accountOut: swapParams.accountOut, tokenIn: swapParams.tokenIn, tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, + slippage: 0, route: [PROTOCOL], swap, verify, } } - async exactInFromAnyToAsset( + async exactInFromAnyToTranche( swapParams: SwapParams, ): Promise { const trancheData = this.getSupportedTranche( @@ -322,32 +181,26 @@ export class StrategyERC4626Wrapper { tokenOut, receiver: swapParams.from, } - const innerSwap = await runPipeline(innerSwapParams) - const amountOut = await fetchPreviewDeposit( + + const amountOut = await this.getDepositAmountOut( swapParams.chainId, trancheData.aaTranche, BigInt(innerSwap.amountOut), ) - const amountOutMin = await fetchPreviewDeposit( - swapParams.chainId, - trancheData.aaTranche, - BigInt(innerSwap.amountOutMin), - ) + const amountOutMin = applySlippage(amountOut, swapParams.slippage) - // Swapper.deposit will deposit all of available balance into the wrapper, and move the wrapper straight to receiver, where it can be skimmed - const swapperDepositMulticallItem = encodeDepositMulticallItem( - trancheData.underlying, - trancheData.aaTranche, - 0n, - swapParams.receiver, - ) + const swapHandlerMulticallItem = + await this.encodeSwapToTrancheSwapMulticallItem( + trancheData.aaTranche, + swapParams, + maxUint256, // this will deposit everything that was bought in the inner swapllol + ) const multicallItems = [ ...innerSwap.swap.multicallItems, - swapperDepositMulticallItem, + swapHandlerMulticallItem, ] - const swap = buildApiResponseSwap(swapParams.from, multicallItems) const verify = buildApiResponseVerifySkimMin( swapParams.chainId, @@ -356,7 +209,6 @@ export class StrategyERC4626Wrapper { amountOutMin, swapParams.deadline, ) - return { amountIn: String(swapParams.amount), amountInMax: String(swapParams.amount), @@ -375,248 +227,79 @@ export class StrategyERC4626Wrapper { } } - async targetDebtFromVaultToUnderlying( + encodeSwapToTrancheSwapMulticallItem( + tranche: Address, swapParams: SwapParams, - ): Promise { - // TODO expects dust - add to dust list - const trancheData = this.getSupportedTranche(swapParams.tokenIn.addressInfo) - const withdrawAmount = adjustForInterest(swapParams.amount) - - const { - swapMulticallItem: withdrawMulticallItem, - amountIn, - amountOut, - } = await encodeWithdraw( - swapParams, - trancheData.aaTranche, - withdrawAmount, - swapParams.from, - ) - - const multicallItems = [withdrawMulticallItem] - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifyDebtMax( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - swapParams.targetDebt, - swapParams.deadline, - ) + amountIn: bigint, + ) { + const trancheData = this.getSupportedTranche(tranche) + + const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ + trancheData.swapHandler, + encodeSwapExactTokensForAATranche(amountIn), + ]) + + const swapperAmountOut = + swapParams.swapperMode === SwapperMode.EXACT_IN + ? 0n //ignored + : swapParams.targetDebt - return { - amountIn: String(amountIn), // adjusted for accruing debt - amountInMax: String(amountIn), - amountOut: String(amountOut), - amountOutMin: String(amountOut), + const swapHandlerMulticallItem = encodeSwapMulticallItem({ + handler: SWAPPER_HANDLER_GENERIC, + mode: BigInt(swapParams.swapperMode), + account: swapParams.accountOut, + tokenIn: swapParams.tokenIn.addressInfo, + tokenOut: tranche, vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: 0, - route: [PROTOCOL], - swap, - verify, - } - } - - async targetDebtFromVaultToAny( - swapParams: SwapParams, - ): Promise { - // TODO expects dust out - add to dust list - const trancheData = this.getSupportedTranche(swapParams.tokenIn.addressInfo) - const tokenIn = findToken(swapParams.chainId, trancheData.underlying) - if (!tokenIn) throw new Error("Inner token not found") - const innerSwapParams = { - ...swapParams, - tokenIn, - vaultIn: trancheData.underlyingDustVault, - onlyFixedInputExactOut: true, // eliminate dust in the intermediate asset (vault underlying) - } - - const innerQuote = await runPipeline(innerSwapParams) - - const withdrawSwapParams = { - ...swapParams, - swapperMode: SwapperMode.EXACT_IN, // change to exact in, otherwise multicall item will be target debt and will attempt a repay - } - const { - swapMulticallItem: withdrawMulticallItem, - amountIn: withdrawAmountIn, - } = await encodeWithdraw( - withdrawSwapParams, - trancheData.aaTranche, - BigInt(innerQuote.amountIn), - swapParams.from, - ) - - // repay or exact out will return unused input, which is the intermediate asset - const multicallItems = [ - withdrawMulticallItem, - ...innerQuote.swap.multicallItems, - ] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifyDebtMax( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - swapParams.targetDebt, - swapParams.deadline, - ) - - return { - amountIn: String(withdrawAmountIn), - amountInMax: String(withdrawAmountIn), - amountOut: String(innerQuote.amountOut), - amountOutMin: String(innerQuote.amountOutMin), - vaultIn: swapParams.vaultIn, receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, - route: [PROTOCOL, ...innerQuote.route], - swap, - verify, - } - } + amountOut: swapperAmountOut, + data: swapData, + }) - async targetDebtFromUnderlyingToVault( - swapParams: SwapParams, - ): Promise { - const trancheData = this.getSupportedTranche( - swapParams.tokenOut.addressInfo, - ) + return swapHandlerMulticallItem + } - const mintAmount = adjustForInterest(swapParams.amount) + async getDepositAmountOut( + chainId: number, + tranche: Address, + amountIn: bigint, + ) { + const trancheData = this.getSupportedTranche(tranche) - const { - swapMulticallItem: mintMulticallItem, - amountIn, - amountOut, - } = await encodeMint( - swapParams, + const virtualPrice = await fetchVirtualPrice( + chainId, + trancheData.cdo, trancheData.aaTranche, - mintAmount, - swapParams.from, ) + const amountOut = + (amountIn * + trancheData.priceOne * + 10n ** (18n - trancheData.underlyingDecimals)) / + virtualPrice - // mint is encoded in target debt mode, so repay will happen automatically - const multicallItems = [mintMulticallItem] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifyDebtMax( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - swapParams.targetDebt, - swapParams.deadline, - ) - - return { - amountIn: String(amountIn), - amountInMax: String(adjustForInterest(amountIn)), // compensate for intrinsic interest accrued in the vault (tokenIn) - amountOut: String(amountOut), - amountOutMin: String(amountOut), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: 0, - route: [PROTOCOL], - swap, - verify, - } + return amountOut } - async targetDebtFromAnyToVault( - swapParams: SwapParams, - ): Promise { - const trancheData = this.getSupportedTranche( - swapParams.tokenOut.addressInfo, - ) - - const mintAmount = adjustForInterest(swapParams.amount) - const tokenIn = findToken(swapParams.chainId, trancheData.underlying) - if (!tokenIn) throw new Error("Inner token in not found") - const mintSwapParams = { - ...swapParams, - tokenIn, - vaultIn: trancheData.underlyingDustVault, - } + async getWithdrawAmountOut( + chainId: number, + tranche: Address, + amountIn: bigint, + ) { + const trancheData = this.getSupportedTranche(tranche) - const { - swapMulticallItem: mintMulticallItem, - amountIn: mintAmountIn, - amountOut, - } = await encodeMint( - mintSwapParams, + const virtualPrice = await fetchVirtualPrice( + chainId, + trancheData.cdo, trancheData.aaTranche, - mintAmount, - swapParams.from, - ) - - const tokenOut = findToken(swapParams.chainId, trancheData.underlying) - if (!tokenOut) throw new Error("Inner token not found") - const innerSwapParams = { - ...swapParams, - amount: mintAmountIn, - tokenOut, - receiver: swapParams.from, - onlyFixedInputExactOut: true, // this option will overswap, which should cover growing exchange rate - } - - const innerQuote = await runPipeline(innerSwapParams) - - // re-encode inner swap from target debt to exact out so that repay is not executed before mint TODO fix with exact out support in all strategies - const innerSwapItems = innerQuote.swap.multicallItems.map((item) => { - if (item.functionName !== "swap") return item - - const newItem = encodeSwapMulticallItem({ - ...item.args[0], - mode: BigInt(SwapperMode.EXACT_OUT), - }) - - return newItem - }) - - // repay is done through mint item, which will return unused input, which is the intermediate asset - const multicallItems = [...innerSwapItems, mintMulticallItem] - - const swap = buildApiResponseSwap(swapParams.from, multicallItems) - - const verify = buildApiResponseVerifyDebtMax( - swapParams.chainId, - swapParams.receiver, - swapParams.accountOut, - swapParams.targetDebt, - swapParams.deadline, ) + const amountOut = + (amountIn * + virtualPrice * + 10n ** (18n - trancheData.underlyingDecimals)) / + trancheData.priceOne - return { - amountIn: String(innerQuote.amountIn), - amountInMax: String(innerQuote.amountInMax), - amountOut: String(amountOut), - amountOutMin: String(amountOut), - vaultIn: swapParams.vaultIn, - receiver: swapParams.receiver, - accountIn: swapParams.accountIn, - accountOut: swapParams.accountOut, - tokenIn: swapParams.tokenIn, - tokenOut: swapParams.tokenOut, - slippage: swapParams.slippage, - route: [...innerQuote.route, PROTOCOL], - swap, - verify, - } + return amountOut } isSupportedTranche(asset: Address) { @@ -645,228 +328,25 @@ export class StrategyERC4626Wrapper { } } -export async function encodeRedeem( - swapParams: SwapParams, - vault: Address, - amountIn: bigint, - receiver: Address, -) { - const amountOut = await fetchPreviewRedeem( - swapParams.chainId, - vault, - amountIn, - ) - - const abiItem = { - inputs: [ - { name: "amount", type: "uint256" }, - { name: "receiver", type: "address" }, - { name: "owner", type: "address" }, - ], - name: "redeem", - stateMutability: "nonpayable", - type: "function", - } - - const redeemData = encodeFunctionData({ - abi: [abiItem], - args: [amountIn, receiver, swapParams.from], - }) - - const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ - vault, - redeemData, - ]) - - const swapperAmountOut = - swapParams.swapperMode === SwapperMode.EXACT_IN - ? 0n //ignored - : swapParams.swapperMode === SwapperMode.EXACT_OUT - ? amountOut - : swapParams.targetDebt - - const swapMulticallItem = encodeSwapMulticallItem({ - handler: SWAPPER_HANDLER_GENERIC, - mode: BigInt(swapParams.swapperMode), - account: swapParams.accountOut, - tokenIn: swapParams.tokenIn.addressInfo, - tokenOut: swapParams.tokenOut.addressInfo, - vaultIn: swapParams.vaultIn, - accountIn: swapParams.accountIn, - receiver: swapParams.receiver, - amountOut: swapperAmountOut, - data: swapData, - }) - - return { - amountIn, - amountOut, - swapMulticallItem, - } -} - -export async function encodeWithdraw( - swapParams: SwapParams, - vault: Address, - amountOut: bigint, - receiver: Address, -) { - const amountIn = await fetchPreviewWithdraw( - swapParams.chainId, - vault, - amountOut, - ) - - const abiItem = { - inputs: [ - { name: "amount", type: "uint256" }, - { name: "receiver", type: "address" }, - { name: "owner", type: "address" }, - ], - name: "withdraw", - stateMutability: "nonpayable", - type: "function", - } - - const withdrawData = encodeFunctionData({ - abi: [abiItem], - args: [amountOut, receiver, swapParams.from], - }) - - const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ - vault, - withdrawData, - ]) - - const swapperAmountOut = - swapParams.swapperMode === SwapperMode.EXACT_IN - ? 0n //ignored - : swapParams.swapperMode === SwapperMode.EXACT_OUT - ? amountOut - : swapParams.targetDebt - - const swapMulticallItem = encodeSwapMulticallItem({ - handler: SWAPPER_HANDLER_GENERIC, - mode: BigInt(swapParams.swapperMode), - account: swapParams.accountOut, - tokenIn: swapParams.tokenIn.addressInfo, - tokenOut: swapParams.tokenOut.addressInfo, - vaultIn: swapParams.vaultIn, - accountIn: swapParams.accountIn, - receiver: swapParams.receiver, - amountOut: swapperAmountOut, - data: swapData, - }) - - return { - amountIn, - amountOut, - swapMulticallItem, - } -} - -export async function encodeMint( - swapParams: SwapParams, - vault: Address, - amountOut: bigint, - receiver: Address, -) { - const amountIn = await fetchPreviewMint(swapParams.chainId, vault, amountOut) - - const abiItem = { - inputs: [ - { name: "amount", type: "uint256" }, - { name: "receiver", type: "address" }, - ], - name: "mint", - stateMutability: "nonpayable", - type: "function", - } - - const mintData = encodeFunctionData({ - abi: [abiItem], - args: [amountOut, receiver], - }) - - const swapData = encodeAbiParameters(parseAbiParameters("address, bytes"), [ - vault, - mintData, - ]) - - const swapperAmountOut = - swapParams.swapperMode === SwapperMode.EXACT_IN - ? 0n //ignored - : swapParams.swapperMode === SwapperMode.EXACT_OUT - ? amountOut - : swapParams.targetDebt - - const swapMulticallItem = encodeSwapMulticallItem({ - handler: SWAPPER_HANDLER_GENERIC, - mode: BigInt(swapParams.swapperMode), - account: swapParams.accountOut, - tokenIn: swapParams.tokenIn.addressInfo, - tokenOut: swapParams.tokenOut.addressInfo, - vaultIn: swapParams.vaultIn, - accountIn: swapParams.accountIn, - receiver: swapParams.receiver, - amountOut: swapperAmountOut, - data: swapData, - }) - - return { - amountIn, - amountOut, - swapMulticallItem, - } -} - -export async function fetchPreviewRedeem( - chainId: number, - vault: Address, - amount: bigint, -) { - const client = getViemClient(chainId) - - const abiItem = { - name: "previewRedeem", - inputs: [{ name: "shares", type: "uint256" }], - outputs: [{ name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - } - - const query = { - address: vault, - abi: [abiItem], - functionName: "previewRedeem", - args: [amount], - } as const - - const data = (await client.readContract(query)) as bigint - - return data -} - -export async function fetchPreviewWithdraw( +export async function fetchVirtualPrice( chainId: number, - vault: Address, - amount: bigint, + cdo: Address, + tranche: Address, ) { const client = getViemClient(chainId) - const abiItem = { - name: "previewWithdraw", - inputs: [{ name: "assets", type: "uint256" }], - outputs: [{ name: "", type: "uint256" }], + name: "virtualPrice", + inputs: [{ name: "_tranche", type: "address" }], + outputs: [{ name: "_virtualPrice", type: "uint256" }], stateMutability: "view", type: "function", } const query = { - address: vault, + address: cdo, abi: [abiItem], - functionName: "previewWithdraw", - args: [amount], + functionName: "virtualPrice", + args: [tranche], } as const const data = (await client.readContract(query)) as bigint @@ -874,62 +354,24 @@ export async function fetchPreviewWithdraw( return data } -export async function fetchPreviewDeposit( - chainId: number, - vault: Address, - amount: bigint, -) { - const client = getViemClient(chainId) - - const abiItem = { - name: "previewDeposit", - inputs: [{ name: "assets", type: "uint256" }], - outputs: [{ name: "", type: "uint256" }], - stateMutability: "view", - type: "function", - } - - const query = { - address: vault, - abi: [abiItem], - functionName: "previewDeposit", - args: [amount], - } as const - - const data = (await client.readContract(query)) as bigint - - return data +const getViemClient = (chainId: number) => { + if (!viemClients[chainId]) + throw new Error(`No client found for chainId ${chainId}`) + return viemClients[chainId].extend(publicActions) } -export async function fetchPreviewMint( - chainId: number, - vault: Address, - amount: bigint, -) { - const client = getViemClient(chainId) - +function encodeSwapExactTokensForAATranche(amount: bigint) { const abiItem = { - name: "previewMint", - inputs: [{ name: "shares", type: "uint256" }], - outputs: [{ name: "", type: "uint256" }], - stateMutability: "view", + inputs: [{ name: "amountIn", type: "uint256" }], + name: "swapExactTokensForAATranche", + stateMutability: "nonpayable", type: "function", } - const query = { - address: vault, + const functionData = encodeFunctionData({ abi: [abiItem], - functionName: "previewMint", args: [amount], - } as const - - const data = (await client.readContract(query)) as bigint - - return data -} + }) -const getViemClient = (chainId: number) => { - if (!viemClients[chainId]) - throw new Error(`No client found for chainId ${chainId}`) - return viemClients[chainId].extend(publicActions) + return functionData }