Understanding and Tracking Positions in Morpho
- This guide introduces you to the essential functionalities within Morpho that allow users and developers to track and understand their positions, offering insights into the mechanics of supply and borrow Annual Percentage Yield (APY), user assets, market totals, and health factors.
- Do not forget to have a look at the MorphoBalancesLib which is a library that allows you to accrue interest and update the assets values of a market. As those contracts are not deployed by the Morpho Association, you will find the offchain computation to do so in the TypeScript section below.
Why Tracking Positions?
Tracking financial positions in Morpho enables users to:
- Optimize Returns: Understanding supply and borrow APY helps users make informed decisions to maximize their investment returns.
- Market Exposure: By monitoring collateral and borrow balances, users can effectively manage their exposure to specific markets, ensuring they maintain healthy positions.
- Strategic Decisions: Access to detailed market data allows users to strategize their next moves based on the overall market supply and borrow status.
Importance of Accruing Interests
The scripts provided herein are designed to guide users on the proper logic for fetching
onchain data from Morpho. It is important to highlight the significance of accruing
interest when building a system that tracks positions. Accruing interest ensures
that the data retrieved is both current and accurate, considering that interest is
only accrued up to the last transaction. This necessity is addressed by the MorphoBalancesLib
library in the Solidity examples and by the accrueInterests
function implementation
in the TypeScript examples.
Features Overview
This section delves into the specific functionalities provided by Morpho to track positions:
supplyAPY
and borrowAPY
Calculate the supply and borrow APY for a given market, excluding potential rewards.
supplyAssetsUser
, collateralAssetsUser
& borrowAssetsUser
Determine the total supply, collateral, and borrow balances of a specific user in a market. These figures are critical for understanding one's standing and exposure in the market.
marketTotalSupply
& marketTotalBorrow
Reveal the total supply and borrow volumes in a specific market, providing a snapshot of the market's liquidity and borrowing activity.
userHealthFactor
Assess the health factor of a user's position in a specific market, a crucial measure to avoid liquidation and monitor one's position.
- Solidity (Foundry)
- TypeScript (ethers v6)
- TypeScript (viem v2)
Requirement
To deepen your understanding and explore more intricate examples, it is highly recommended to consult the README file of the Morpho Snippets. This resource is designed to complement the snippets provided here by offering detailed explanations and additional context for each example.
pragma solidity ^0.8.0;
import { IIrm } from "@morpho-blue/interfaces/IIrm.sol";
import {
Id,
IMorpho,
MarketParams,
Market,
} from "@morpho-blue/interfaces/IMorpho.sol";
import { MathLib } from "@morpho-blue/libraries/MathLib.sol";
import { MorphoBalancesLib } from "@morpho-blue/libraries/periphery/MorphoBalancesLib.sol";
import { MorphoStorageLib } from "@morpho-blue/libraries/periphery/MorphoStorageLib.sol";
/// @title Morpho Blue Snippets
/// @author Morpho Labs
/// @custom:contact security@morpho.org
/// @notice The Morpho Blue Snippets contract.
contract BlueSnippets {
using MathLib for uint256;
using MorphoBalancesLib for IMorpho;
/* IMMUTABLES */
IMorpho public immutable morpho;
/* CONSTRUCTOR */
/// @notice Constructs the contract.
/// @param morphoAddress The address of the Morpho Blue contract.
constructor(address morphoAddress) {
morpho = IMorpho(morphoAddress);
}
/* VIEW FUNCTIONS */
// INFORMATIONAL: No 'Total Supply' and no 'Total Borrow' functions to calculate on chain as there could be some
// weird oracles/markets created
/// @notice Calculates the supply APY (Annual Percentage Yield) for a given market.
/// @param marketParams The parameters of the market.
/// @param market The market for which the supply APY is being calculated.
/// @return supplyApy The calculated supply APY (scaled by WAD).
function supplyAPY(MarketParams memory marketParams, Market memory market)
public
view
returns (uint256 supplyApy)
{
(uint256 totalSupplyAssets,, uint256 totalBorrowAssets,) = morpho.expectedMarketBalances(marketParams);
if (marketParams.irm != address(0)) {
uint256 utilization = totalBorrowAssets == 0 ? 0 : totalBorrowAssets.wDivUp(totalSupplyAssets);
supplyApy = borrowAPY(marketParams, market).wMulDown(1 ether - market.fee).wMulDown(utilization);
}
}
/// @notice Calculates the borrow APY (Annual Percentage Yield) for a given market.
/// @param marketParams The parameters of the market.
/// @param market The state of the market.
/// @return borrowApy The calculated borrow APY (scaled by WAD).
function borrowAPY(MarketParams memory marketParams, Market memory market)
public
view
returns (uint256 borrowApy)
{
if (marketParams.irm != address(0)) {
borrowApy = IIrm(marketParams.irm).borrowRateView(marketParams, market).wTaylorCompounded(365 days);
}
}
/// @notice Calculates the total supply balance of a given user in a specific market.
/// @param marketParams The parameters of the market.
/// @param user The address of the user whose supply balance is being calculated.
/// @return totalSupplyAssets The calculated total supply balance.
function supplyAssetsUser(MarketParams memory marketParams, address user)
public
view
returns (uint256 totalSupplyAssets)
{
totalSupplyAssets = morpho.expectedSupplyAssets(marketParams, user);
}
/// @notice Calculates the total collateral balance of a given user in a specific market.
/// @dev It uses extSloads to load only one storage slot of the Position struct and save gas.
/// @param marketId The identifier of the market.
/// @param user The address of the user whose collateral balance is being calculated.
/// @return totalCollateralAssets The calculated total collateral balance.
function collateralAssetsUser(Id marketId, address user) public view returns (uint256 totalCollateralAssets) {
bytes32[] memory slots = new bytes32[](1);
slots[0] = MorphoStorageLib.positionBorrowSharesAndCollateralSlot(marketId, user);
bytes32[] memory values = morpho.extSloads(slots);
totalCollateralAssets = uint256(values[0] >> 128);
}
/// @notice Calculates the total borrow balance of a given user in a specific market.
/// @param marketParams The parameters of the market.
/// @param user The address of the user whose borrow balance is being calculated.
/// @return totalBorrowAssets The calculated total borrow balance.
function borrowAssetsUser(MarketParams memory marketParams, address user)
public
view
returns (uint256 totalBorrowAssets)
{
totalBorrowAssets = morpho.expectedBorrowAssets(marketParams, user);
}
/// @notice Calculates the total supply of assets in a specific market.
/// @param marketParams The parameters of the market.
/// @return totalSupplyAssets The calculated total supply of assets.
function marketTotalSupply(MarketParams memory marketParams) public view returns (uint256 totalSupplyAssets) {
totalSupplyAssets = morpho.expectedTotalSupplyAssets(marketParams);
}
/// @notice Calculates the total borrow of assets in a specific market.
/// @param marketParams The parameters of the market.
/// @return totalBorrowAssets The calculated total borrow of assets.
function marketTotalBorrow(MarketParams memory marketParams) public view returns (uint256 totalBorrowAssets) {
totalBorrowAssets = morpho.expectedTotalBorrowAssets(marketParams);
}
/// @notice Calculates the health factor of a user in a specific market.
/// @param marketParams The parameters of the market.
/// @param id The identifier of the market.
/// @param user The address of the user whose health factor is being calculated.
/// @return healthFactor The calculated health factor.
function userHealthFactor(MarketParams memory marketParams, Id id, address user)
public
view
returns (uint256 healthFactor)
{
uint256 collateralPrice = IOracle(marketParams.oracle).price();
uint256 collateral = morpho.collateral(id, user);
uint256 borrowed = morpho.expectedBorrowAssets(marketParams, user);
uint256 maxBorrow = collateral.mulDivDown(collateralPrice, ORACLE_PRICE_SCALE).wMulDown(marketParams.lltv);
if (borrowed == 0) return type(uint256).max;
healthFactor = maxBorrow.wDivDown(borrowed);
}
}
Steps to execute on your local machine
To execute the TypeScript examples on your local machine, follow these straightforward steps:-
Setup: Begin by copying the code snippet below. Open your favorite code editor and paste the snippet into a new file named
track-positions.ts
. -
Configuration: Ensure you have a valid
RPC_URL
set within the script. This URL connects your script to the Ethereum blockchain. -
Customization: Modify the
marketIds
anduser
address variables within the script according to your specific use case. These adjustments allow you to fetch data relevant to your interests. -
Execution: Install the necessary dependencies and run the script by executing the following commands in your terminal:
yarn init
yarn add ethers ethers-types
ts-node track-positions.ts -
Observation: Upon successful execution, the script will output data similar to the following example in your console. This data represents the fetched market positions, offering insights into supply and borrow dynamics, APYs, user assets, and health factors:
MarketId: 0xc54d7acf14de29e0e5527cabd7a576506870346a78a11a6762e2cca66322ec41
Total Supply Assets: 2144635243926164023670n
Total Borrow Assets: 1930015732891965024460n
Supply APY: 20591744030290918n
Borrow APY: 22881564760664107n
Supply Assets User: 0n
Collateral Asset User: 964626319984916716118n
Borrow Assets User: 875260242333630105121n
Health Factor: 1.227528211225863131
Is Healthy: true
MarketId: 0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc
Total Supply Assets: 25223784287390n
Total Borrow Assets: 23305270757731n
Supply APY: 73572827396353019n
Borrow APY: 79629417179947192n
Supply Assets User: 0n
Collateral Asset User: 0n
Borrow Assets User: 0n
Health Factor: 115792089237316195423570985008687907853269984665640564039457.584007913129639935
Is Healthy: true
import {
formatUnits,
getDefaultProvider,
isAddress,
Provider,
ZeroAddress,
} from "ethers";
import {
MorphoBlue,
MorphoBlue__factory,
BlueOracle__factory,
BlueIrm__factory,
} from "ethers-types";
const RPC_URL =
"https://eth-mainnet.g.alchemy.com/v2/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
/// -----------------------------------------------
/// -------------------- TYPES --------------------
/// -----------------------------------------------
type MarketState = {
totalSupplyAssets: bigint;
totalSupplyShares: bigint;
totalBorrowAssets: bigint;
totalBorrowShares: bigint;
lastUpdate: bigint;
fee: bigint;
};
type MarketParams = {
loanToken: string;
collateralToken: string;
oracle: string;
irm: string;
lltv: bigint;
};
type PositionUser = {
supplyShares: bigint;
borrowShares: bigint;
collateral: bigint;
};
interface Contracts {
morphoBlue: MorphoBlue;
}
/// ---------------------------------------------------
/// -------------------- CONSTANTS --------------------
/// ---------------------------------------------------
// User address to track. This should be replaced with the target address you're interested in.
const user = "0xCDB238D68d8Da74487711bC1F8f13f3d00667D1A";
// Market IDs to monitor. These should be updated based on the markets you wish to track.
const wstethWethChainlinkAdaptiveCurveIRM945 =
"0xc54d7acf14de29e0e5527cabd7a576506870346a78a11a6762e2cca66322ec41";
const wstethUsdcChainlinkAdaptiveCurveIRM860 =
"0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc";
const whitelistedIds = [
wstethWethChainlinkAdaptiveCurveIRM945,
wstethUsdcChainlinkAdaptiveCurveIRM860,
];
// Main contract address for MorphoBlue. Update this value according to the deployed contract.
const MORPHO_ADDRESS = "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb";
const IRM_ADDRESS = "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC";
const pow10 = (exponant: bigint | number) => 10n ** BigInt(exponant);
const ORACLE_PRICE_SCALE = pow10(36);
const WAD = pow10(18);
const SECONDS_PER_YEAR = 3600 * 24 * 365;
const VIRTUAL_ASSETS = 1n;
const VIRTUAL_SHARES = 10n ** 6n;
const MAX_UINT256 = BigInt(
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
);
/// -----------------------------------------------
/// -------------------- UTILS --------------------
/// -----------------------------------------------
const wMulDown = (x: bigint, y: bigint): bigint => mulDivDown(x, y, WAD);
const wDivDown = (x: bigint, y: bigint): bigint => mulDivDown(x, WAD, y);
const wDivUp = (x: bigint, y: bigint): bigint => mulDivUp(x, WAD, y);
const mulDivDown = (x: bigint, y: bigint, d: bigint): bigint => (x * y) / d;
const mulDivUp = (x: bigint, y: bigint, d: bigint): bigint =>
(x * y + (d - 1n)) / d;
const wTaylorCompounded = (x: bigint, n: bigint): bigint => {
const firstTerm = x * n;
const secondTerm = mulDivDown(firstTerm, firstTerm, 2n * WAD);
const thirdTerm = mulDivDown(secondTerm, firstTerm, 3n * WAD);
return firstTerm + secondTerm + thirdTerm;
};
export const toAssetsDown = (
shares: bigint,
totalAssets: bigint,
totalShares: bigint
): bigint => {
return mulDivDown(
shares,
totalAssets + VIRTUAL_ASSETS,
totalShares + VIRTUAL_SHARES
);
};
/// @dev Calculates the value of `shares` quoted in assets, rounding down.
const toSharesDown = (
assets: bigint,
totalAssets: bigint,
totalShares: bigint
): bigint => {
return mulDivDown(
assets,
totalShares + VIRTUAL_SHARES,
totalAssets + VIRTUAL_ASSETS
);
};
/// @dev Calculates the value of `shares` quoted in assets, rounding up.
const toAssetsUp = (
shares: bigint,
totalAssets: bigint,
totalShares: bigint
): bigint => {
return mulDivUp(
shares,
totalAssets + VIRTUAL_ASSETS,
totalShares + VIRTUAL_SHARES
);
};
/// ---------------------------------------------------
/// -------------------- FUNCTIONS --------------------
/// ---------------------------------------------------
const accrueInterests = (
lastBlockTimestamp: bigint,
marketState: MarketState,
borrowRate: bigint
) => {
const elapsed = lastBlockTimestamp - marketState.lastUpdate;
// Early return if no time has elapsed since the last update
if (elapsed === 0n || marketState.totalBorrowAssets === 0n) {
return marketState;
}
// Calculate interest
const interest = wMulDown(
marketState.totalBorrowAssets,
wTaylorCompounded(borrowRate, elapsed)
);
// Prepare updated market state with new totals
const marketWithNewTotal = {
...marketState,
totalBorrowAssets: marketState.totalBorrowAssets + interest,
totalSupplyAssets: marketState.totalSupplyAssets + interest,
};
// Early return if there's no fee
if (marketWithNewTotal.fee === 0n) {
return marketWithNewTotal;
}
// Calculate fee and feeShares if the fee is not zero
const feeAmount = wMulDown(interest, marketWithNewTotal.fee);
const feeShares = toSharesDown(
feeAmount,
marketWithNewTotal.totalSupplyAssets - feeAmount,
marketWithNewTotal.totalSupplyShares
);
// Return final market state including feeShares
return {
...marketWithNewTotal,
totalSupplyShares: marketWithNewTotal.totalSupplyShares + feeShares,
};
};
const getProvider = () => {
const endpoint = RPC_URL;
if (!endpoint) {
throw Error("RPC_URL not set. Exiting…");
}
return getDefaultProvider(endpoint);
};
const morphoContracts = async (provider?: Provider) => {
if (!isAddress(MORPHO_ADDRESS)) throw new Error("MORPHO_ADDRESS unset");
const morphoBlue = MorphoBlue__factory.connect(
MORPHO_ADDRESS,
provider ?? getProvider()
);
return { morphoBlue };
};
/**
* Fetches and calculates user position data for a given market.
* @param {Contracts} contracts - The initialized contract instances.
* @param {string} id - The market ID to fetch data for.
* @param {string} user - The user address to fetch position for.
* @param {Provider} [provider] - The ethers provider.
* Returns total supply assets, total borrow assets, user's supply assets, user's collateral assets, user's borrow assets, health factor, health status, supply APY and borrow APY.
*/
const fetchData = async (
{ morphoBlue }: Contracts,
id: string,
user: string,
provider?: Provider
) => {
provider ??= getProvider();
const block = await provider.getBlock("latest");
const [marketParams_, marketState_, position_] = await Promise.all([
morphoBlue.idToMarketParams(id),
morphoBlue.market(id),
morphoBlue.position(id, user),
]);
const marketParams: MarketParams = {
loanToken: marketParams_.loanToken,
collateralToken: marketParams_.collateralToken,
oracle: marketParams_.oracle,
irm: marketParams_.irm,
lltv: marketParams_.lltv,
};
let marketState: MarketState = {
totalSupplyAssets: marketState_.totalSupplyAssets,
totalSupplyShares: marketState_.totalSupplyShares,
totalBorrowAssets: marketState_.totalBorrowAssets,
totalBorrowShares: marketState_.totalBorrowShares,
lastUpdate: marketState_.lastUpdate,
fee: marketState_.fee,
};
const position: PositionUser = {
supplyShares: position_.supplyShares,
borrowShares: position_.borrowShares,
collateral: position_.collateral,
};
const irm = BlueIrm__factory.connect(IRM_ADDRESS, provider);
const borrowRate =
IRM_ADDRESS !== ZeroAddress
? await irm.borrowRateView(marketParams, marketState)
: 0n;
marketState = accrueInterests(
BigInt(block!.timestamp),
marketState,
borrowRate
);
const borrowAssetsUser = toAssetsUp(
position.borrowShares,
marketState.totalBorrowAssets,
marketState.totalBorrowShares
);
const supplyAssetsUser = toAssetsDown(
position.supplyShares,
marketState.totalSupplyAssets,
marketState.totalSupplyShares
);
const collateralAssetUser = position.collateral;
const oracle = BlueOracle__factory.connect(marketParams_.oracle, provider);
const collateralPrice = await oracle.price();
const maxBorrow = wMulDown(
mulDivDown(position.collateral, collateralPrice, ORACLE_PRICE_SCALE),
marketParams_.lltv
);
const isHealthy = maxBorrow >= borrowAssetsUser;
const healthFactor =
borrowAssetsUser === 0n
? MAX_UINT256
: wDivDown(maxBorrow, borrowAssetsUser);
const borrowAPY = wTaylorCompounded(borrowRate, BigInt(SECONDS_PER_YEAR));
let supplyAPY = 0n;
const marketTotalBorrow = marketState.totalBorrowAssets;
const marketTotalSupply = marketState.totalSupplyAssets;
if (marketTotalSupply !== 0n) {
const utilization = wDivUp(marketTotalBorrow, marketTotalSupply);
supplyAPY = wMulDown(
wMulDown(borrowAPY, WAD - marketState.fee),
utilization
);
}
return {
marketTotalSupply,
marketTotalBorrow,
supplyAssetsUser,
collateralAssetUser,
borrowAssetsUser,
healthFactor,
isHealthy,
supplyAPY,
borrowAPY,
};
};
/// ----------------------------------------------
/// -------------------- MAIN --------------------
/// ----------------------------------------------
const run = async () => {
const provider = getProvider();
const contracts = await morphoContracts(provider);
const results = await Promise.allSettled(
whitelistedIds.map(async (market) => {
try {
const {
marketTotalSupply,
marketTotalBorrow,
supplyAssetsUser,
collateralAssetUser,
borrowAssetsUser,
healthFactor,
isHealthy,
supplyAPY,
borrowAPY,
} = await fetchData(contracts, market, user, provider);
console.log("MarketId:", market);
console.log("Total Supply Assets: ", marketTotalSupply);
console.log("Total Borrow Assets: ", marketTotalBorrow);
console.log("Supply APY: ", supplyAPY);
console.log("Borrow APY: ", borrowAPY);
console.log("Supply Assets User: ", supplyAssetsUser);
console.log("Collateral Asset User:", collateralAssetUser);
console.log("Borrow Assets User: ", borrowAssetsUser);
console.log(
"Health Factor: ",
formatUnits(healthFactor.toString())
);
console.log("Is Healthy: ", isHealthy);
} catch (error) {
console.error(`Error fetching data for marketId: ${market}`, error);
return null;
}
})
);
};
run().then(() => process.exit(0));
Steps to execute on your local machine
To execute the TypeScript examples on your local machine, follow these straightforward steps:-
Setup: Begin by copying the code snippet below. Open your favorite code editor and paste the snippet into a new file named
track-positions-viem.ts
. -
Configuration: Ensure you have a valid
RPC_URL
set within the script. This URL connects your script to the Ethereum blockchain. -
Customization: Modify the
marketIds
anduser
address variables within the script according to your specific use case. These adjustments allow you to fetch data relevant to your interests. -
Execution: Install the necessary dependencies and run the script by executing the following commands in your terminal:
yarn init
yarn add
ts-node track-positions-viem.ts -
Observation: Upon successful execution, the script will output data similar to the following example in your console. This data represents the fetched market positions, offering insights into supply and borrow dynamics, APYs, user assets, and health factors:
Total Supply Assets: 2144635243926164023670n
Total Borrow Assets: 1930015732891965024460n
Supply APY: 20591744030290918n
Borrow APY: 22881564760664107n
Supply Assets User: 0n
Collateral Asset User: 964626319984916716118n
Borrow Assets User: 875260242333630105121n
Health Factor: 1.227528211225863131
Is Healthy: true
MarketId: 0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc
Total Supply Assets: 25223784287390n
Total Borrow Assets: 23305270757731n
Supply APY: 73572827396353019n
Borrow APY: 79629417179947192n
Supply Assets User: 0n
Collateral Asset User: 0n
Borrow Assets User: 0n
Health Factor: 115792089237316195423570985008687907853269984665640564039457.584007913129639935
Is Healthy: true
// Import necessary functions and types from viem
import {
createPublicClient,
http,
formatUnits,
isAddress,
zeroAddress,
getContract,
PublicClient,
} from "viem";
import { mainnet } from "viem/chains";
import {
blueAbi as MorphoBlueAbi,
blueOracleAbi as BlueOracleAbi,
adaptiveCurveIrmAbi as BlueIrmAbi,
} from "../abis/abis";
const RPC_URL =
"https://eth-mainnet.g.alchemy.com/v2/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
/// -----------------------------------------------
/// -------------------- TYPES --------------------
/// -----------------------------------------------
type MarketState = {
totalSupplyAssets: bigint;
totalSupplyShares: bigint;
totalBorrowAssets: bigint;
totalBorrowShares: bigint;
lastUpdate: bigint;
fee: bigint;
};
type MarketParams = {
loanToken: `0x${string}`;
collateralToken: `0x${string}`;
oracle: `0x${string}`;
irm: `0x${string}`;
lltv: bigint;
};
type PositionUser = {
supplyShares: bigint;
borrowShares: bigint;
collateral: bigint;
};
interface Contracts {
morphoBlue: any; // Adjusted to 'any' or appropriate type from viem
}
/// ---------------------------------------------------
/// -------------------- CONSTANTS --------------------
/// ---------------------------------------------------
// User address to track. This should be replaced with the target address you're interested in.
const user = "0xCDB238D68d8Da74487711bC1F8f13f3d00667D1A";
// Market IDs to monitor. These should be updated based on the markets you wish to track.
const wstethWethChainlinkAdaptiveCurveIRM945 =
"0xc54d7acf14de29e0e5527cabd7a576506870346a78a11a6762e2cca66322ec41";
const wstethUsdcChainlinkAdaptiveCurveIRM860 =
"0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc";
const whitelistedIds = [
wstethWethChainlinkAdaptiveCurveIRM945,
wstethUsdcChainlinkAdaptiveCurveIRM860,
];
// Main contract address for MorphoBlue. Update this value according to the deployed contract.
const MORPHO_ADDRESS = "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb";
const IRM_ADDRESS = "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC";
const pow10 = (exponent: bigint | number) => 10n ** BigInt(exponent);
const ORACLE_PRICE_SCALE = pow10(36);
const WAD = pow10(18);
const SECONDS_PER_YEAR = 3600 * 24 * 365;
const VIRTUAL_ASSETS = 1n;
const VIRTUAL_SHARES = 10n ** 6n;
const MAX_UINT256 = BigInt(
"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
);
/// -----------------------------------------------
/// -------------------- UTILS --------------------
/// -----------------------------------------------
const wMulDown = (x: bigint, y: bigint): bigint => mulDivDown(x, y, WAD);
const wDivDown = (x: bigint, y: bigint): bigint => mulDivDown(x, WAD, y);
const wDivUp = (x: bigint, y: bigint): bigint => mulDivUp(x, WAD, y);
const mulDivDown = (x: bigint, y: bigint, d: bigint): bigint => (x * y) / d;
const mulDivUp = (x: bigint, y: bigint, d: bigint): bigint =>
(x * y + (d - 1n)) / d;
const wTaylorCompounded = (x: bigint, n: bigint): bigint => {
const firstTerm = x * n;
const secondTerm = mulDivDown(firstTerm, firstTerm, 2n * WAD);
const thirdTerm = mulDivDown(secondTerm, firstTerm, 3n * WAD);
return firstTerm + secondTerm + thirdTerm;
};
export const toAssetsDown = (
shares: bigint,
totalAssets: bigint,
totalShares: bigint
): bigint => {
return mulDivDown(
shares,
totalAssets + VIRTUAL_ASSETS,
totalShares + VIRTUAL_SHARES
);
};
/// @dev Calculates the value of `shares` quoted in assets, rounding down.
const toSharesDown = (
assets: bigint,
totalAssets: bigint,
totalShares: bigint
): bigint => {
return mulDivDown(
assets,
totalShares + VIRTUAL_SHARES,
totalAssets + VIRTUAL_ASSETS
);
};
/// @dev Calculates the value of `shares` quoted in assets, rounding up.
const toAssetsUp = (
shares: bigint,
totalAssets: bigint,
totalShares: bigint
): bigint => {
return mulDivUp(
shares,
totalAssets + VIRTUAL_ASSETS,
totalShares + VIRTUAL_SHARES
);
};
/// ---------------------------------------------------
/// -------------------- FUNCTIONS --------------------
/// ---------------------------------------------------
const accrueInterests = (
lastBlockTimestamp: bigint,
marketState: MarketState,
borrowRate: bigint
) => {
const elapsed = lastBlockTimestamp - marketState.lastUpdate;
// Early return if no time has elapsed since the last update
if (elapsed === 0n || marketState.totalBorrowAssets === 0n) {
return marketState;
}
// Calculate interest
const interest = wMulDown(
marketState.totalBorrowAssets,
wTaylorCompounded(borrowRate, elapsed)
);
// Prepare updated market state with new totals
const marketWithNewTotal = {
...marketState,
totalBorrowAssets: marketState.totalBorrowAssets + interest,
totalSupplyAssets: marketState.totalSupplyAssets + interest,
};
// Early return if there's no fee
if (marketWithNewTotal.fee === 0n) {
return marketWithNewTotal;
}
// Calculate fee and feeShares if the fee is not zero
const feeAmount = wMulDown(interest, marketWithNewTotal.fee);
const feeShares = toSharesDown(
feeAmount,
marketWithNewTotal.totalSupplyAssets - feeAmount,
marketWithNewTotal.totalSupplyShares
);
// Return final market state including feeShares
return {
...marketWithNewTotal,
totalSupplyShares: marketWithNewTotal.totalSupplyShares + feeShares,
};
};
const getPublicClient = (): PublicClient => {
const endpoint = RPC_URL;
if (!endpoint) {
throw Error("RPC_URL not set. Exiting…");
}
return createPublicClient({
chain: mainnet,
transport: http(endpoint),
});
};
const morphoContracts = async (
publicClient: PublicClient
): Promise<Contracts> => {
if (!isAddress(MORPHO_ADDRESS)) throw new Error("MORPHO_ADDRESS unset");
const morphoBlue = getContract({
address: MORPHO_ADDRESS,
abi: MorphoBlueAbi,
client: publicClient,
});
return { morphoBlue };
};
/**
* Fetches and calculates user position data for a given market.
* @param {Contracts} contracts - The initialized contract instances.
* @param {string} id - The market ID to fetch data for.
* @param {string} user - The user address to fetch position for.
* @param {PublicClient} publicClient - The viem public client.
* Returns total supply assets, total borrow assets, user's supply assets, user's collateral assets, user's borrow assets, health factor, health status, supply APY and borrow APY.
*/
const fetchData = async (
{ morphoBlue }: Contracts,
id: string,
user: string,
publicClient: PublicClient
) => {
const block = await publicClient.getBlock({ blockTag: "latest" });
const [marketParams_, marketState_, position_] = await Promise.all([
morphoBlue.read.idToMarketParams([id]),
morphoBlue.read.market([id]),
morphoBlue.read.position([id, user]),
]);
const marketParams: MarketParams = {
loanToken: marketParams_[0],
collateralToken: marketParams_[1],
oracle: marketParams_[2],
irm: marketParams_[3],
lltv: marketParams_[4],
};
let marketState: MarketState = {
totalSupplyAssets: marketState_[0],
totalSupplyShares: marketState_[1],
totalBorrowAssets: marketState_[2],
totalBorrowShares: marketState_[3],
lastUpdate: marketState_[4],
fee: marketState_[5],
};
const position: PositionUser = {
supplyShares: position_[0],
borrowShares: position_[1],
collateral: position_[2],
};
const irm = getContract({
address: IRM_ADDRESS,
abi: BlueIrmAbi,
client: publicClient,
});
const borrowRate =
(IRM_ADDRESS as `0x${string}`) !== (zeroAddress as `0x${string}`)
? await irm.read.borrowRateView([marketParams, marketState])
: 0n;
marketState = accrueInterests(
BigInt(block.timestamp),
marketState,
borrowRate
);
const borrowAssetsUser = toAssetsUp(
position.borrowShares,
marketState.totalBorrowAssets,
marketState.totalBorrowShares
);
const supplyAssetsUser = toAssetsDown(
position.supplyShares,
marketState.totalSupplyAssets,
marketState.totalSupplyShares
);
const collateralAssetUser = position.collateral;
let collateralPrice: bigint = 0n;
if (marketParams.oracle !== zeroAddress) {
const oracle = getContract({
address: marketParams.oracle,
abi: BlueOracleAbi,
client: publicClient,
});
try {
collateralPrice = await oracle.read.price();
} catch (error) {
console.error("Error reading price from oracle:", error);
}
} else {
console.warn("Oracle address is zero. Using collateral price of 0.");
}
const maxBorrow = wMulDown(
mulDivDown(position.collateral, collateralPrice, ORACLE_PRICE_SCALE),
marketParams.lltv
);
const isHealthy = maxBorrow >= borrowAssetsUser;
const healthFactor =
borrowAssetsUser === 0n
? MAX_UINT256
: wDivDown(maxBorrow, borrowAssetsUser);
const borrowAPY = wTaylorCompounded(borrowRate, BigInt(SECONDS_PER_YEAR));
let supplyAPY = 0n;
const marketTotalBorrow = marketState.totalBorrowAssets;
const marketTotalSupply = marketState.totalSupplyAssets;
if (marketTotalSupply !== 0n) {
const utilization = wDivUp(marketTotalBorrow, marketTotalSupply);
supplyAPY = wMulDown(
wMulDown(borrowAPY, WAD - marketState.fee),
utilization
);
}
return {
marketTotalSupply,
marketTotalBorrow,
supplyAssetsUser,
collateralAssetUser,
borrowAssetsUser,
healthFactor,
isHealthy,
supplyAPY,
borrowAPY,
};
};
/// ----------------------------------------------
/// -------------------- MAIN --------------------
/// ----------------------------------------------
const run = async () => {
const publicClient = getPublicClient();
const contracts = await morphoContracts(publicClient);
const results = await Promise.allSettled(
whitelistedIds.map(async (market) => {
try {
const {
marketTotalSupply,
marketTotalBorrow,
supplyAssetsUser,
collateralAssetUser,
borrowAssetsUser,
healthFactor,
isHealthy,
supplyAPY,
borrowAPY,
} = await fetchData(contracts, market, user, publicClient);
console.log("MarketId:", market);
console.log("Total Supply Assets: ", marketTotalSupply);
console.log("Total Borrow Assets: ", marketTotalBorrow);
console.log("Supply APY: ", supplyAPY);
console.log("Borrow APY: ", borrowAPY);
console.log("Supply Assets User: ", supplyAssetsUser);
console.log("Collateral Asset User:", collateralAssetUser);
console.log("Borrow Assets User: ", borrowAssetsUser);
console.log("Health Factor: ", formatUnits(healthFactor, 18));
console.log("Is Healthy: ", isHealthy);
} catch (error) {
console.error(`Error fetching data for marketId: ${market}`, error);
return null;
}
})
);
};
run().then(() => process.exit(0));