Skip to main content

Compound Fork Lending

Compound is a well-known lending protocol in the Web3 ecosystem. Users can deposit their funds to earn interest, and leverage their position to borrow other assets against their collateral. At the time of writing, Compound is host to over 4.7B of deposited assets for lending.

The protocol is managed by a Comptroller, a risk-management layer which keeps track of user positions and the price at which they would be liquidated. This simple isolated market architecture is often forked to bring lending functionality to other networks, or dynamic isolated markets like in protocols like Rari Fuse.

In this example, we'll look at Cozy Finance, a protocol using the Compound lending protocol architecture. In this protocol, a user can earn yield for depositing assets and earn interest as long as a trigger (an exploit or a hack in a DeFi protocol) doesn't occur.

Add a dependency to the CompoundAppModule in your app module

The CompoundAppModule exposes helper classes that we need to build these positions. Let's open our app module in src/apps/trader-joe/cozy-finance.module.ts and modify the imports key in the module decorator.

@Register.AppModule({
appId: COZY_FINANCE_DEFINITION.id,
imports: [CompoundAppModule],
providers: [
// ...
],
})
export class CozyFinanceAppModule extends AbstractApp() {}

Using the CompoundSupplyTokenHelper

The CompoundSupplyTokenHelper helper class can be used to build a list of AppTokenPosition objects for supply tokens. These tokens represent deposits in an isolated market that reflect Compound's comptroller architecture.

First, let's generate a new token fetcher with yarn studio create-token-fetcher cozy-finance. When prompted for a group, select Create New, then enter supply as the ID and Supply as the label. When prompted for a network, select ethereum.

Let's now open up our newly generator boilerplate in src/apps/cozy-finance/ethereum/cozy-finance.supply.token-fetcher.ts:

import { Inject } from "@nestjs/common";

import { IAppToolkit, APP_TOOLKIT } from "~app-toolkit/app-toolkit.interface";
import { Register } from "~app-toolkit/decorators";
import { PositionFetcher } from "~position/position-fetcher.interface";
import { AppTokenPosition } from "~position/position.interface";
import { Network } from "~types/network.interface";

import { CozyFinanceContractFactory } from "../contracts";
import { COZY_FINANCE_DEFINITION } from "../trader-joe.definition";

const appId = COZY_FINANCE_DEFINITION.id;
const groupId = COZY_FINANCE_DEFINITION.groups.supply.id;
const network = Network.ETHEREUM_MAINNET;

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory
) {}

async getPositions() {
return [];
}
}

Inject and reference the helper class through the CompoundSupplyTokenHelper

We'll inject the CompoundSupplyTokenHelper exported by the CompoundAppModule.

Grab the Cozy Finance comptroller and supply token ABIs, and save them in src/apps/cozy-finance/contracts/abis as cozy-finance-comptroller.json and cozy-token.json. Regenerate the app contract factory with pnpm studio generate:contract-factory cozy-finance.

We'll call the getTokens method on this helper class, and use the generics to specify the type of the comptroller contract as CozyFinanceComptroller, and the type of the supply token as CozyToken. These classes were generated by generating the contract factory.

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
// ...
});
}
}

Add appId, groupId, and network parameters

We'll specify our appId, groupId, and network identifiers. These should match the values specified in the @Register.TokenPositionFetcher decorator.

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
// ...
});
}
}

Add dependencies parameter

We'll use the dependencies parameter to specify which token groups are required as dependencies for building this set of supply tokens. In the case of Cozy Finance, the tokens that can be supplied to this lending market are all base tokens, so we'll leave the dependencies parameter as an empty array.

NOTE:: You can have a look at the isolated markets in the Rari Fuse example to see how dependencies might work in your application. In the case of Rari Fuse, a user can supply gOHM as collateral, so a dependency is required to the Olympus application, and, in particular, the gOhm token group.

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
dependencies: [],
// ...
});
}
}

Add comptrollerAddress parameter

