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

Commit

Permalink
feat(apps): Spool.fi v2 support
Browse files Browse the repository at this point in the history
  • Loading branch information
tadej-solidant committed Nov 27, 2023
1 parent 27ebf12 commit 5ebbc6a
Show file tree
Hide file tree
Showing 15 changed files with 9,609 additions and 0 deletions.
810 changes: 810 additions & 0 deletions src/apps/spool-v2/contracts/abis/spool-staking.json

Large diffs are not rendered by default.

815 changes: 815 additions & 0 deletions src/apps/spool-v2/contracts/abis/spool-vault.json

Large diffs are not rendered by default.

1,173 changes: 1,173 additions & 0 deletions src/apps/spool-v2/contracts/abis/spool-vospool.json

Large diffs are not rendered by default.

998 changes: 998 additions & 0 deletions src/apps/spool-v2/contracts/ethers/SpoolStaking.ts

Large diffs are not rendered by default.

1,137 changes: 1,137 additions & 0 deletions src/apps/spool-v2/contracts/ethers/SpoolVault.ts

Large diffs are not rendered by default.

1,157 changes: 1,157 additions & 0 deletions src/apps/spool-v2/contracts/ethers/SpoolVospool.ts

Large diffs are not rendered by default.

828 changes: 828 additions & 0 deletions src/apps/spool-v2/contracts/ethers/factories/SpoolStaking__factory.ts

Large diffs are not rendered by default.

833 changes: 833 additions & 0 deletions src/apps/spool-v2/contracts/ethers/factories/SpoolVault__factory.ts

Large diffs are not rendered by default.

1,191 changes: 1,191 additions & 0 deletions src/apps/spool-v2/contracts/ethers/factories/SpoolVospool__factory.ts

Large diffs are not rendered by default.

109 changes: 109 additions & 0 deletions src/apps/spool-v2/ethereum/spool-v2.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { gql } from 'graphql-request';

export const SPOOL_ADDRESS = '0x40803cea2b2a32bda1be61d3604af6a814e70976';
export const VOSPOOL_ADDRESS = '0xaf56d16a7fe479f2fcd48ff567ff589cb2d2a0e9';
export const STAKING_ADDRESS = '0xc3160c5cc63b6116dd182faa8393d3ad9313e213';
export const SUBGRAPH_API_BASE_URL = 'https://api.studio.thegraph.com/query/41372/spool-v2_mainnet/version/latest';
export const SUBGRAPH_V1_API_BASE_URL = "https://api.thegraph.com/subgraphs/name/spoolfi/spool?source=zapper"
export const ANALYTICS_API_BASE_URL = 'https://subgraph-api.58sa2u0osmd2c.eu-west-1.cs.amazonlightsail.com/mainnet';
export const SPOOL_STAKED_QUERY = gql`
query getStaking($address: String!) {
userSpoolStaking(id: $address) {
id
spoolStaked
}
}
`;

export const REWARDS_QUERY = gql`
query {
stakingRewardTokens {
id
token {
id
name
symbol
decimals
}
isRemoved
endTime
startTime
}
}
`;


export const FEES_QUERY = gql`
query {
globals {
treasuryFee
ecosystemFee
}
}
`;

