Understanding and Tracking Positions in Morpho Blue
This guide introduces you to the essential functionalities within Morpho Blue 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.
Why Track Positions?
Tracking financial positions in Morpho Blue 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.
The scripts provided herein are designed to guide users on the proper logic for fetching onchain data from Morpho Blue. 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 Blue to track positions:
supplyAPY
& 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)
To deepen your understanding and explore more intricate examples, it is highly recommended to consult the README file of the Morpho Blue 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);
}
}
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: 13181840605118696942689n
Total Borrow Assets: 9606108045713298052231n
Supply APY: 11644086691428704n
Borrow APY: 15978426853848669n
Supply Assets User: 0n
Collateral Asset User: 398584498017683393941n
Borrow Assets User: 412899842468502667923n
Health Factor: 1.058373488593982618
Is Healthy: true
MarketId: 0xb323495f7e4148be5643a4ea4a8221eef163e4bccfdedc2a6f4696baacbc86cc
Total Supply Assets: 31279410364579n
Total Borrow Assets: 30460199919046n
Supply APY: 413141778728058186n
Borrow APY: 424253001291258906n
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 = "0x8b4943EBcf5F31923E0C409943d5FeF53c0C53CB";
// 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));