In Compound forks, the comptrollerAddress is the deployed address of the comptroller contract that is used to manage available markets, and their associated risk. Let's go ahead and add this property:

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
dependencies: [],
comptrollerAddress: "0x895879b2c1fbb6ccfcd101f2d3f3c76363664f92",
// ...
});
}
}

Multiple Comptrollers?

Some protocols allow users to create separate isolated markets to enable collateraliztion and borrowing of different sets of assets. The second largest lending market on Rari Fuse allows trading of assets like gOHM and even Curve LPs tokens.

In this case, we'll invoke the getTokens method per comptroller address as follows:

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumRariFuseSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(CompoundContractFactory)
private readonly compoundContractFactory: CompoundContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper,
@Inject(RariFuseContractFactory)
private readonly contractFactory: RariFuseContractFactory,
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit
) {}

async getPositions() {
const network = Network.ETHEREUM_MAINNET;
const poolDirectoryAddress = "0x835482fe0532f169024d5e9410199369aad5c77e";
const controllerContract = this.contractFactory.rariFusePoolsDirectory({
address: poolDirectoryAddress,
network,
});
const pools = await controllerContract.getAllPools();

const baseTokens = await this.appToolkit.getBaseTokenPrices(network);
const appTokens = await this.appToolkit.getAppTokenPositions(
{
appId: CURVE_DEFINITION.id,
groupIds: [CURVE_DEFINITION.groups.pool.id],
network,
},
{
appId: YEARN_DEFINITION.id,
groupIds: [YEARN_DEFINITION.groups.vault.id],
network,
},
{
appId: OLYMPUS_DEFINITION.id,
groupIds: [OLYMPUS_DEFINITION.groups.gOhm.id],
network,
}
);

const markets = await Promise.all(
pools.map((pool) => {
return this.compoundSupplyTokenHelper.getTokens({
network,
appId,
groupId,
comptrollerAddress: pool.comptroller.toLowerCase(),
marketName: pool.name,
allTokens: [...appTokens, ...baseTokens],
// ...
});
})
);

return markets.flat();
}
}

Add getComptrollerContract and getTokenContract parameters

We'll need to know what contract to use to make requests to the comptroller and supply token addresses.

In a previous section, we've already downloaded the JSON files for these ABIs to src/apps/cozy-finance/contracts/abis, and generated the contract factory, and referenced the types via the generics of the getTokens method, allowing us to safely type these two parameters.

Let's see what these look like:

In Compound forks, the comptrollerAddress is the deployed address of the comptroller contract that is used to manage available markets, and their associated risk. Let's go ahead and add this property:

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
dependencies: [],
comptrollerAddress: "0x895879b2c1fbb6ccfcd101f2d3f3c76363664f92",
getComptrollerContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyFinanceComptroller({
address,
network,
}),
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
// ...
});
}
}

Add getAllMarkets parameter

We'll use the getAllMarkets parameter to define how our helper class will retrieve supply token addresses enabled for lending in this market.

On the Cozy Finance comptroller, we can call the getAllMarkets method on the comptroller contract, which is initialized by the helper and injected in the getAllMarkets callback as a parameter.

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
dependencies: [],
comptrollerAddress: "0x895879b2c1fbb6ccfcd101f2d3f3c76363664f92",
getComptrollerContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyFinanceComptroller({
address,
network,
}),
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
getAllMarkets: ({ contract, multicall }) =>
multicall.wrap(contract).getAllMarkets(),
// ...
});
}
}

Add getUnderlyingAddress parameter

We'll use the getUnderlyingAddress parameter to define how our helper class will resolve the underlying token address for each market.

In Cozy Finance, this address is resolved via the underlying method on the supply token contract. The supply token contract instance is passed through to the getUnderlyingAddress callback as a parameter.

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
dependencies: [],
comptrollerAddress: "0x895879b2c1fbb6ccfcd101f2d3f3c76363664f92",
getComptrollerContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyFinanceComptroller({
address,
network,
}),
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
getAllMarkets: ({ contract, multicall }) =>
multicall.wrap(contract).getAllMarkets(),
getUnderlyingAddress: ({ contract, multicall }) =>
multicall.wrap(contract).underlying(),
// ...
});
}
}

