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
pnpm 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, thegOhm
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.