Skip to content
This repository has been archived by the owner on Jan 24, 2024. It is now read-only.

Commit

Permalink
feat(compound): Migrate to template (#1370)
Browse files Browse the repository at this point in the history
  • Loading branch information
JForsaken committed Sep 8, 2022
1 parent e68b318 commit 222fa48
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 158 deletions.
153 changes: 153 additions & 0 deletions src/apps/compound/common/compound.borrow.contract-position-fetcher.ts
@@ -0,0 +1,153 @@
import { BigNumberish, Contract } from 'ethers';

import { ETH_ADDR_ALIAS, ZERO_ADDRESS } from '~app-toolkit/constants/address';
import { BLOCKS_PER_DAY } from '~app-toolkit/constants/blocks';
import { buildDollarDisplayItem } from '~app-toolkit/helpers/presentation/display-item.present';
import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present';
import { isMulticallUnderlyingError } from '~multicall/multicall.ethers';
import { DisplayProps } from '~position/display.interface';
import { MetaType } from '~position/position.interface';
import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher';
import {
DefaultContractPositionDefinition,
GetDataPropsParams,
GetDefinitionsParams,
GetDisplayPropsParams,
GetTokenBalancesParams,
GetTokenDefinitionsParams,
UnderlyingTokenDefinition,
} from '~position/template/contract-position.template.types';

export type CompoundBorrowTokenDataProps = {
apy: number;
liquidity: number;
isActive: boolean;
};

export abstract class CompoundBorrowContractPositionFetcher<
R extends Contract,
S extends Contract,
> extends ContractPositionTemplatePositionFetcher<R, CompoundBorrowTokenDataProps> {
abstract comptrollerAddress: string;
abstract getCompoundCTokenContract(address: string): R;
abstract getCompoundComptrollerContract(address: string): S;

abstract getMarkets(contract: S): Promise<string[]>;
abstract getUnderlyingAddress(contract: R): Promise<string>;
abstract getExchangeRate(contract: R): Promise<BigNumberish>;
abstract getBorrowRate(contract: R): Promise<BigNumberish>;
abstract getCash(contract: R): Promise<BigNumberish>;
abstract getBorrowBalance(opts: { address: string; contract: R }): Promise<BigNumberish>;
abstract getCTokenSupply(contract: R): Promise<BigNumberish>;
abstract getCTokenDecimals(contract: R): Promise<number>;

getContract(address: string): R {
return this.getCompoundCTokenContract(address);
}

async getDefinitions({ multicall }: GetDefinitionsParams): Promise<DefaultContractPositionDefinition[]> {
const comptroller = this.getCompoundComptrollerContract(this.comptrollerAddress);
const addresses = await this.getMarkets(multicall.wrap(comptroller));
return addresses.map(addr => ({ address: addr.toLowerCase() }));
}

async getTokenDefinitions({
contract,
}: GetTokenDefinitionsParams<R, DefaultContractPositionDefinition>): Promise<UnderlyingTokenDefinition[] | null> {
const underlyingAddressRaw = await this.getUnderlyingAddress(contract).catch(err => {
// if the underlying call failed, it's the compound-wrapped native token
if (isMulticallUnderlyingError(err)) return ZERO_ADDRESS;
throw err;
});

const underlyingAddress = underlyingAddressRaw.toLowerCase().replace(ETH_ADDR_ALIAS, ZERO_ADDRESS);
return [{ address: underlyingAddress, metaType: MetaType.BORROWED }];
}

async getLabel({
contractPosition,
}: GetDisplayPropsParams<R, CompoundBorrowTokenDataProps>): Promise<DisplayProps['label']> {
const [underlyingToken] = contractPosition.tokens;
return getLabelFromToken(underlyingToken);
}

async getSecondaryLabel({
contractPosition,
}: GetDisplayPropsParams<R, CompoundBorrowTokenDataProps>): Promise<DisplayProps['secondaryLabel']> {
const [underlyingToken] = contractPosition.tokens;
return buildDollarDisplayItem(underlyingToken.price);
}

protected getDenormalizedRate({
blocksPerDay,
rate,
}: {
rate: BigNumberish;
blocksPerDay: number;
decimals: number;
}): number {
return 100 * (Math.pow(1 + (blocksPerDay * Number(rate)) / Number(1e18), 365) - 1);
}

protected async getApy({ contract, contractPosition }: GetDataPropsParams<R, CompoundBorrowTokenDataProps>) {
const [underlyingToken] = contractPosition.tokens;
const borrowRate = await this.getBorrowRate(contract);
const blocksPerDay = BLOCKS_PER_DAY[this.network];
return this.getDenormalizedRate({
blocksPerDay,
rate: borrowRate,
decimals: underlyingToken.decimals,
});
}

protected async getPricePerShare({
contract,
contractPosition,
}: GetDataPropsParams<R, CompoundBorrowTokenDataProps>) {
const [underlyingToken] = contractPosition.tokens;
const rateRaw = await this.getExchangeRate(contract);
const mantissa = underlyingToken.decimals + 10;

return Number(rateRaw) / 10 ** mantissa;
}

async getDataProps(opts: GetDataPropsParams<R, CompoundBorrowTokenDataProps>): Promise<CompoundBorrowTokenDataProps> {
const { contractPosition, contract } = opts;
const [underlyingToken] = contractPosition.tokens;

const [decimals, supplyRaw, pricePerShare, apy, cashRaw] = await Promise.all([
this.getCTokenDecimals(contract),
this.getCTokenSupply(contract),
this.getPricePerShare(opts),
this.getApy(opts),
this.getCash(contract).catch(e => {
if (isMulticallUnderlyingError(e)) return 0;
throw e;
}),
]);

const supply = Number(supplyRaw) / 10 ** decimals;
const price = pricePerShare * underlyingToken.price;
const underlyingLiquidity = price * supply;

// The "cash" needs to be converted back into a proper number format.
// We use the underlying token as the basis for the conversion.
const cashSupply = Number(cashRaw) / 10 ** underlyingToken.decimals;
// Liquidity is the total supply of "cash" multiplied by the price of an underlying token
const borrowedPositionliquidity = cashSupply * underlyingToken.price;

const borrowLiquidity =
borrowedPositionliquidity > underlyingLiquidity ? 0 : underlyingLiquidity - borrowedPositionliquidity;

return {
liquidity: -borrowLiquidity,
isActive: Boolean(borrowLiquidity > 0),
apy,
};
}

async getTokenBalancesPerPosition({ address, contract }: GetTokenBalancesParams<R, CompoundBorrowTokenDataProps>) {
const balanceRaw = await this.getBorrowBalance({ contract, address });
return [balanceRaw];
}
}
@@ -0,0 +1,82 @@
import { Inject } from '@nestjs/common';
import { BigNumberish } from 'ethers';

import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present';
import { MetaType } from '~position/position.interface';
import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher';
import {
DefaultContractPositionDefinition,
GetDataPropsParams,
GetDisplayPropsParams,
GetTokenBalancesParams,
GetTokenDefinitionsParams,
UnderlyingTokenDefinition,
} from '~position/template/contract-position.template.types';

import { CompoundComptroller, CompoundContractFactory } from '../contracts';

export type CompoundClaimablePositionDataProps = {
lensAddress: string;
};

export abstract class CompoundClaimableContractPositionFetcher extends ContractPositionTemplatePositionFetcher<CompoundComptroller> {
abstract lensAddress: string;
abstract rewardTokenAddress: string;
abstract comptrollerAddress: string;

constructor(
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
@Inject(CompoundContractFactory) private readonly contractFactory: CompoundContractFactory,
) {
super(appToolkit);
}

getContract(address: string): CompoundComptroller {
return this.contractFactory.compoundComptroller({ address, network: this.network });
}

async getDefinitions(): Promise<DefaultContractPositionDefinition[]> {
return [{ address: this.comptrollerAddress }];
}

async getTokenDefinitions(
_opts: GetTokenDefinitionsParams<CompoundComptroller, DefaultContractPositionDefinition>,
): Promise<UnderlyingTokenDefinition[] | null> {
return [{ address: this.rewardTokenAddress, metaType: MetaType.CLAIMABLE }];
}

async getLabel({ contractPosition }: GetDisplayPropsParams<CompoundComptroller>) {
const rewardToken = contractPosition.tokens[0];
return `Claimable ${getLabelFromToken(rewardToken)}`;
}

async getDataProps(
_params: GetDataPropsParams<CompoundComptroller, CompoundClaimablePositionDataProps>,
): Promise<CompoundClaimablePositionDataProps> {
return {
lensAddress: this.lensAddress,
};
}

async getTokenBalancesPerPosition({
address,
contractPosition,
}: GetTokenBalancesParams<CompoundComptroller, CompoundClaimablePositionDataProps>): Promise<BigNumberish[]> {
const [rewardToken] = contractPosition.tokens;
const {
address: comptrollerAddress,
dataProps: { lensAddress },
} = contractPosition;

const lensContract = this.contractFactory.compoundLens({ address: lensAddress, network: this.network });
const rewardMetadata = await lensContract.callStatic.getCompBalanceMetadataExt(
rewardToken.address,
comptrollerAddress,
address,
);

const rewardBalanceRaw = rewardMetadata[3];
return [rewardBalanceRaw];
}
}
55 changes: 55 additions & 0 deletions src/apps/compound/common/compound.position-presenter.ts
@@ -0,0 +1,55 @@
import { sumBy } from 'lodash';

import { Register } from '~app-toolkit/decorators';
import { PresentationConfig } from '~app/app.interface';
import { PositionPresenterTemplate, ReadonlyBalances } from '~position/template/position-presenter.template';

export abstract class CompoundPositionPresenter extends PositionPresenterTemplate {
explorePresentationConfig?: PresentationConfig = {
tabs: [
{
label: 'Lending',
viewType: 'split',
views: [
{
viewType: 'list',
label: 'Supply',
groupIds: ['supply'],
},
{
viewType: 'list',
label: 'Borrow',
groupIds: ['borrow'],
},
],
},
],
};

@Register.BalanceProductMeta('Lending')
async getLendingMeta(_address: string, balances: ReadonlyBalances) {
const collaterals = balances.filter(balance => balance.balanceUSD > 0);
const debt = balances.filter(balance => balance.balanceUSD < 0);
const totalCollateralUSD = sumBy(collaterals, a => a.balanceUSD);
const totalDebtUSD = sumBy(debt, a => a.balanceUSD);
const utilRatio = (Math.abs(totalDebtUSD) / totalCollateralUSD) * 100;

return [
{
label: 'Collateral',
value: totalCollateralUSD,
type: 'dollar',
},
{
label: 'Debt',
value: totalDebtUSD,
type: 'dollar',
},
{
label: 'Utilization Rate',
value: utilRatio,
type: 'pct',
},
];
}
}

0 comments on commit 222fa48

Please sign in to comment.