Manage protection programmatically

Learn how to view and manage positions and accounts programmatically on the Cozy protocol.

This guide illustrates how you can view and manage your protection positions programmatically using a script written in TypeScript. The tasks in this section assume that you are a participant in a protection market and have supplied collateral to purchase protection as described in Buy protection programmatically. If you haven't already done so, you should read the Buy protection programmatically guide or review the buy-protection.ts script before continuing with this guide.

All of the code snippets in this guide are from the manage-protection.ts script in the Cozy developer guides repository. See that repository for more context, definitions of helper methods used, and related information.

Everything in this guide assumes that you have experience with JavaScript, ethers.js, and Solidity.

Prepare to manage positions

The manage-protection.ts script includes code to set up a your environment and buy protection so that you have an account with positions to manage. Because those steps are covered in other guides, they aren't included in this guide. For managing positions, however, you need to specify how many decimals Cozy tokens have. You can add this information to the Environment Setup section of the code with a line like this:

manage-protection.ts
// All Cozy Tokens have 8 decimals, so we define this for convenience
const cTokenDecimals = 8;

After the Environment Setup instructions, the manage-protection.ts script includes a condensed version of the code for buying protection, which is covered in more detail in the buy-protection.ts script. You can view the condensed version of the code here.

View positions

As the last step of buying protection, you borrowed funds from a protection market. In this case, you have borrowed protected USDC from a Yearn protection market. Now, let's get an array of Cozy token (cToken) addresses for each market you've entered.

manage-protection.ts
// STEP 1: VIEWING POSITIONS
// To start, let's get an array of CToken addresses for each market
// we've entered. When you borrow from a market, you are automatically
// entered into that market
const ourAddress = signer.address; // shorthand, for convenience
// Get an array of CozyToken addresses of markets entered
const assets = await comptroller.getAssetsIn(ourAddress);

When you borrow from a market, you are automatically entered into that market. When you supply to a market, however, you must explicitly enter the market.

In practice, this distinction means that the getAssetsIn method only returns:

  • Assets that you have borrowed.

  • Assets that you have supplied and have used to enter markets.

If you have supplied assets but have not explicitly entered a market with them, those assets are not included in the results because there's no position to manage and the Comptroller does not need to be aware of those assets.

To determine whether you have borrowed from a market or supplied to the markets, you can loop through each asset and learn its current status. The comments in this code snippet explain what's happening at each step.

manage-protection.ts
console.log('Checking status of each asset...\n');
for (const asset of assets) {
// First we'll check balances in this loop and see if any of our
// balances come from borrows. For brevity, we prefix all properties
// about the underlying token with `u`
const cToken = new Contract(asset, cozyTokenAbi, signer);
const [name, symbol] = await Promise.all([cToken.name(), cToken.symbol()]);
console.log(`Current asset has name "${name}" and symbol "${symbol}"`);
// Let's also learn about the underlying token. First we determine
// if the underlying is ETH then get a contract instance for the
// underlying token if the underlying is not ETH
const uAddr = await cToken.underlying();
const isEth = uAddr === ethAddress; // true if underlying is ETH, false otherwise
const underlying = isEth ? null : new Contract(uAddr, erc20Abi, signer);
// Lastly we either read set the values if ETH is underlying, or
// read the values if it's a token
const uName = underlying ? await underlying.name() : 'Ether';
const uSymbol = underlying ? await cToken.symbol() : 'ETH';
const uDecimals = underlying ? await underlying.decimals() : 18;
console.log(` Underlying ${uName} (${uSymbol}) has ${uDecimals} decimals`);
// Get our balance of the Cozy token
const balance = await cToken.balanceOf(ourAddress);
const balanceScaled = formatUnits(balance, cTokenDecimals);
if (balance.eq(Zero)) {
// Balance is zero, so we have not supplied the underlying token
console.log(' No Cozy Token balance, so nothing supplied');
} else {
// Balance is > 0, so we have supplied some amount of underlying
// tokens. We get exchange rate to figure out how much underlying
// we have. The exchange rate is a mantissa (18 decimal value), so
// the value returned is scaled by
// 10 ** (18 + underlyingDecimals - cTokenDecimals)
const exchangeRate = await cToken.exchangeRateStored();
const scale = 18 + uDecimals - cTokenDecimals;
const uBalance = balance.mul(exchangeRate);
const uBalanceScaled = formatUnits(uBalance, scale + cTokenDecimals);
console.log(` Balance of ${balanceScaled} Cozy Tokens (equal to ${uBalanceScaled} underlying)`);
}
// Now get our balance of the underlying token
const uBalance = underlying ? await underlying.balanceOf(ourAddress) : await signer.provider.getBalance(ourAddress);
if (uBalance.eq(Zero)) {
// Underlying balance is zero, so we have not borrowed the
// underlying token
console.log(` No underlying ${uSymbol} balance`);
} else {
// Underlying balance is above zero, BUT we still may not have
// borrowed this token -- we may have already had some
const uBalanceScaled = formatUnits(uBalance, uDecimals);
console.log(` Balance of ${uBalanceScaled} underlying ${uSymbol} tokens`);
// Read the amount borrowed
const borrowBalance = await cToken.borrowBalanceStored(ourAddress);
const borrowBalanceScaled = formatUnits(borrowBalance, uDecimals); // scale to human readable units
// Now we determine if the funds were borrowed
if (borrowBalance.eq(Zero)) console.log(` None of the underlying ${uSymbol} tokens we have were borrowed`);
else if (borrowBalance.eq(uBalance)) console.log(` All the underlying ${uSymbol} tokens we have were borrowed`);
else console.log(` ${borrowBalanceScaled} of the ${uBalanceScaled} underlying ${uSymbol} tokens were borrowed`);
}
console.log('\n');
} // end for each asset