export const VAULTS_ANALYTICS_QUERY = gql`
query {
smartVaults {
id
tvr
baseApy
rewardsApy
adjustedApy
}
}
`;
export const VAULTS_QUERY = gql`
query {
smartVaults {
id
assetGroup {
assetGroupTokens {
token {
id
}
}
}
id
name
riskTolerance
riskProvider {
id
}
smartVaultFees {
id
depositFeePercentage
managementFeePercentage
performanceFeePercentage
}
smartVaultStrategies {
id
isRemoved
allocation
strategy {
id
riskScores {
riskScore
riskProvider {
id
}
}
}
}
smartVaultRewardTokens {
id
token {
id
name
symbol
decimals
}
rewardRate
startTime
endTime
totalAmount
isRemoved
}
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { Inject } from '@nestjs/common';
import { BigNumber } from 'ethers';
import { parseEther } from 'ethers/lib/utils';
import _ from 'lodash';

import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator';
import { buildDollarDisplayItem, buildNumberDisplayItem } from '~app-toolkit/helpers/presentation/display-item.present';
import { getLabelFromToken } from '~app-toolkit/helpers/presentation/image.present';
import { gqlFetch } from '~app-toolkit/helpers/the-graph.helper';
import { SpoolV2ContractFactory, SpoolStaking } from '~apps/spool-v2/contracts';

Check failure on line 11 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts

View workflow job for this annotation

GitHub Actions / Build Check

Cannot find module '~apps/spool-v2/contracts' or its corresponding type declarations.
import {
REWARDS_QUERY,
SPOOL_ADDRESS,
SPOOL_STAKED_QUERY,
STAKING_ADDRESS,
SUBGRAPH_V1_API_BASE_URL,
VOSPOOL_ADDRESS,
} from '~apps/spool-v2/ethereum/spool-v2.constants';
import { StakingReward, UserSpoolStaking } from '~apps/spool-v2/ethereum/spool-v2.types';
import { MetaType } from '~position/position.interface';
import { isClaimable, isSupplied } from '~position/position.utils';
import { ContractPositionTemplatePositionFetcher } from '~position/template/contract-position.template.position-fetcher';
import {
GetDisplayPropsParams,
GetTokenBalancesParams,
GetDataPropsParams,
} from '~position/template/contract-position.template.types';

type SpoolStakingDataProps = {
tvl: number;
spoolStaked: number;
totalAccVoSpool: number;
};

@PositionTemplate()
export class EthereumSpoolV2StakingContractPositionFetcher extends ContractPositionTemplatePositionFetcher<
SpoolStaking,
SpoolStakingDataProps
> {
groupLabel = 'Staking';

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

getContract(address: string): SpoolStaking {
return this.contractFactory.spoolStaking({ address, network: this.network });
}

async getDefinitions() {
return [{ address: STAKING_ADDRESS }];
}

async getTokenDefinitions() {
const now = parseInt(String(new Date().getTime() / 1000));
const stakingRewards = await gqlFetch<StakingReward>({
endpoint: SUBGRAPH_V1_API_BASE_URL,
query: REWARDS_QUERY,
});

// SpoolStaking can emit arbitrary tokens; VoSpoolRewards always emits SPOOL
const rewardAddresses = _.uniq(
stakingRewards.stakingRewardTokens
.filter(reward => !reward.isRemoved && parseInt(reward.startTime) >= now && parseInt(reward.endTime) <= now)
.map(reward => reward.token.id)
.concat([SPOOL_ADDRESS]),
);

return [
{ metaType: MetaType.SUPPLIED, address: SPOOL_ADDRESS, network: this.network },
{ metaType: MetaType.VESTING, address: VOSPOOL_ADDRESS, network: this.network },
...rewardAddresses.map(address => ({ metaType: MetaType.CLAIMABLE, address, network: this.network })),
];
}

async getDataProps({ multicall, contractPosition }: GetDataPropsParams<SpoolStaking, SpoolStakingDataProps>) {
const [spoolToken, voSpoolToken] = contractPosition.tokens;
const spoolContract = this.contractFactory.erc20(spoolToken);
const voSpoolContract = this.contractFactory.spoolVospool(voSpoolToken);

const totalStaked = await multicall.wrap(spoolContract).balanceOf(STAKING_ADDRESS);

Check failure on line 85 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts

View workflow job for this annotation

GitHub Actions / Build Check

Property 'balanceOf' does not exist on type 'GetContractReturnType<Abi, { account: undefined; batch?: { multicall?: boolean | { batchSize?: number | undefined; wait?: number | undefined; } | undefined; } | undefined; cacheTime: number; chain: Chain | undefined; ... 53 more ...; extend: <const client extends { ...; }>(fn: (client: Client<...>) => client) => Cli...'.
const votingPowerRaw = await multicall.wrap(voSpoolContract).getTotalGradualVotingPower();

Check failure on line 86 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts

View workflow job for this annotation

GitHub Actions / Build Check

Property 'getTotalGradualVotingPower' does not exist on type 'GetContractReturnType<Abi, { account: undefined; batch?: { multicall?: boolean | { batchSize?: number | undefined; wait?: number | undefined; } | undefined; } | undefined; cacheTime: number; chain: Chain | undefined; ... 53 more ...; extend: <const client extends { ...; }>(fn: (client: Client<...>) => client) => Cli...'.

const spoolStaked = totalStaked.div(BigNumber.from(10).pow(spoolToken.decimals)).toNumber();
const totalAccVoSpool = votingPowerRaw.div(BigNumber.from(10).pow(voSpoolToken.decimals)).toNumber();
const pricePrecision = 10 ** 10;
const tvl = BigNumber.from((pricePrecision * spoolToken.price).toFixed(0))
.mul(totalStaked)
.div(pricePrecision)
.div(BigNumber.from(10).pow(spoolToken.decimals))
.toNumber();

return { tvl, spoolStaked, totalAccVoSpool };
}

async getLabel({ contractPosition }: GetDisplayPropsParams<SpoolStaking>) {
return `${getLabelFromToken(contractPosition.tokens[0])} Staking`;
}

async getStatsItems({ contractPosition }: GetDisplayPropsParams<SpoolStaking, SpoolStakingDataProps>) {
return [
{ label: 'TVL', value: buildDollarDisplayItem(contractPosition.dataProps.tvl) },
{ label: 'SPOOL Staked', value: buildNumberDisplayItem(contractPosition.dataProps.spoolStaked) },
{ label: 'voSPOOL Accumulated', value: buildNumberDisplayItem(contractPosition.dataProps.totalAccVoSpool) },
];
}

async getTokenBalancesPerPosition({
address,
contractPosition,
multicall,
}: GetTokenBalancesParams<SpoolStaking, SpoolStakingDataProps>) {
const suppliedToken = contractPosition.tokens.find(isSupplied)!;
const rewardTokens = contractPosition.tokens.filter(isClaimable);

const staking = this.contractFactory.spoolStaking({ address: STAKING_ADDRESS, network: this.network });
const voSpool = this.contractFactory.spoolVospool({ address: VOSPOOL_ADDRESS, network: this.network });

const [votingPowerRaw, voSpoolRewards, ...tokenRewards] = await Promise.all([
multicall.wrap(voSpool).getUserGradualVotingPower(address),

Check failure on line 124 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts

View workflow job for this annotation

GitHub Actions / Build Check

Property 'getUserGradualVotingPower' does not exist on type 'GetContractReturnType<Abi, { account: undefined; batch?: { multicall?: boolean | { batchSize?: number | undefined; wait?: number | undefined; } | undefined; } | undefined; cacheTime: number; chain: Chain | undefined; ... 53 more ...; extend: <const client extends { ...; }>(fn: (client: Client<...>) => client) => Cli...'.
multicall.wrap(staking).callStatic.getUpdatedVoSpoolRewardAmount({ from: address }),

Check failure on line 125 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts

View workflow job for this annotation

GitHub Actions / Build Check

Property 'callStatic' does not exist on type 'GetContractReturnType<Abi, { account: undefined; batch?: { multicall?: boolean | { batchSize?: number | undefined; wait?: number | undefined; } | undefined; } | undefined; cacheTime: number; chain: Chain | undefined; ... 53 more ...; extend: <const client extends { ...; }>(fn: (client: Client<...>) => client) => Cli...'.
...rewardTokens.map(reward => multicall.wrap(staking).earned(reward.address, address)),

Check failure on line 126 in src/apps/spool-v2/ethereum/spool-v2.staking.contract-position-fetcher.ts

View workflow job for this annotation

GitHub Actions / Build Check

Property 'earned' does not exist on type 'GetContractReturnType<Abi, { account: undefined; batch?: { multicall?: boolean | { batchSize?: number | undefined; wait?: number | undefined; } | undefined; } | undefined; cacheTime: number; chain: Chain | undefined; ... 53 more ...; extend: <const client extends { ...; }>(fn: (client: Client<...>) => client) => Cli...'.
]);

const stakedSpool = await gqlFetch<UserSpoolStaking>({
endpoint: SUBGRAPH_V1_API_BASE_URL,
query: SPOOL_STAKED_QUERY,
variables: { address },
});

const stakedAmount = parseEther(stakedSpool?.userSpoolStaking?.spoolStaked || '0').toString();

const rewardBalances = rewardTokens.map((token, idx) => {
// Add voSPOOL rewards (always in SPOOL)
const rewards =
token.address.toLowerCase() == suppliedToken.address.toLowerCase()
? tokenRewards[idx].add(voSpoolRewards)
: tokenRewards[idx];

return rewards.toString();
});

return [stakedAmount, votingPowerRaw.toString(), ...rewardBalances];
}
}
137 changes: 137 additions & 0 deletions src/apps/spool-v2/ethereum/spool-v2.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { IMulticallWrapper } from '~multicall';
import { ContractPosition } from '~position/position.interface';

export type SpoolVaults = {
smartVaults: VaultDetails[];
};

export type VaultDetails = {
id: string;
name: string;
riskTolerance: string;
riskProvider: {
id: string;
};
assetGroup: {
assetGroupTokens: {
token: {
id: string;
};
}[];
};
smartVaultFees: {
depositFeePercentage: string;
managementFeePercentage: string;
performanceFeePercentage: string;
};
smartVaultStrategies: {
id: string;
allocation: string;
isRemoved: boolean;
strategy: {
id: string;
riskScores: {
riskScore: string;
riskProvider: {
id: string;
};
}[];
};
}[];
smartVaultRewardTokens: {
id: string;
token: {
id: string;
name: string;
symbol: string;
decimals: number;
};
rewardRate: number;
totalAmount: string;
isRemoved: number;
}[];
};

export type Globals = {
globals: {
treasuryFee: string;
ecosystemFee: string;
};
};

export type VaultsAnalytics = {
smartVaults: {
id: string;
tvr: string;
baseApy: string;
rewardsApy: string[];
adjustedApy: string;
incentivizedApy: string;
}[];
};

export type Platform = {
platform: {
id: string;
treasuryFeeSize: string;
ecosystemFeeSize: string;
};
};

export type StakingReward = {
stakingRewardTokens: {
id: string;
token: {
id: string;
name: string;
symbol: string;
decimals: number;
};
isRemoved: boolean;
endTime: string;
startTime: string;
}[];
};

export type UserSpoolStaking = {
userSpoolStaking: {
id: string;
spoolStaked: string;
};
};

export type VaultDataProps = {
strategies: string[];
};

export type VaultPosition = ContractPosition<VaultDataProps>;

export type SpoolVaultDefinition = {
suppliedTokenAddresses: string[];
address: string;
name: string;
riskModel: string;
rewardTokenAddresses: string[];
strategyAddresses: string[];
stats: {
riskTolerance: number;
adjustedApy: number;
incentivizedApy: number;
apy: number;
tvr: number;
fees: number;
};
};

export type SpoolVaultDataProps = {
totalValueRouted: number;
apy: number;
adjustedApy: number;
incentivizedApy: number;
fees: number;
strategies: string[];
riskScore: string;
riskTolerance: number;
};

export type ResolveBalancesProps = { address: string; contractPosition: VaultPosition; multicall: IMulticallWrapper };
Loading

0 comments on commit 5ebbc6a

Please sign in to comment.