diff --git a/packages/blue-api-sdk/src/types.ts b/packages/blue-api-sdk/src/types.ts index da4d013b..80635d4e 100644 --- a/packages/blue-api-sdk/src/types.ts +++ b/packages/blue-api-sdk/src/types.ts @@ -58,6 +58,8 @@ export type Asset = { oraclePriceUsd: Maybe; /** Current price in USD, for display purpose. */ priceUsd: Maybe; + /** Risk related data on the asset */ + riskAnalysis: Maybe>; /** Current spot price in ETH. */ spotPriceEth: Maybe; symbol: Scalars["String"]["output"]; @@ -90,6 +92,11 @@ export type AssetSpotPriceEthArgs = { timestamp?: InputMaybe; }; +export enum AssetOrderBy { + Address = "Address", + CredoraRiskScore = "CredoraRiskScore", +} + /** Asset yield */ export type AssetYield = { __typename?: "AssetYield"; @@ -102,6 +109,10 @@ export type AssetsFilters = { address_in?: InputMaybe>; /** Filter by chain id */ chainId_in?: InputMaybe>; + /** Filter by credora risk score greater than or equal to given value */ + credoraRiskScore_gte?: InputMaybe; + /** Filter by credora risk score lower than or equal to given value */ + credoraRiskScore_lte?: InputMaybe; /** Filter by asset id */ id_in?: InputMaybe>; search?: InputMaybe; @@ -212,6 +223,8 @@ export type Market = { realizedBadDebt: Maybe; /** Underlying amount of assets that can be reallocated to this market */ reallocatableLiquidityAssets: Maybe; + /** Risk related data on the market */ + riskAnalysis: Maybe>; /** Current state */ state: Maybe; /** Vaults with the market in supply queue */ @@ -311,6 +324,10 @@ export type MarketFilters = { /** Filter by collateral asset tags. */ collateralAssetTags_in?: InputMaybe>; countryCode?: InputMaybe; + /** Filter by credora risk score greater than or equal to given value */ + credoraRiskScore_gte?: InputMaybe; + /** Filter by credora risk score lower than or equal to given value */ + credoraRiskScore_lte?: InputMaybe; /** Filter by greater than or equal to given fee rate */ fee_gte?: InputMaybe; /** Filter by lower than or equal to given fee rate */ @@ -746,6 +763,7 @@ export enum MarketOrderBy { BorrowAssetsUsd = "BorrowAssetsUsd", BorrowShares = "BorrowShares", CollateralAssetSymbol = "CollateralAssetSymbol", + CredoraRiskScore = "CredoraRiskScore", DailyBorrowApy = "DailyBorrowApy", DailyNetBorrowApy = "DailyNetBorrowApy", Fee = "Fee", @@ -1483,6 +1501,7 @@ export type Query = { publicAllocators: PaginatedPublicAllocator; search: SearchResults; transaction: Transaction; + /** @deprecated Multiple Transaction entities correspond to a single hash, because a Transaction entity corresponds to an onchain event. */ transactionByHash: Transaction; transactions: PaginatedTransactions; user: User; @@ -1507,6 +1526,8 @@ export type QueryAssetByAddressArgs = { export type QueryAssetsArgs = { first?: InputMaybe; + orderBy?: InputMaybe; + orderDirection?: InputMaybe; skip?: InputMaybe; where?: InputMaybe; }; @@ -1709,6 +1730,19 @@ export type QueryVaultsArgs = { where?: InputMaybe; }; +/** Risk analysis */ +export type RiskAnalysis = { + __typename?: "RiskAnalysis"; + isUnderReview: Scalars["Boolean"]["output"]; + provider: RiskProvider; + score: Scalars["Float"]["output"]; + timestamp: Scalars["Float"]["output"]; +}; + +export enum RiskProvider { + Credora = "CREDORA", +} + /** Global search results */ export type SearchResults = { __typename?: "SearchResults"; @@ -1756,7 +1790,8 @@ export type TransactionData = | MarketCollateralTransferTransactionData | MarketLiquidationTransactionData | MarketTransferTransactionData - | VaultTransactionData; + | VaultTransactionData + | VaultTransferTransactionData; /** Filtering options for transactions. AND operator is used for multiple filters, while OR operator is used for multiple values in the same filter. */ export type TransactionFilters = { @@ -1847,6 +1882,7 @@ export enum TransactionType { MarketWithdrawCollateral = "MarketWithdrawCollateral", MetaMorphoDeposit = "MetaMorphoDeposit", MetaMorphoFee = "MetaMorphoFee", + MetaMorphoTransfer = "MetaMorphoTransfer", MetaMorphoWithdraw = "MetaMorphoWithdraw", } @@ -1870,7 +1906,7 @@ export type User = { __typename?: "User"; address: Scalars["Address"]["output"]; chain: Chain; - historicalState: Array; + historicalState: UserHistory; id: Scalars["ID"]["output"]; marketPositions: Array; state: UserState; @@ -1888,6 +1924,16 @@ export type UserHistory = { vaultsPnlUsd: Maybe>; }; +/** User state history */ +export type UserHistoryMarketsPnlUsdArgs = { + options?: InputMaybe; +}; + +/** User state history */ +export type UserHistoryVaultsPnlUsdArgs = { + options?: InputMaybe; +}; + /** User state */ export type UserState = { __typename?: "UserState"; @@ -1961,6 +2007,8 @@ export type Vault = { pendingCaps: Maybe>; /** Public allocator configuration */ publicAllocatorConfig: Maybe; + /** Risk related data on the vault */ + riskAnalysis: Maybe>; state: Maybe; symbol: Scalars["String"]["output"]; /** Vault warnings */ @@ -2080,6 +2128,10 @@ export type VaultFilters = { countryCode?: InputMaybe; /** Filter by MetaMorpho creator address */ creatorAddress_in?: InputMaybe>; + /** Filter by credora risk score greater than or equal to given value */ + credoraRiskScore_gte?: InputMaybe; + /** Filter by credora risk score lower than or equal to given value */ + credoraRiskScore_lte?: InputMaybe; /** Filter by MetaMorpho current curator address */ curatorAddress_in?: InputMaybe>; /** Filter by greater than or equal to given fee rate. */ @@ -2337,6 +2389,7 @@ export type VaultMetadataCurator = { export enum VaultOrderBy { Address = "Address", Apy = "Apy", + CredoraRiskScore = "CredoraRiskScore", Curator = "Curator", DailyApy = "DailyApy", DailyNetApy = "DailyNetApy", @@ -2602,7 +2655,7 @@ export type VaultStateReward = { yearlySupplyTokens: Scalars["BigInt"]["output"]; }; -/** Meta Morpho vault transfer transaction data */ +/** Meta Morpho vault transaction data */ export type VaultTransactionData = { __typename?: "VaultTransactionData"; assets: Scalars["BigInt"]["output"]; @@ -2611,6 +2664,13 @@ export type VaultTransactionData = { vault: Vault; }; +/** Meta Morpho vault transfer transaction data */ +export type VaultTransferTransactionData = { + __typename?: "VaultTransferTransactionData"; + shares: Scalars["BigInt"]["output"]; + vault: Vault; +}; + /** Vault warning */ export type VaultWarning = { __typename?: "VaultWarning"; diff --git a/packages/blue-api-sdk/src/warnings.ts b/packages/blue-api-sdk/src/warnings.ts index 962f91bc..b3027e3f 100644 --- a/packages/blue-api-sdk/src/warnings.ts +++ b/packages/blue-api-sdk/src/warnings.ts @@ -13,6 +13,8 @@ export enum MarketWarningType { IncorrectCollateralExchangeRate = "incorrect_collateral_exchange_rate", IncorrectLoanExchangeRate = "incorrect_loan_exchange_rate", NonWhitelisted = "not_whitelisted", + OracleNotFromFactory = "oracle_not_from_factory", + MisconfiguredOracleDecimals = "misconfigured_oracle_decimals", } export enum VaultWarningType { diff --git a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts index 18f9e8af..63bbdb88 100644 --- a/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts +++ b/packages/liquidation-sdk-viem/examples/whitelisted-erc4626-1inch.ts @@ -136,10 +136,8 @@ export const check = async < .filter( (seizedAssets) => collateralToken.toUsd(seizedAssets)! > parseEther("1000") || - (accrualPosition.collateral === seizedAssets && - loanToken.toUsd(accrualPosition.borrowAssets)! > - parseEther("1000")), - ) // Do not try seizing less than $1000 collateral, except if we can realize more than $1000 bad debt. + accrualPosition.collateral === seizedAssets, + ) // Do not try seizing less than $1000 collateral, except if we can realize debt. .map(async (seizedAssets) => { const repaidShares = market.getLiquidationRepaidShares(seizedAssets)!; @@ -175,6 +173,9 @@ export const check = async < triedLiquidity.map( async ({ seizedAssets, repaidAssets, withdrawnAssets }) => { try { + const badDebtRealization = + seizedAssets === accrualPosition.collateral; + let srcToken = collateralUnderlyingAsset ?? market.params.collateralToken; let srcAmount = withdrawnAssets ?? seizedAssets; @@ -252,8 +253,9 @@ export const check = async < srcToken, srcAmount, market.params, - slippage, + slippage / 10n ** 16n, repaidAssets, + client.account.address, ); if (result) { @@ -322,7 +324,7 @@ export const check = async < ); const profitUsd = loanToken.toUsd(dstAmount - repaidAssets)!; - if (gasLimitUsd > profitUsd) + if (!badDebtRealization && gasLimitUsd > profitUsd) throw Error( `gas cost ($${gasLimitUsd.formatWad( 2, diff --git a/packages/liquidation-sdk-viem/src/LiquidationEncoder.ts b/packages/liquidation-sdk-viem/src/LiquidationEncoder.ts index a2de5079..4a62447f 100644 --- a/packages/liquidation-sdk-viem/src/LiquidationEncoder.ts +++ b/packages/liquidation-sdk-viem/src/LiquidationEncoder.ts @@ -462,6 +462,7 @@ export class LiquidationEncoder< marketParams: MarketParams, slippage: bigint, repaidAssets: bigint, + origin: Address, ) { let srcToken = initialSrcToken; const srcAmount = initialSrcAmount; @@ -476,6 +477,7 @@ export class LiquidationEncoder< amount: srcAmount, from: this.address, slippage, + origin, includeTokensInfo: false, includeProtocols: false, includeGas: false, @@ -513,6 +515,7 @@ export class LiquidationEncoder< dst: marketParams.loanToken, amount: halfAmount, from: this.address, + origin, slippage, includeTokensInfo: false, includeProtocols: false, @@ -529,6 +532,7 @@ export class LiquidationEncoder< dst: marketParams.loanToken, amount: halfAmount, from: this.address, + origin, slippage, includeTokensInfo: false, includeProtocols: false, diff --git a/packages/liquidation-sdk-viem/src/swap/types.ts b/packages/liquidation-sdk-viem/src/swap/types.ts index d60f9326..207c555f 100644 --- a/packages/liquidation-sdk-viem/src/swap/types.ts +++ b/packages/liquidation-sdk-viem/src/swap/types.ts @@ -7,6 +7,7 @@ export interface SwapParams { dst: string; amount: BigIntish; from: string; + origin: string; slippage: BigIntish; protocols?: string; fee?: BigIntish; diff --git a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts index 7060fdcf..a531530c 100644 --- a/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts +++ b/packages/liquidation-sdk-viem/test/examples/whitelisted-erc4626-1inch.test.ts @@ -906,6 +906,163 @@ describe("erc4626-1inch", () => { }, ); + // Cannot run concurrently because `fetch` is mocked globally. + test.sequential( + `should liquidate on standard market an unprofitable position with bad debt`, + async ({ client, encoder }) => { + const collateralPriceUsd = 3_129; + const ethPriceUsd = 2_653; + + const marketId = + "0xb8fc70e82bc5bb53e773626fcc6a23f7eefa036918d7ef216ecfb1950a94a85e" as MarketId; // wstETH/WETH (96.5%) + + const market = await fetchMarket(marketId, client); + const [collateralToken, loanToken] = await Promise.all([ + fetchToken(market.params.collateralToken, client), + fetchToken(market.params.loanToken, client), + ]); + + const collateral = parseUnits("10000", collateralToken.decimals); + await client.deal({ + erc20: collateralToken.address, + account: borrower.address, + amount: collateral, + }); + await client.approve({ + account: borrower, + address: collateralToken.address, + args: [morpho, maxUint256], + }); + await client.writeContract({ + account: borrower, + address: morpho, + abi: blueAbi, + functionName: "supplyCollateral", + args: [market.params, collateral, borrower.address, "0x"], + }); + + const borrowed = market.getMaxBorrowAssets(collateral)! - 1n; + await client.deal({ + erc20: loanToken.address, + account: borrower.address, + amount: borrowed - market.liquidity, + }); + await client.approve({ + account: borrower, + address: loanToken.address, + args: [morpho, maxUint256], + }); + await client.writeContract({ + account: borrower, + address: morpho, + abi: blueAbi, + functionName: "supply", + args: [ + market.params, + borrowed - market.liquidity, // 100% utilization after borrow. + 0n, + borrower.address, + "0x", + ], + }); + + await client.writeContract({ + account: borrower, + address: morpho, + abi: blueAbi, + functionName: "borrow", + args: [ + market.params as InputMarketParams, + borrowed, + 0n, + borrower.address, + borrower.address, + ], + }); + + await client.setNextBlockTimestamp({ + timestamp: (await client.timestamp()) + Time.s.from.y(1n), + }); + + nock(BLUE_API_BASE_URL) + .post("/graphql") + .reply(200, { data: { markets: { items: [{ uniqueKey: marketId }] } } }) + .post("/graphql") + .reply(200, { + data: { + assetByAddress: { + priceUsd: ethPriceUsd, + spotPriceEth: 1, + }, + marketPositions: { + items: [ + { + user: { + address: borrower.address, + }, + market: { + uniqueKey: marketId, + collateralAsset: { + address: market.params.collateralToken, + decimals: collateralToken.decimals, + priceUsd: collateralPriceUsd, + }, + loanAsset: { + address: market.params.loanToken, + decimals: loanToken.decimals, + priceUsd: ethPriceUsd, + spotPriceEth: 1 / ethPriceUsd, + }, + }, + }, + ], + }, + }, + }); + + await client.deal({ + erc20: loanToken.address, + account: liquidator.address, + amount: maxUint256, + }); + await client.approve({ + account: liquidator, + address: loanToken.address, + args: [morpho, maxUint256], + }); + await client.writeContract({ + account: liquidator, + address: morpho, + abi: blueAbi, + functionName: "liquidate", + args: [market.params, borrower.address, collateral - 1n, 0n, "0x"], + }); + + await syncTimestamp(client, await client.timestamp()); + + await fetchAccrualPosition(borrower.address as Address, marketId, client); + + mockOneInch(encoder, [ + { + srcAmount: 1n, + dstAmount: "100000000000000", + }, + ]); + mockParaSwap(encoder, [{ srcAmount: 1n, dstAmount: "100000000000000" }]); + + await check(encoder.address, client, client.account, [marketId]); + + const finalAccrualPosition = await fetchAccrualPosition( + borrower.address as Address, + marketId, + client, + ); + + expect(finalAccrualPosition.borrowShares).toEqual(0n); + expect(finalAccrualPosition.collateral).toEqual(0n); + }, + ); + // Cannot run concurrently because `fetch` is mocked globally. test.sequential( `should liquidate on a PT standard market before maturity`,