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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
readContract,
sendTransaction,
} from "viem/actions";
import { Spectra } from "../src/tokens/spectra";

const converter = new BlueSdkConverter({
parseAddress: safeGetAddress,
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
155 changes: 154 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,21 @@ import {
type Client,
type Transport,
encodeFunctionData,
erc4626Abi,
getAddress,
} from "viem";
import { readContract } from "viem/actions";
import { daiUsdsConverterAbi, mkrSkyConverterAbi } from "./abis.js";
import {
SpectraCurveAbi,
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 +121,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 = getAddress(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.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 @@ -325,6 +391,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 @@ -386,6 +461,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 @@ -455,6 +566,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
112 changes: 110 additions & 2 deletions packages/liquidation-sdk-viem/src/abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,11 +943,11 @@ export const curveStableSwapNGAbi = [
inputs: [
{
name: "i",
type: "int128",
type: "uint256",
},
{
name: "j",
type: "int128",
type: "uint256",
},
{
name: "dx",
Expand Down Expand Up @@ -2331,3 +2331,111 @@ 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;

export const SpectraCurveAbi = [
{
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",
},
],
},
];

export const testAbi = [
{
type: "function",
name: "maxRedeem",
inputs: [{ name: "owner", type: "address", internalType: "address" }],
outputs: [
{
name: "maxWstUSRAmount",
type: "uint256",
internalType: "uint256",
},
],
stateMutability: "view",
},
] as const;
Loading
Loading