Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add spectra liquidation logic #253

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
LiquidationEncoder,
Midas,
Pendle,
Spectra,
apiSdk,
mainnetAddresses,
} from "@morpho-org/liquidation-sdk-viem";
Expand Down Expand Up @@ -167,10 +168,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 @@ -196,6 +199,12 @@ export const check = async <
));
}

({ srcAmount, srcToken } = await encoder.handleSpectraTokens(
srcToken,
seizedAssets,
spectraTokens,
));
Jean-Grimal marked this conversation as resolved.
Show resolved Hide resolved

// 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
160 changes: 156 additions & 4 deletions packages/liquidation-sdk-viem/src/LiquidationEncoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
type Transport,
encodeFunctionData,
erc20Abi,
erc4626Abi,
getAddress,
parseEther,
} from "viem";
import { readContract } from "viem/actions";
import {
Expand All @@ -21,10 +24,12 @@ import {
mkrSkyConverterAbi,
redemptionVaultAbi,
sUsdsAbi,
spectraCurveAbi,
spectraPrincipalTokenAbi,
} from "./abis.js";
import { curvePools, mainnetAddresses } from "./addresses.js";
import { fetchBestSwap } from "./swap/index.js";
import { Midas, Pendle, Sky, Usual } from "./tokens/index.js";
import { Midas, Pendle, Sky, Spectra, Usual } from "./tokens/index.js";

interface SwapAttempt {
srcAmount: bigint;
Expand All @@ -43,9 +48,7 @@ export class LiquidationEncoder<
seizedAssets: bigint,
pendleTokens: Pendle.TokenListResponse,
) {
if (
!Pendle.isPTToken(collateralToken, this.client.chain.id, pendleTokens)
) {
if (!Pendle.isPT(collateralToken, this.client.chain.id, pendleTokens)) {
return {
srcAmount: seizedAssets,
srcToken: collateralToken,
Expand Down Expand Up @@ -120,6 +123,68 @@ export class LiquidationEncoder<
return { srcAmount, srcToken };
}

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

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

let srcAmount = seizedAssets;
let srcToken = collateralToken;

if (Number(maturity) < Date.now() / 1000) {
this.spectraPTRedeem(collateralToken, seizedAssets);

srcAmount = await this.spectraRedeemAmount(collateralToken, seizedAssets);
srcToken = getAddress(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 = getAddress(pt.pools[0].address) as `0x${string}`;

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

const swapAmount = MathLib.wMulDown(
await readContract(this.client, {
address: poolAddress,
abi: spectraCurveAbi,
functionName: "get_dy",
args: [ptIndex, ibtIndex, seizedAssets],
}),
parseEther("0.9999999"), // 0.0000001% buffer because exact value doesn't work
);

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

this.erc20Approve(collateralToken, poolAddress, MathLib.MAX_UINT_256);

this.spectraCurveSwap(
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 @@ -332,6 +397,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 @@ -393,6 +467,42 @@ export class LiquidationEncoder<
);
}

public spectraCurveSwap(
pool: Address,
amount: bigint,
inputTokenIndex: bigint,
outputTokenIndex: bigint,
minDestAmount: bigint,
receiver: Address,
) {
this.pushCall(
pool,
0n,
/**
* @notice Perform an exchange between two coins
* @dev Index values can be found via the `coins` public getter method
* @param i Index value for the coin to send
* @param j Index value of the coin to receive
* @param _dx Amount of `i` being exchanged
* @param _min_dy Minimum amount of `j` to receive
* @param _receiver Address that receives `j`
* @return Actual amount of `j` received
*/
encodeFunctionData({
abi: spectraCurveAbi,
functionName: "exchange",
args: [
inputTokenIndex,
outputTokenIndex,
amount,
minDestAmount,
false,
receiver,
],
}),
);
}

public previewUSDSWithdrawalAmount(amount: bigint) {
return readContract(this.client, {
address: mainnetAddresses.sUsds!,
Expand Down Expand Up @@ -462,6 +572,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: spectraPrincipalTokenAbi,
functionName: "convertToUnderlying",
args: [amount],
});
}

public spectraPTRedeem(pt: Address, amount: bigint) {
this.pushCall(
pt,
0n,
encodeFunctionData({
abi: spectraPrincipalTokenAbi,
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
92 changes: 92 additions & 0 deletions packages/liquidation-sdk-viem/src/abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2332,6 +2332,98 @@ export const daiUsdsConverterAbi = [
},
] as const;

export const spectraPrincipalTokenAbi = [
{
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;

export const spectraCurveAbi = [
Jean-Grimal marked this conversation as resolved.
Show resolved Hide resolved
{
stateMutability: "view",
type: "function",
name: "get_dy",
inputs: [
{
name: "i",
type: "uint256",
},
{
name: "j",
type: "uint256",
},
{
name: "dx",
type: "uint256",
},
],
outputs: [
{
name: "",
type: "uint256",
},
],
},
{
stateMutability: "payable",
type: "function",
name: "exchange",
inputs: [
{
name: "i",
type: "uint256",
},
{
name: "j",
type: "uint256",
},
{
name: "dx",
type: "uint256",
},
{
name: "min_dy",
type: "uint256",
},
{
name: "use_eth",
type: "bool",
},
{
name: "receiver",
type: "address",
},
],
outputs: [
{
name: "",
type: "uint256",
},
],
},
] as const;

export const redemptionVaultAbi = [
{
anonymous: false,
Expand Down
1 change: 1 addition & 0 deletions packages/liquidation-sdk-viem/src/tokens/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./pendle.js";
export * from "./usual.js";
export * from "./sky.js";
export * from "./spectra.js";
export * from "./midas.js";
2 changes: 1 addition & 1 deletion packages/liquidation-sdk-viem/src/tokens/pendle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ export namespace Pendle {
);
}

export function isPTToken(
export function isPT(
token: string,
chainId: ChainId,
pendleTokens: TokenListResponse,
Expand Down
Loading
Loading