Add getExchangeRate and getExchangeRateMantissa parameters

We'll use the getExchangeRate parameter to get the raw exchange rate between the supply token and the underlying token. i.e.: What is the ratio between 1 cozyETH and 1 ETH?

In Cozy Finance, we can retrieve the raw rate via the exchangeRateCurrent method on the supply token contract. The supply token contract instance is passed through to the getExchangeRate callback as a parameter.

In order to denormalize the exchange rate, we need to know the mantissa so we can denormalize the exchange rate.

exchangeRate = exchangeRateRaw / 10 ** mantissa;

In Cozy Finance, the mantissa is the number of decimals of the underlying token, plus 10. This mantissa strategy is that used by the Compound protocol as well.

If you've read the App Tokens documentation, you'll understand that this exchangeRate is actually the pricePerShare value.

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
dependencies: [],
comptrollerAddress: "0x895879b2c1fbb6ccfcd101f2d3f3c76363664f92",
getComptrollerContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyFinanceComptroller({
address,
network,
}),
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
getAllMarkets: ({ contract, multicall }) =>
multicall.wrap(contract).getAllMarkets(),
getUnderlyingAddress: ({ contract, multicall }) =>
multicall.wrap(contract).underlying(),
getExchangeRate: ({ contract, multicall }) =>
multicall.wrap(contract).exchangeRateCurrent(),
getExchangeRateMantissa: ({ underlyingTokenDecimals }) =>
underlyingTokenDecimals + 10,
// ...
});
}
}

Add getSupplyRate and getBorrowRate parameters

We'll use the getSupplyRate and getBorrowRate parameters to define how our helper class will retrieve the interest rates for supply and borrow respectively. In our case, we'll call the supplyRatePerBlock method and borrowRatePerBlock method on the supply token contract.

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
dependencies: [],
comptrollerAddress: "0x895879b2c1fbb6ccfcd101f2d3f3c76363664f92",
getComptrollerContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyFinanceComptroller({
address,
network,
}),
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
getAllMarkets: ({ contract, multicall }) =>
multicall.wrap(contract).getAllMarkets(),
getUnderlyingAddress: ({ contract, multicall }) =>
multicall.wrap(contract).underlying(),
getExchangeRate: ({ contract, multicall }) =>
multicall.wrap(contract).exchangeRateCurrent(),
getExchangeRateMantissa: ({ underlyingTokenDecimals }) =>
underlyingTokenDecimals + 10,
getSupplyRate: ({ contract, multicall }) =>
multicall.wrap(contract).supplyRatePerBlock(),
getBorrowRate: ({ contract, multicall }) =>
multicall.wrap(contract).borrowRatePerBlock(),
// ...
});
}
}

These values are transformed into compounded APY and APR values by compounding per block per day for 365 days. This strategy can be overridden with the getDenormalizedRate method.

For example, if your Compound fork compounds per second, you'll want to instead compound per second per day for 365 days.

Add getDisplayLabel parameter

We'll use the getDisplayLabel parameter to build a human readable label for this position.

In the case of Cozy Finance, it is useful to display the trigger as part of the label. The trigger itself is part of the market token name, so we'll extract it and append it to the underlying token symbol as a label.

