diff --git a/src/swapService/config/mainnet.ts b/src/swapService/config/mainnet.ts index fc36668..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" @@ -15,6 +16,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 @@ -30,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: { 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/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/strategyIdleCDOTranche.ts b/src/swapService/strategies/strategyIdleCDOTranche.ts new file mode 100644 index 0000000..f6fd466 --- /dev/null +++ b/src/swapService/strategies/strategyIdleCDOTranche.ts @@ -0,0 +1,377 @@ +import { viemClients } from "@/common/utils/viemClients" +import { + type Address, + encodeAbiParameters, + encodeFunctionData, + isAddressEqual, + maxUint256, + parseAbiParameters, + publicActions, +} from "viem" +import { type SwapApiResponse, SwapperMode } from "../interface" +import { runPipeline } from "../runner" +import type { StrategyResult, SwapParams } from "../types" +import { + SWAPPER_HANDLER_GENERIC, + applySlippage, + buildApiResponseSwap, + buildApiResponseVerifySkimMin, + encodeSwapMulticallItem, + findToken, + isExactInRepay, + matchParams, +} from "../utils" + +const defaultConfig: { + supportedTranches: Array<{ + chainId: number + swapHandler: Address + cdo: Address + aaTranche: Address + aaTrancheVault: Address + underlying: Address + underlyingDecimals: bigint + priceOne: bigint + }> +} = { + supportedTranches: [ + { + // IdleCDO AA Tranche - idle_Fasanara + chainId: 1, + swapHandler: "0xA24689b6Ab48eCcF7038c70eBC39f9ed4217aFE3", + cdo: "0xf6223C567F21E33e859ED7A045773526E9E3c2D5", + aaTranche: "0x45054c6753b4Bce40C5d54418DabC20b070F85bE", + aaTrancheVault: "0xd820C8129a853a04dC7e42C64aE62509f531eE5A", + underlying: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + underlyingDecimals: 6n, + priceOne: 1000000n, + }, + ], +} + +const PROTOCOL = { providerName: "Idle" } + +// Strategy uses a special SwapHandler contract, which deposits into IdleCDO tranches +export class StrategyIdleCDOTranche { + 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) => + swapParams.swapperMode === SwapperMode.EXACT_IN && + v.chainId === swapParams.chainId && + // 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: StrategyIdleCDOTranche.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)) { + throw new Error("Not supported") + } + if ( + this.isSupportedTrancheUnderlying({ + aaTranche: swapParams.tokenOut.addressInfo, + underlying: swapParams.tokenIn.addressInfo, + }) + ) { + result.response = + await this.exactInFromUnderlyingToTranche(swapParams) + } else { + result.response = await this.exactInFromAnyToTranche(swapParams) + } + break + } + // case SwapperMode.EXACT_OUT: + default: { + result.error = "Unsupported swap mode" + } + } + } catch (error) { + result.error = error + } + + return result + } + + async exactInFromUnderlyingToTranche( + swapParams: SwapParams, + ): Promise { + const trancheData = this.getSupportedTranche( + swapParams.tokenOut.addressInfo, + ) + + const amountOut = await this.getDepositAmountOut( + swapParams.chainId, + trancheData.aaTranche, + swapParams.amount, + ) + + const swapHandlerMulticallItem = this.encodeSwapToTrancheSwapMulticallItem( + trancheData.aaTranche, + swapParams, + swapParams.amount, + ) + + const multicallItems = [swapHandlerMulticallItem] + + const swap = buildApiResponseSwap(swapParams.from, multicallItems) + + const amountOutMin = amountOut // tranche price should not decrease under normal circumstances + 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: 0, + route: [PROTOCOL], + swap, + verify, + } + } + + async exactInFromAnyToTranche( + 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 this.getDepositAmountOut( + swapParams.chainId, + trancheData.aaTranche, + BigInt(innerSwap.amountOut), + ) + const amountOutMin = applySlippage(amountOut, swapParams.slippage) + + 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, + swapHandlerMulticallItem, + ] + 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, + } + } + + encodeSwapToTrancheSwapMulticallItem( + tranche: Address, + swapParams: SwapParams, + 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 + + const swapHandlerMulticallItem = encodeSwapMulticallItem({ + handler: SWAPPER_HANDLER_GENERIC, + mode: BigInt(swapParams.swapperMode), + account: swapParams.accountOut, + tokenIn: swapParams.tokenIn.addressInfo, + tokenOut: tranche, + vaultIn: swapParams.vaultIn, + accountIn: swapParams.accountIn, + receiver: swapParams.receiver, + amountOut: swapperAmountOut, + data: swapData, + }) + + return swapHandlerMulticallItem + } + + async getDepositAmountOut( + chainId: number, + tranche: Address, + amountIn: bigint, + ) { + const trancheData = this.getSupportedTranche(tranche) + + const virtualPrice = await fetchVirtualPrice( + chainId, + trancheData.cdo, + trancheData.aaTranche, + ) + const amountOut = + (amountIn * + trancheData.priceOne * + 10n ** (18n - trancheData.underlyingDecimals)) / + virtualPrice + + return amountOut + } + + async getWithdrawAmountOut( + chainId: number, + tranche: Address, + amountIn: bigint, + ) { + const trancheData = this.getSupportedTranche(tranche) + + const virtualPrice = await fetchVirtualPrice( + chainId, + trancheData.cdo, + trancheData.aaTranche, + ) + const amountOut = + (amountIn * + virtualPrice * + 10n ** (18n - trancheData.underlyingDecimals)) / + trancheData.priceOne + + return amountOut + } + + 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 fetchVirtualPrice( + chainId: number, + cdo: Address, + tranche: Address, +) { + const client = getViemClient(chainId) + 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 +} + +const getViemClient = (chainId: number) => { + if (!viemClients[chainId]) + throw new Error(`No client found for chainId ${chainId}`) + return viemClients[chainId].extend(publicActions) +} + +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 +}