SDK Guide: Performing a Liquidation
The @townsq/mm-sdk is a lightweight JavaScript/TypeScript SDK designed for interacting with the liquidation functionality of the TownSquare Lending Protocol.
It enables developers to:
Identify under-collateralized (liquidatable) loans.
Compute loan health and repayment requirements.
Trigger on-chain liquidation transactions.
Note: This SDK currently focuses only on liquidation. Support for borrowing, supplying, and other protocol actions will be added in future releases.
Installation
npm install @townsq/mm-sdkor
yarn add @townsq/mm-sdkUnderstanding TownSquare Liquidation
Before diving into code, it’s important to understand how liquidation works in TownSquare.
1. What Makes a Loan Liquidatable?
A loan becomes liquidatable when its borrowed value (with interest) is greater than or equal to its collateral value.
Formally checked as:
dn.gte(
violatorLoanInfo.totalEffectiveBorrowBalanceValue,
violatorLoanInfo.totalEffectiveCollateralBalanceValue,
);If the above condition is true → the loan is unsafe → liquidators may step in.
2. Liquidator Requirements
To liquidate, a liquidator must already have a loan opened within the protocol.
This loan provides the funds for repayment.
The liquidator’s loan must hold enough collateral of the repayment token.
If insufficient, the liquidation transaction will fail.
3. Loan Types & Efficiencies
TownSquare supports three loan types, each with specific efficiency rules:
General Loans → Accept all supported tokens.
Stable Efficiency → Restricted to stable coins related assets
MON Efficiency Loans → You can only borrow or supply MON-related assets with this loan
When computing loan health or preparing a liquidation, always check the loanTypeId to apply the correct efficiency rules.
Reading Loans
When a liquidator service starts, it is recommended to read all existing loans into memory (or preferably into a persistent database).
This gives a snapshot of the system’s state.
From here, you will only need to process incremental updates from events, rather than repeatedly querying the entire chain.
Without this snapshot, you may miss loans that are already unhealthy at startup.
Updating Loan State with Events
The LoanManager contract is the central source of truth for loan changes. It emits events for every important action that affects loan health. Liquidators must listen to these events and update their internal state accordingly.
Events to track:
Deposit
{ loanId: string; poolId: number; amount: bigint; fAmount: bigint; }Indicates collateral was added to the loan.
Borrow
{ loanId: string; poolId: number; amount: bigint; isStableBorrow: boolean; stableInterestRate: bigint; }Indicates debt was added to the loan. If
isStableBorrowis true, the borrow position is fixed-rate.Withdraw
{ loanId: string; poolId: number; amount: bigint; fAmount: bigint; }Indicates collateral was removed from the loan.
Repay
{ loanId: string; poolId: number; principalPaid: bigint; interestPaid: bigint; excessPaid: bigint; }Indicates loan debt was reduced. This directly improves loan health.
Liquidate
{ violatorLoanId: string; liquidatorLoanId: string; colPoolId: number; borPoolId: number; repayBorrowBalance: bigint; liquidatorCollateralFAmount: bigint; reserveCollateralFAmount: bigint; }Indicates a liquidation occurred. The violator’s loan was partially or fully closed, and the liquidator’s loan received seized collateral.
When a liquidator service starts, it is recommended to read all existing loans into memory (or preferably into a persistent database).
This gives a snapshot of the system’s state.
From here, you will only need to process incremental updates from events, rather than repeatedly querying the entire chain.
Without this snapshot, you may miss loans that are already unhealthy at startup.
When an event is received (e.g., Borrow, Deposit, Repay, Withdraw, Liquidate), liquidators should recompute the full loan state before persisting it in their database. This ensures loan health metrics are always accurate.
Example (computing loanInfo after receiving a loanId from an event):
const poolsInfo: Partial<Record<TSTokenId, PoolInfo>> = {};
await Promise.all(
Object.values(TESTNET_TS_TOKEN_ID).map(async (tsTokenId) => {
const poolInfo = await TSPool.read.poolInfo(tsTokenId);
poolsInfo[tsTokenId] = poolInfo;
}),
);
const loanId = event.args.violatorLoanId as LoanId;
const loanTypeMap = Object.entries(TESTNET_LOAN_TYPE_ID).reduce(
(acc, [key, value]) => {
acc[key as any] = value;
return acc;
},
{} as Record<number, LoanTypeId>,
);
try {
const oraclePrices = await TSOracle.read.oraclePrices();
const userLoans = await TSLoan.read.userLoans([loanId]);
const loan = userLoans?.get(loanId);
if (!loan) return;
const loanType = Object.values(loanTypeMap).find((type) => type === loan.loanTypeId);
if (!loanType || loanType === TESTNET_LOAN_TYPE_ID.DEPOSIT) return;
const loanTypeInfo = {
[loanType]: await TSLoan.read.loanTypeInfo(loanType),
};
const loanInfo = TSLoan.util.userLoansInfo(
userLoans,
poolsInfo,
loanTypeInfo,
oraclePrices,
)[loanId];
if (!loanInfo) return;
// At this point, `loanInfo` contains all computed fields needed
// to evaluate liquidation eligibility and can be stored in your DB.
} catch (e) {
console.error("Failed to compute loan info", e);
}Updating Loan State with Events
The LoanManager contract is the central source of truth for loan changes. It emits events for every important action that affects loan health. Liquidators must listen to these events and update their internal state accordingly.
Step-by-Step: Liquidate a Loan
1. Initialize the SDK and set network
import { TSCore, NetworkType, TS_CHAIN_ID } from 'townsq-mm-sdk';
TSCore.init({ network: NetworkType.TESTNET, provider: { evm: {} } });
TSCore.setNetwork(NetworkType.TESTNET);Set up your signer
import { createWalletClient, http } from 'viem';
import { CHAIN_VIEM } from '@townsq/mm-sdk';
const signer = createWalletClient({
chain: CHAIN_VIEM[TS_CHAIN_ID.MONAD_TESTNET],
transport: http(), // You can pass your rpc (optional)
});
TSCore.setTSsSigner({
signer,
tsChainId: TS_CHAIN_ID.MONAD_TESTNET,
});
Resolve Account ID from Address
import { TSAccount } from '@townsq/mm-sdk';
const accountId = await TSAccount.read.getAccountIdOfAddressOnChain(
'0xYourEvmAddressHere'
);
4. Check if the Loan is Liquidatable
import { TSLoan, TSPool, TSOracle, TESTNET_TS_TOKEN_ID, TESTNET_LOAN_TYPE_ID } from 'townsq-mm-sdk';
import * as dn from 'dnum';
const violatorLoanId = '0x...'; // loan ID to liquidate
// Fetch oracle price and violator loan
const [oraclePrices, userLoans] = await Promise.all([
TSOracle.read.oraclePrices(),
TSLoan.read.userLoans([violatorLoanId]),
]);
const poolsInfo = await Promise.all(
Object.values(TESTNET_TS_TOKEN_ID).map(async (tokenId) => ({
tokenId,
pool: await TSPool.read.poolInfo(tokenId),
}))
);
// Prepare loan info
const userGeneralLoansInfo = TSLoan.util.userLoansInfo(
userLoans,
Object.fromEntries(poolsInfo.map(({ tokenId, pool }) => [tokenId, pool])),
{
[TESTNET_LOAN_TYPE_ID.GENERAL]: await TSLoan.read.loanTypeInfo(TESTNET_LOAN_TYPE_ID.GENERAL),
},
oraclePrices
);
const loanInfo = userGeneralLoansInfo[violatorLoanId];
if (dn.lt(
loanInfo.totalEffectiveBorrowBalanceValue,
loanInfo.totalEffectiveCollateralBalanceValue
)) {
console.log("Loan is healthy — not eligible for liquidation.");
return;
}
5. Prepare Liquidation Transaction
Important: The liquidator’s loan must have sufficient collateral If the liquidator's loan doesn’t hold sufficient collateral to cover for the amount the liquidator wants to pay, the transaction will fail.
import { TSLoan, convertToGenericAddress, ChainType, parseUnits } from '@townsq/mm-sdk'
const prepareLiquidationCall = await TSLoan.prepare.liquidate(
accountId,
'0xYourLiquidatorLoanId',
violatorLoanId,
TESTNET_TS_TOKEN_ID.USDC, // Repay in USDC
TESTNET_TS_TOKEN_ID.WETH, // Receive WETH as collateral
parseUnits('100', 18), // Amount to repay
parseUnits('0', 18), // Minimum collateral to seize
convertToGenericAddress('0xYourAddressAssociatedToYourAccount', ChainType.EVM)
);
6. Execute the Liquidation
const result = await TSLoan.write.liquidate(accountId, prepareLiquidationCall);
console.log(`Transaction hash: ${result}`);Last updated