@Register.TokenPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceSupplyTokenFetcher
implements PositionFetcher<AppTokenPosition>
{
constructor(
@Inject(APP_TOOLKIT) private readonly appToolkit: IAppToolkit,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory,
@Inject(CompoundSupplyTokenHelper)
private readonly compoundSupplyTokenHelper: CompoundSupplyTokenHelper
) {}

async getPositions() {
return this.compoundSupplyTokenHelper.getTokens<
CozyFinanceComptroller,
CozyToken
>({
appId: COZY_FINANCE_DEFINITION.id,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network: Network.ETHEREUM_MAINNET,
dependencies: [],
comptrollerAddress: "0x895879b2c1fbb6ccfcd101f2d3f3c76363664f92",
getComptrollerContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyFinanceComptroller({
address,
network,
}),
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
getAllMarkets: ({ contract, multicall }) =>
multicall.wrap(contract).getAllMarkets(),
getUnderlyingAddress: ({ contract, multicall }) =>
multicall.wrap(contract).underlying(),
getExchangeRate: ({ contract, multicall }) =>
multicall.wrap(contract).exchangeRateCurrent(),
getExchangeRateMantissa: ({ underlyingTokenDecimals }) =>
underlyingTokenDecimals + 10,
getSupplyRate: ({ contract, multicall }) =>
multicall.wrap(contract).supplyRatePerBlock(),
getBorrowRate: ({ contract, multicall }) =>
multicall.wrap(contract).borrowRatePerBlock(),
getDisplayLabel: async ({ contract, multicall, underlyingToken }) => {
const [symbol, name] = await Promise.all([
multicall.wrap(contract).symbol(),
multicall.wrap(contract).name(),
]);
if (!name.startsWith(`${symbol}-`)) return underlyingToken.symbol;
const triggerLabel = name.replace(`${symbol}-`, "");
return `${underlyingToken.symbol} - ${triggerLabel}`;
},
});
}
}

We're done the supply tokens! Run your application with pnpm start dev, then ensure your tokens are populated by hitting http://localhost:5001/apps/cozy-finance/tokens?groupIds[]=supply.

These tokens represent user supply balances on Cozy Finance, but, in a Compound fork, users are also able to borrow up to risk tolerance defined in the comptroller. Let's see what this looks like.

Using CompoundBorrowContractPositionHelper

The CompoundBorrowContractPositionHelper helper class can be used to build a list of ContractPosition objects for borrow contract positions.

These positions represent borrows in an isolated market that reflect Compound's comptroller architecture.

You may notice the asymmetry in that deposits are represented as tokens, whereas borrows are not represented by tokens. This is an implementation detail of Compound.

For example, Aave V2 represents both supply and borrow positions with tokens (the borrow tokens are, of course, non-transferrable).

Let's open src/apps/cozy-finance/ethereum/cozy-finance.borrow.contract-position-fetcher.ts and implement it.

import { Inject } from "@nestjs/common";

import { Register } from "~app-toolkit/decorators";
import { CompoundBorrowContractPositionHelper } from "~apps/compound";
import { PositionFetcher } from "~position/position-fetcher.interface";
import { ContractPosition } from "~position/position.interface";
import { Network } from "~types/network.interface";

import { COZY_FINANCE_DEFINITION } from "../cozy-finance.definition";

const appId = COZY_FINANCE_DEFINITION.id;
const groupId = COZY_FINANCE_DEFINITION.groups.borrow.id;
const network = Network.ETHEREUM_MAINNET;

@Register.ContractPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceBorrowContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(CompoundBorrowContractPositionHelper)
private readonly compoundBorrowContractPositionHelper: CompoundBorrowContractPositionHelper
) {}

async getPositions() {
return this.compoundBorrowContractPositionHelper.getPositions({
network,
appId,
groupId,
supplyGroupId: COZY_FINANCE_DEFINITION.groups.supply.id,
});
}
}

Super easy! The borrow contract positions are generated directly from the supply tokens, which already have all of the required information.

Run your application with pnpm start dev, then ensure your positions are populated by hitting http://localhost:5001/apps/cozy-finance/positions?groupIds[]=borrow.

Now, we can use these tokens and positions to build wallet balances!

Using CompoundBorrowContractPositionHelper

The CompoundBorrowContractPositionHelper helper class can be used to build a list of ContractPosition objects for borrow contract positions.

These positions represent borrows in an isolated market that reflect Compound's comptroller architecture.

