import { BigNumberish, Provider, ZeroAddress, toBigInt } from "ethers"; import { AdaptiveCurveIrm__factory, BlueOracle__factory, MorphoBlue__factory, } from "ethers-types"; import { ViewOverrides } from "ethers-types/dist/common"; import { getChainAddresses } from "../addresses"; import { ChainId, ChainUtils } from "../chain"; import { InvalidInterestAccrualError } from "../errors"; import { AdaptiveCurveIrmLib, RoundingDirection } from "../maths"; import { MarketId } from "../types"; import { MarketConfig } from "./MarketConfig"; import { MarketUtils } from "./MarketUtils"; export enum CapacityLimitReason { liquidity = "Liquidity", balance = "Balance", position = "Position", collateral = "Collateral", cap = "Cap", } export interface CapacityLimit { value: bigint; limiter: CapacityLimitReason; } export interface MaxPositionCapacities { supply: CapacityLimit; withdraw: CapacityLimit; borrow: CapacityLimit; repay: CapacityLimit; supplyCollateral: CapacityLimit; withdrawCollateral: CapacityLimit; } export interface InputMarket { config: MarketConfig; totalSupplyAssets: BigNumberish; totalBorrowAssets: BigNumberish; totalSupplyShares: BigNumberish; totalBorrowShares: BigNumberish; lastUpdate: BigNumberish; fee: BigNumberish; price: BigNumberish; rateAtTarget?: BigNumberish; } export class Market implements InputMarket { static async fetchFromId( id: MarketId, runner: { provider: Provider }, { chainId, overrides = {}, }: { chainId?: ChainId; overrides?: ViewOverrides } = {} ) { chainId ??= ChainUtils.parseSupportedChainId( (await runner.provider.getNetwork()).chainId ); const config = await MarketConfig.fetch(id, runner, chainId); return Market.fetchFromConfig(config, runner, { chainId, overrides }); } static async fetchFromConfig( config: MarketConfig, runner: { provider: Provider }, { chainId, overrides = {}, }: { chainId?: ChainId; overrides?: ViewOverrides } = {} ) { chainId ??= ChainUtils.parseSupportedChainId( (await runner.provider.getNetwork()).chainId ); const { morpho, adaptiveCurveIrm } = getChainAddresses(chainId); const [ { totalSupplyAssets, totalSupplyShares, totalBorrowShares, totalBorrowAssets, lastUpdate, fee, }, price, rateAtTarget, ] = await Promise.all([ MorphoBlue__factory.connect(morpho, runner).market(config.id, overrides), config.oracle !== ZeroAddress ? BlueOracle__factory.connect(config.oracle, runner).price(overrides) : 0n, config.irm === adaptiveCurveIrm ? await AdaptiveCurveIrm__factory.connect( config.irm, runner ).rateAtTarget(config.id, overrides) : undefined, ]); return new Market({ config, totalSupplyAssets, totalBorrowAssets, totalSupplyShares, totalBorrowShares, lastUpdate, fee, price, rateAtTarget, }); } /** * The market's config. */ public readonly config: MarketConfig; /** * The amount of loan assets supplied in total on the market. */ public totalSupplyAssets: bigint; /** * The amount of loan assets supplied in total on the market. */ public totalBorrowAssets: bigint; /** * The amount of loan assets supplied in total on the market. */ public totalSupplyShares: bigint; /** * The amount of loan assets supplied in total on the market. */ public totalBorrowShares: bigint; /** * The block timestamp (in __seconds__) when the interest was last accrued. */ public lastUpdate: bigint; /** * The fee percentage of the market, scaled by WAD. */ public fee: bigint; /** * The price as returned by the market's oracle. */ public price: bigint; /** * If the market uses the Adaptive Curve IRM, the rate at target utilization. * Undefined otherwise. */ public rateAtTarget?: bigint; constructor({ config, totalSupplyAssets, totalBorrowAssets, totalSupplyShares, totalBorrowShares, lastUpdate, fee, price, rateAtTarget, }: InputMarket) { this.config = config; this.totalSupplyAssets = toBigInt(totalSupplyAssets); this.totalBorrowAssets = toBigInt(totalBorrowAssets); this.totalSupplyShares = toBigInt(totalSupplyShares); this.totalBorrowShares = toBigInt(totalBorrowShares); this.lastUpdate = toBigInt(lastUpdate); this.fee = toBigInt(fee); this.price = toBigInt(price); if (rateAtTarget != null) this.rateAtTarget = toBigInt(rateAtTarget); } get id() { return this.config.id; } get isIdle() { return this.config.collateralToken === ZeroAddress; } /** * @warning Cannot be used to calculate the liquidity available inside a callback, * because the balance of Blue may be lower than the market's liquidity due to assets being transferred out prior to the callback. */ get liquidity() { return this.totalSupplyAssets - this.totalBorrowAssets; } get utilization() { return MarketUtils.getUtilization(this); } get apyAtTarget() { if (this.rateAtTarget == null) return; return MarketUtils.getApy(this.rateAtTarget); } get supplyRate() { return MarketUtils.getSupplyRate(this.borrowRate, this); } get borrowRate() { if (this.rateAtTarget == null) return 0n; return AdaptiveCurveIrmLib.getBorrowRate( this.utilization, this.rateAtTarget, 0n ).avgBorrowRate; } get supplyApy() { return MarketUtils.getApy(this.supplyRate); } get borrowApy() { return MarketUtils.getApy(this.borrowRate); } public accrueInterest(timestamp: BigNumberish) { timestamp = toBigInt(timestamp); const elapsed = timestamp - this.lastUpdate; if (elapsed < 0n) throw new InvalidInterestAccrualError( this.id, timestamp, this.lastUpdate ); let borrowRate = 0n; let { rateAtTarget } = this; if (this.rateAtTarget != null) { const { avgBorrowRate, endRateAtTarget } = AdaptiveCurveIrmLib.getBorrowRate( this.utilization, this.rateAtTarget, elapsed ); borrowRate = avgBorrowRate; rateAtTarget = endRateAtTarget; } const { interest, feeShares } = MarketUtils.getAccruedInterest( borrowRate, this, elapsed ); return new Market({ ...this, totalSupplyAssets: this.totalSupplyAssets + interest, totalBorrowAssets: this.totalBorrowAssets + interest, totalSupplyShares: this.totalSupplyShares + feeShares, lastUpdate: timestamp, rateAtTarget, }); } public getLiquidityToUtilization(utilization: BigNumberish) { return MarketUtils.getLiquidityToUtilization(this, utilization); } public getCollateralValue(collateral: BigNumberish) { return MarketUtils.getCollateralValue(collateral, this); } public getMaxBorrowAssets(collateral: BigNumberish) { return MarketUtils.getMaxBorrowAssets(collateral, this, this.config); } public getMaxBorrowableAssets(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.getMaxBorrowableAssets(position, this, this.config); } public getLiquidationSeizedAssets(repaidShares: BigNumberish) { return MarketUtils.getLiquidationSeizedAssets( repaidShares, this, this.config ); } public getLiquidationRepaidShares(seizedAssets: BigNumberish) { return MarketUtils.getLiquidationRepaidShares( seizedAssets, this, this.config ); } public getSeizableCollateral(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.getSeizableCollateral(position, this, this.config); } public getWithdrawableCollateral(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.getWithdrawableCollateral(position, this, this.config); } public isHealthy(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.isHealthy(position, this, this.config); } public getLiquidationPrice(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.getLiquidationPrice(position, this, this.config); } public getPriceVariationToLiquidation(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.getPriceVariationToLiquidation( position, this, this.config ); } public getHealthFactor(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.getHealthFactor(position, this, this.config); } public getLtv(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.getLtv(position, this); } public getBorrowCapacityUsage(position: { collateral: BigNumberish; borrowShares: BigNumberish; }) { return MarketUtils.getBorrowCapacityUsage(position, this, this.config); } public toSupplyAssets(shares: BigNumberish, rounding?: RoundingDirection) { return MarketUtils.toSupplyAssets(shares, this, rounding); } public toSupplyShares(assets: BigNumberish, rounding?: RoundingDirection) { return MarketUtils.toSupplyShares(assets, this, rounding); } public toBorrowAssets(shares: BigNumberish, rounding?: RoundingDirection) { return MarketUtils.toBorrowAssets(shares, this, rounding); } public toBorrowShares(assets: BigNumberish, rounding?: RoundingDirection) { return MarketUtils.toBorrowShares(assets, this, rounding); } public getBorrowCapacityLimit(collateral: bigint): CapacityLimit { const maxBorrowableAssets = this.getMaxBorrowAssets(collateral); const { liquidity } = this; if (maxBorrowableAssets > liquidity) return { value: liquidity, limiter: CapacityLimitReason.liquidity, }; return { value: maxBorrowableAssets, limiter: CapacityLimitReason.collateral, }; } public getRepayCapacityLimit( borrowShares: bigint, loanTokenBalance: bigint ): CapacityLimit { const borrowAssets = this.toBorrowAssets(borrowShares); if (borrowAssets > loanTokenBalance) return { value: loanTokenBalance, limiter: CapacityLimitReason.balance, }; return { value: borrowAssets, limiter: CapacityLimitReason.position, }; } public getWithdrawCapacityLimit(supplyShares: bigint): CapacityLimit { const supplyAssets = this.toSupplyAssets(supplyShares); const { liquidity } = this; if (supplyAssets > liquidity) return { value: liquidity, limiter: CapacityLimitReason.liquidity, }; return { value: supplyAssets, limiter: CapacityLimitReason.position, }; } public getWithdrawCollateralCapacityLimit(position: { collateral: bigint; borrowShares: bigint; }): CapacityLimit { const withdrawableCollateral = this.getWithdrawableCollateral(position); if (position.collateral > withdrawableCollateral) return { value: withdrawableCollateral, limiter: CapacityLimitReason.collateral, }; return { value: position.collateral, limiter: CapacityLimitReason.position, }; } public getMaxCapacities( position: { collateral: bigint; supplyShares: bigint; borrowShares: bigint; }, loanTokenBalance: bigint, collateralTokenBalance: bigint ): MaxPositionCapacities { return { supply: { value: loanTokenBalance, limiter: CapacityLimitReason.balance, }, withdraw: this.getWithdrawCapacityLimit(position.supplyShares), borrow: this.getBorrowCapacityLimit(position.collateral), repay: this.getRepayCapacityLimit( position.borrowShares, loanTokenBalance ), supplyCollateral: { value: collateralTokenBalance, limiter: CapacityLimitReason.balance, }, withdrawCollateral: this.getWithdrawCollateralCapacityLimit(position), }; } }