Skip to content

Commit

Permalink
feat: add spectra liquidation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Grimal committed Jan 27, 2025
1 parent 3764527 commit 1268dd7
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
mainnetAddresses,
} from "@morpho-org/liquidation-sdk-viem";
import { Time } from "@morpho-org/morpho-ts";
import { Spectra } from "src/tokens/spectra";
import {
type Account,
type Chain,
Expand Down Expand Up @@ -164,10 +165,12 @@ export const check = async <
const slippage =
(market.params.liquidationIncentiveFactor - BigInt.WAD) / 2n;

const pendleTokens =
const [pendleTokens, spectraTokens] = await Promise.all([
chainId === ChainId.EthMainnet
? await Pendle.getTokens(chainId)
: undefined;
? Pendle.getTokens(chainId)
: undefined,
Spectra.getTokens(chainId),
]);

await Promise.allSettled(
triedLiquidity.map(
Expand All @@ -193,6 +196,12 @@ export const check = async <
));
}

({ srcAmount, srcToken } = await encoder.handleSpectraTokens(
market.params.collateralToken,
seizedAssets,
spectraTokens,
));

// As there is no liquidity for sUSDS, we use the sUSDS withdrawal function to get USDS instead
if (
market.params.collateralToken === mainnetAddresses.sUsds &&
Expand Down
117 changes: 116 additions & 1 deletion packages/liquidation-sdk-viem/src/LiquidationEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ import {
type Client,
type Transport,
encodeFunctionData,
erc4626Abi,
} from "viem";
import { readContract } from "viem/actions";
import { daiUsdsConverterAbi, mkrSkyConverterAbi } from "./abis.js";
import {
SpectraPrincipalToken,
daiUsdsConverterAbi,
mkrSkyConverterAbi,
} from "./abis.js";
import { curveStableSwapNGAbi, sUsdsAbi } from "./abis.js";
import { curvePools, mainnetAddresses } from "./addresses.js";
import { fetchBestSwap } from "./swap/index.js";
import { Pendle, Sky, Usual } from "./tokens/index.js";
import { Spectra } from "./tokens/spectra.js";

interface SwapAttempt {
srcAmount: bigint;
Expand Down Expand Up @@ -113,6 +119,64 @@ export class LiquidationEncoder<
return { srcAmount, srcToken };
}

async handleSpectraTokens(
collateralToken: Address,
seizedAssets: bigint,
spectraTokens: Spectra.PrincipalToken[],
) {
if (!Spectra.isPTToken(collateralToken, spectraTokens)) {
return {
srcAmount: seizedAssets,
srcToken: collateralToken,
};
}

const pt = Spectra.getPTInfo(collateralToken, spectraTokens);
const maturity = pt.maturity;

let srcAmount = seizedAssets;
let srcToken = collateralToken;

if (maturity > Date.now()) {
this.spectraPTRedeem(collateralToken, seizedAssets);

srcAmount = await this.spectraRedeemAmount(collateralToken, seizedAssets);
srcToken = pt.underlying.address as Address;
} else {
if (pt.pools.length === 0 || pt.pools[0] === undefined)
return { srcAmount: seizedAssets, srcToken: collateralToken };
const ibt = pt.ibt.address as `0x${string}`;
const poolAddress = pt.pools[0].address as `0x${string}`;

const index0Token = await this.getCurveSwapIndex0Token(poolAddress);
const ptIndex = index0Token === pt.underlying.address ? 0n : 1n;
const ibtIndex = ptIndex === 0n ? 1n : 0n;

const swapAmount = await this.getCurveSwapOutputAmountFromInput(
poolAddress,
seizedAssets,
ptIndex,
ibtIndex,
);

srcAmount = await this.spectraRedeemAmount(ibt, swapAmount);
srcToken = pt.underlying.address as Address;

this.erc20Approve(collateralToken, poolAddress, MathLib.MAX_UINT_256);
this.curveSwap(
poolAddress,
seizedAssets,
ptIndex,
ibtIndex,
swapAmount,
this.address,
);
this.spectraIBTRedeem(ibt, swapAmount);
}

return { srcAmount, srcToken };
}

/**
* Swaps USD0USD0++ for USDC through the USD0/USD0++ && USD0/USDC pools
* Route is USD0USD0++ -> USD0 -> USDC
Expand Down Expand Up @@ -325,6 +389,15 @@ export class LiquidationEncoder<
});
}

public getCurveSwapIndex0Token(pool: Address) {
return readContract(this.client, {
address: pool,
abi: curveStableSwapNGAbi,
functionName: "coins",
args: [0n],
});
}

public removeLiquidityFromCurvePool(
pool: Address,
amount: bigint,
Expand Down Expand Up @@ -455,6 +528,48 @@ export class LiquidationEncoder<
);
}

public previewIBTRedeem(ibt: Address, shares: bigint) {
return readContract(this.client, {
address: ibt,
abi: erc4626Abi,
functionName: "previewRedeem",
args: [shares],
});
}

public spectraRedeemAmount(pt: Address, amount: bigint) {
return readContract(this.client, {
address: pt,
abi: SpectraPrincipalToken,
functionName: "convertToUnderlying",
args: [amount],
});
}

public spectraPTRedeem(pt: Address, amount: bigint) {
this.pushCall(
pt,
0n,
encodeFunctionData({
abi: SpectraPrincipalToken,
functionName: "redeem",
args: [amount, this.address, this.address],
}),
);
}

public spectraIBTRedeem(ibt: Address, amount: bigint) {
this.pushCall(
ibt,
0n,
encodeFunctionData({
abi: erc4626Abi,
functionName: "redeem",
args: [amount, this.address, this.address],
}),
);
}

public async handleTokenSwap(
chainId: ChainId,
initialSrcToken: Address,
Expand Down
27 changes: 27 additions & 0 deletions packages/liquidation-sdk-viem/src/abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2331,3 +2331,30 @@ export const daiUsdsConverterAbi = [
type: "function",
},
] as const;

export const SpectraPrincipalToken = [
{
type: "function",
name: "redeem",
inputs: [
{ name: "shares", type: "uint256", internalType: "uint256" },
{ name: "receiver", type: "address", internalType: "address" },
{ name: "owner", type: "address", internalType: "address" },
],
outputs: [{ name: "assets", type: "uint256", internalType: "uint256" }],
stateMutability: "nonpayable",
},
{
type: "function",
name: "convertToUnderlying",
inputs: [
{
name: "principalAmount",
type: "uint256",
internalType: "uint256",
},
],
outputs: [{ name: "", type: "uint256", internalType: "uint256" }],
stateMutability: "view",
},
] as const;
141 changes: 141 additions & 0 deletions packages/liquidation-sdk-viem/src/tokens/spectra.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { ChainId } from "@morpho-org/blue-sdk";

export namespace Spectra {
export const apiUrl = (chainId: ChainId) => {
switch (chainId) {
case ChainId.EthMainnet:
return "https://app.spectra.finance/api/v1/MAINNET/pools";
case ChainId.BaseMainnet:
return "https://app.spectra.finance/api/v1/BASE/pools";
default:
return undefined;
}
};

export type PrincipalToken = {
address: string;
name: string;
symbol: string;
decimals: bigint;
chainId: bigint;
rate: bigint;
yt: YieldToken;
ibt: InterestBearingToken;
underlying: UnderlyingAsset;
maturity: bigint;
createdAt: bigint;
pools: Pool[];
maturityValue: MaturityValue;
};

type YieldToken = {
address: string;
decimals: bigint;
chainId: bigint;
};

type InterestBearingToken = {
address: string;
name: string;
symbol: string;
decimals: bigint;
chainId: bigint;
rate: bigint;
apr: APR;
price: Price;
protocol: string;
};

type APR = {
total: number;
details: {
base: number;
rewards: Record<string, number>;
};
};

type Price = {
underlying: number;
usd: number;
};

type UnderlyingAsset = {
address: string;
name: string;
symbol: string;
decimals: bigint;
chainId: bigint;
price: {
usd: number;
};
};

type Pool = {
address: string;
chainId: bigint;
lpt: LPToken;
liquidity: Liquidity;
impliedApy: number | null;
lpApy: LPApy;
ibtToPt: bigint | null;
ptToIbt: bigint | null;
ptPrice: TokenPrice;
ytPrice: TokenPrice;
ibtAmount: bigint;
ptAmount: bigint;
feeRate: bigint;
};

type LPToken = {
address: string;
decimals: bigint;
chainId: bigint;
supply: bigint;
};

type Liquidity = {
underlying: number;
usd: number;
};

type LPApy = {
total: number | null;
details: {
fees: number;
pt: number | null;
ibt: number;
};
};

type TokenPrice = {
underlying: number;
usd: number;
};

type MaturityValue = {
underlying: bigint;
usd: number;
};

export async function getTokens(chainId: ChainId): Promise<PrincipalToken[]> {
const url = apiUrl(chainId);

if (!url) {
return [];
}

const res = await fetch(url);

if (!res.ok) throw new Error(res.statusText);

return (await res.json()) as PrincipalToken[];
}

export function isPTToken(token: string, spectraTokens: PrincipalToken[]) {
return spectraTokens.some((tokenInfo) => tokenInfo.address === token);
}

export function getPTInfo(token: string, spectraTokens: PrincipalToken[]) {
return spectraTokens.find((tokenInfo) => tokenInfo.address === token)!;
}
}

0 comments on commit 1268dd7

Please sign in to comment.