You may notice the asymmetry in that deposits are represented as tokens, whereas borrows are not represented by tokens. This is an implementation detail of Compound.

For example, Aave V2 represents both supply and borrow positions with tokens (the borrow tokens are, of course, non-transferrable).

Let's open src/apps/cozy-finance/ethereum/cozy-finance.borrow.contract-position-fetcher.ts and implement it.

import { Inject } from "@nestjs/common";

import { Register } from "~app-toolkit/decorators";
import { CompoundBorrowContractPositionHelper } from "~apps/compound";
import { PositionFetcher } from "~position/position-fetcher.interface";
import { ContractPosition } from "~position/position.interface";
import { Network } from "~types/network.interface";

import { COZY_FINANCE_DEFINITION } from "../cozy-finance.definition";

const appId = COZY_FINANCE_DEFINITION.id;
const groupId = COZY_FINANCE_DEFINITION.groups.borrow.id;
const network = Network.ETHEREUM_MAINNET;

@Register.ContractPositionFetcher({ appId, groupId, network })
export class EthereumCozyFinanceBorrowContractPositionFetcher
implements PositionFetcher<ContractPosition>
{
constructor(
@Inject(CompoundBorrowContractPositionHelper)
private readonly compoundBorrowContractPositionHelper: CompoundBorrowContractPositionHelper
) {}

async getPositions() {
return this.compoundBorrowContractPositionHelper.getPositions({
network,
appId,
groupId,
supplyGroupId: COZY_FINANCE_DEFINITION.groups.supply.id,
});
}
}

Super easy! The borrow contract positions are generated directly from the supply tokens, which already have all of the required information.

Now, we can use these tokens and positions to build wallet balances!

Using CompoundSupplyBalanceHelper for supply token balances

Generate a balance fetcher class if you haven't already with pnpm studio create-balance-fetcher cozy-finance.

Inject the CompoundSupplyBalanceHelper class. Use the getBalances method to retrieve balances for the given wallet address.

import { Inject } from "@nestjs/common";

import { Register } from "~app-toolkit/decorators";
import { presentBalanceFetcherResponse } from "~app-toolkit/helpers/presentation/balance-fetcher-response.present";
import { CompoundSupplyBalanceHelper } from "~apps/compound";
import { BalanceFetcher } from "~balance/balance-fetcher.interface";
import { Network } from "~types/network.interface";

import { CozyFinanceContractFactory } from "../contracts";
import { COZY_FINANCE_DEFINITION } from "../cozy-finance.definition";

const appId = COZY_FINANCE_DEFINITION.id;
const network = Network.ETHEREUM_MAINNET;

@Register.BalanceFetcher(COZY_FINANCE_DEFINITION.id, network)
export class EthereumCozyFinanceBalanceFetcher implements BalanceFetcher {
constructor(
@Inject(CompoundSupplyBalanceHelper)
private readonly compoundSupplyBalanceHelper: CompoundSupplyBalanceHelper,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory
) {}

async getSupplyBalances(address: string) {
return this.compoundSupplyBalanceHelper.getBalances({
address,
appId,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network,
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
getBalanceRaw: ({ contract, address, multicall }) =>
multicall.wrap(contract).balanceOf(address),
});
}

async getBalances(address: string) {
const [supplyBalances] = await Promise.all([
this.getSupplyBalances(address),
]);

return presentBalanceFetcherResponse([
{
label: "Lending",
assets: [...supplyBalances],
},
]);
}
}

Using CompoundBorrowBalanceHelper for supply token balances

Inject the CompoundBorrowBalanceHelper class. Use the getBalances method to retrieve balances for the given wallet address.

// ...
import {
CompoundSupplyBalanceHelper,
CompoundBorrowBalanceHelper,
} from "~apps/compound";
// ...