Check account liquidity

The amount of collateral you have is computed by multiplying the supplied USD balance in a market by that market's collateral factor, and summing that across all markets. Total borrow balances are subtracted from that, resulting in an account liquidity value. Users who have negative account liquidity, i.e. a shortfall, are subject to liquidation, and are prevented from withdrawing or borrowing assets.

To avoid liquidation, you must ensure that account liquidity is always greater than zero. You can check this for an account as follows:

manage-protection.ts
// STEP 2: CHECKING ACCOUNT LIQUIDITY
// getAccountLiquidity() returns three values. The first is an error
// code, the second is the excess liquidity, and the third is the
// shortfall. Only one of the last two will ever be positive
const [errorCode, liquidity, shortfall] = await comptroller.getAccountLiquidity(signer.address);
// Make sure there were no errors reading the data
if (errorCode.toString() !== '0') {
logFailure(`Could not read liquidity. Received error code ${errorCode}. Exiting script`);
return;
}
// There were no errors, so now we check if we have an excess or a
// shortfall
if (shortfall.gt(Zero)) {
logFailure(`WARNING: Account is under-collateralized and may get liquidated! Shortfall amount: ${shortfall}`);
} else if (liquidity.gt(Zero)) {
logSuccess(`Account has excess liquidity and is safe. Amount of liquidity: ${liquidity}`);
} else {
logFailure('WARNING: Account has no liquidity and no shortfall');
}

Manage liquidity

After checking your account liquidity, you might find that you are at risk of having your account liquidated. If you find your account liquidity is too close to the minimum required—for example, you have a shortfall or only a small amount of excess liquidity—you can adjust your position by doing the following:

  • Supply additional collateral.

  • Repay debt.

Supply additional collateral

If you want to adjust your account liquidity (another way of measuring what's commonly known as your collateralization ratio) to reduce the risk of liquidation, you can supply more collateral.

To supply more collateral programmatically, follow the detailed steps in buy-protection.ts script (or reference the abbreviated version of the code for supplying collateral in the manage-protection.ts script).

Repay debt

An alternate way to reduce your chance of liquidation is to pay back some, or all, of your borrowed debt.

There are two options for repaying debts:

  • Repay debt you, as the transaction caller, borrowed.

  • Repay debt on behalf of another borrower.

repayBorrow

You can repay your own borrows using the repayBorrow() method. As the following code snippet illustrates, you first approve the contract to spend DAI, then execute the repay.

manage-protection.ts
// If we want to repay our own borrows, we can use the repayBorrow
// method. First we approve the contract to spend our USDC, then we
// execute the repay
// Send approval transaction and wait it to be mined
const usdcApproveTx = await usdc.approve(yearnProtectionMarket.address, MaxUint256);
await usdcApproveTx.wait();
// Define the amount to repay, taking USDC's decimals into account
const usdcDecimals = await usdc.decimals();
const repayAmount = parseUnits('25', usdcDecimals); // repay 25 USDC
const repayTx = await yearnProtectionMarket.repayBorrow(repayAmount);
await findLog(repayTx, yearnProtectionMarket, 'RepayBorrow', provider);
logSuccess('Successfully repaid a portion of the borrow');

This example, repays a debt of 25 DAI for the account that called the repayBorrow method.

If you wanted to repay the full amount instead of just 25 DAI, the best way to do this is to set the repayAmount to MaxUint256. The Cozy contracts recognize this as a magic number that will repay all your token debt. If you try to repay all token debt without using MaxUint256 as the amount, you'll be left with a very tiny borrow balance, known as dust. This happens because interest accrues at the beginning of the repay transaction, and it's extremely difficult to predict how much will accrue and send the exact amount of tokens required. Using MaxUint256 tells the contracts to repay the full debt after interest accrues.

repayBorrowBehalf

Alternatively, you could have executed the same logic in the code snippet above using the repayBorrowBehalf method. For example, you could have used repayBorrowBehalf(borrower,repayAmount) to repay repayAmount on behalf of borrower.

The repayBorrowBehalf() method is particularly useful for repaying ETH debt. Because ETH is required for gas, calculating how much ETH you need to repay ETH debt can be difficult. For example, if you wanted to repay a full ETH balance, you would need to calculate the gas required for the transaction as well as the interest accrued. Instead of trying to predict the exact gas usage plus interest accrual, you can repay full ETH debts by using a special contract called the Maximillion contract. The Maximillion contract lets you send extra ETH along with your transaction. The contract repays your debt using repayBorrowBehalf(), then refunds you the excess after paying back all debt.

The following code snippet illustrates using the Maximillion contract to repay ETH debt.

manage-protection.ts
// Repay all ETH debt with the Maximillion contract (we actually have
// zero ETH debt in the script, but that's ok). First we get an
// instance of the Maximillion contract
const maximillionAddress = getContractAddress('Maximillion', chainId);
const maximillion = new Contract(maximillionAddress, maximillionAbi, signer);
// Get the address of the Cozy ETH Money Market
const ethMarketAddress = getContractAddress('CozyETH', chainId);
// Now we do the repay. In this case our debt is zero, so any ETH sent
// is excess ETH that will be refunded after repaying the debt. Notice
// how we specify that we are repaying debt for ourselves,
// `signer.address`, and we specify the address of the market to repay
// debt in, `ethMarketAddress`. (We don't look for the success logs
// since this is a dummy transaction and we have no debt, but in
// practice you should).
const value = parseUnits('0.1', 18); // ETH has 18 decimals
const repayEthTx = await maximillion.repayBehalfExplicit(signer.address, ethMarketAddress, { value });