@Register.BalanceFetcher(COZY_FINANCE_DEFINITION.id, network)
export class EthereumCozyFinanceBalanceFetcher implements BalanceFetcher {
constructor(
@Inject(CompoundSupplyBalanceHelper)
private readonly compoundSupplyBalanceHelper: CompoundSupplyBalanceHelper,
@Inject(CompoundBorrowBalanceHelper)
private readonly compoundBorrowBalanceHelper: CompoundBorrowBalanceHelper,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory
) {}

async getSupplyBalances(address: string) {
return this.compoundSupplyBalanceHelper.getBalances<CozyToken>({
address,
appId,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network,
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
getBalanceRaw: ({ contract, address, multicall }) =>
multicall.wrap(contract).balanceOf(address),
});
}

async getBorrowBalances(address: string) {
return this.compoundBorrowBalanceHelper.getBalances<CozyToken>({
address,
appId,
groupId: COZY_FINANCE_DEFINITION.groups.borrow.id,
network,
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.compoundCToken({ address, network }),
getBorrowBalanceRaw: ({ contract, address, multicall }) =>
multicall.wrap(contract).borrowBalanceCurrent(address),
});
}

async getBalances(address: string) {
const [supplyBalances, borrowBalances] = await Promise.all([
this.getSupplyBalances(address),
this.getBorrowBalances(address),
]);

return presentBalanceFetcherResponse([
{
label: "Lending",
assets: [...supplyBalances, ...borrowBalances],
},
]);
}
}

Using CompoundLendingMetaHelper for utilization ratio metadata

Inject the CompoundLendingMetaHelper class. Use the getMeta method and pass in the balances for this address as parameters.

This meta helper class will calculate the utilization ratio as metadata, which is useful for users to see if they are close to liquidation.

// ...
import {
CompoundSupplyBalanceHelper,
CompoundBorrowBalanceHelper,
CompoundLendingMetaHelper,
} from "~apps/compound";
// ...

@Register.BalanceFetcher(COZY_FINANCE_DEFINITION.id, network)
export class EthereumCozyFinanceBalanceFetcher implements BalanceFetcher {
constructor(
@Inject(CompoundSupplyBalanceHelper)
private readonly compoundSupplyBalanceHelper: CompoundSupplyBalanceHelper,
@Inject(CompoundBorrowBalanceHelper)
private readonly compoundBorrowBalanceHelper: CompoundBorrowBalanceHelper,
@Inject(CompoundLendingMetaHelper)
private readonly compoundLendingMetaHelper: CompoundLendingMetaHelper,
@Inject(CozyFinanceContractFactory)
private readonly cozyFinanceContractFactory: CozyFinanceContractFactory
) {}

async getSupplyBalances(address: string) {
return this.compoundSupplyBalanceHelper.getBalances<CozyToken>({
address,
appId,
groupId: COZY_FINANCE_DEFINITION.groups.supply.id,
network,
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.cozyToken({ address, network }),
getBalanceRaw: ({ contract, address, multicall }) =>
multicall.wrap(contract).balanceOf(address),
});
}

async getBorrowBalances(address: string) {
return this.compoundBorrowBalanceHelper.getBalances<CozyToken>({
address,
appId,
groupId: COZY_FINANCE_DEFINITION.groups.borrow.id,
network,
getTokenContract: ({ address, network }) =>
this.cozyFinanceContractFactory.compoundCToken({ address, network }),
getBorrowBalanceRaw: ({ contract, address, multicall }) =>
multicall.wrap(contract).borrowBalanceCurrent(address),
});
}

async getBalances(address: string) {
const [supplyBalances, borrowBalances] = await Promise.all([
this.getSupplyBalances(address),
this.getBorrowBalances(address),
]);

const meta = this.compoundLendingMetaHelper.getMeta({
balances: [...supplyBalances, ...borrowBalances],
});

return presentBalanceFetcherResponse([
{
label: "Lending",
assets: [...supplyBalances, ...borrowBalances],
meta,
},
]);
}
}

Run your application with pnpm start dev, then check any addresses positions with http://localhost:5001/apps/cozy-finance/balances?addresses[]=ADDRESS.