Skip to content

Commit

Permalink
feat(tenderize): ✨ add farm support (Zapper-fi#2219)
Browse files Browse the repository at this point in the history
* fix(tenderize): 🐛 fix swap tokens

* feat(tenderize): ✨ add farm support

* feat(tenderize): ✨ display farm reward rate
  • Loading branch information
cruzdanilo authored and 0xdapper committed Feb 28, 2023
1 parent e078ffc commit e7f33ae
Show file tree
Hide file tree
Showing 14 changed files with 1,399 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PositionTemplate } from '~app-toolkit/decorators/position-template.decorator';

import { FarmContractPositionFetcher } from '../common/tenderize.farm.contract-position-fetcher';

@PositionTemplate()
export class ArbitrumTenderizeFarmTokenFetcher extends FarmContractPositionFetcher {
groupLabel = 'Farm';
}
2 changes: 2 additions & 0 deletions src/apps/tenderize/common/tenderize-token-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ export type TenderizeTokenDefinition = {
address: string;
steak: string;
lpToken: string;
tenderToken: string;
tenderSwap: string;
tenderFarm: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Inject } from '@nestjs/common';

import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
import { GetDataPropsParams, GetTokenBalancesParams } from '~position/template/contract-position.template.types';
import {
SingleStakingFarmDataProps,
SingleStakingFarmTemplateContractPositionFetcher,
} from '~position/template/single-staking.template.contract-position-fetcher';

import { TenderizeContractFactory, TenderFarm } from '../contracts';

import { TenderizeTokenDefinitionsResolver } from './tenderize.token-definition-resolver';

export abstract class FarmContractPositionFetcher extends SingleStakingFarmTemplateContractPositionFetcher<
TenderFarm,
SingleStakingFarmDataProps
> {
constructor(
@Inject(APP_TOOLKIT) protected readonly appToolkit: IAppToolkit,
@Inject(TenderizeTokenDefinitionsResolver)
private readonly tokenDefinitionsResolver: TenderizeTokenDefinitionsResolver,
@Inject(TenderizeContractFactory) protected readonly contractFactory: TenderizeContractFactory,
) {
super(appToolkit);
}

getContract(address: string): TenderFarm {
return this.contractFactory.tenderFarm({ network: this.network, address });
}

async getFarmDefinitions() {
const definitions = await this.tokenDefinitionsResolver.getTokenDefinitions(this.network);
return definitions.map(v => ({
address: v.tenderFarm,
stakedTokenAddress: v.lpToken,
rewardTokenAddresses: [v.tenderToken],
}));
}

async getStakedTokenBalance({ address, contract }: GetTokenBalancesParams<TenderFarm, SingleStakingFarmDataProps>) {
return contract.stakeOf(address);
}

async getRewardTokenBalances({ address, contract }: GetTokenBalancesParams<TenderFarm, SingleStakingFarmDataProps>) {
return contract.availableRewards(address);
}

async getRewardRates({ address }: GetDataPropsParams<TenderFarm, SingleStakingFarmDataProps>) {
return [await this.tokenDefinitionsResolver.getRewardRate(this.network, address)];
}
}
8 changes: 4 additions & 4 deletions src/apps/tenderize/common/tenderize.swap.token-fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Inject } from '@nestjs/common';

import { APP_TOOLKIT, IAppToolkit } from '~app-toolkit/app-toolkit.interface';
import { DefaultDataProps } from '~position/display.interface';
import { AppTokenTemplatePositionFetcher } from '~position/template/app-token.template.position-fetcher';
import {
GetUnderlyingTokensParams,
Expand Down Expand Up @@ -35,7 +34,8 @@ export abstract class SwapTokenFetcher extends AppTokenTemplatePositionFetcher<
}

async getDefinitions(): Promise<TenderizeTokenDefinition[]> {
return this.tokenDefinitionsResolver.getTokenDefinitions(this.network);
const definitions = await this.tokenDefinitionsResolver.getTokenDefinitions(this.network);
return definitions.map(v => ({ ...v, address: v.lpToken }));
}

async getAddresses({ definitions }: GetAddressesParams<TenderizeTokenDefinition>): Promise<string[]> {
Expand All @@ -47,7 +47,7 @@ export abstract class SwapTokenFetcher extends AppTokenTemplatePositionFetcher<
}: GetUnderlyingTokensParams<TenderToken, TenderizeTokenDefinition>) {
return [
{ address: definition.steak, network: this.network },
{ address: definition.address, network: this.network },
{ address: definition.tenderToken, network: this.network },
];
}

Expand All @@ -57,7 +57,7 @@ export abstract class SwapTokenFetcher extends AppTokenTemplatePositionFetcher<

async getPrice({
appToken,
}: GetPriceParams<TenderToken, DefaultDataProps, TenderizeTokenDefinition>): Promise<number> {
}: GetPriceParams<TenderToken, DefaultAppTokenDataProps, TenderizeTokenDefinition>): Promise<number> {
return appToken.tokens[0].price;
}

Expand Down
5 changes: 3 additions & 2 deletions src/apps/tenderize/common/tenderize.tender.token-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ export abstract class TenderTokenFetcher extends AppTokenTemplatePositionFetcher
}

async getDefinitions(): Promise<TenderizeTokenDefinition[]> {
return this.tokenDefinitionsResolver.getTokenDefinitions(this.network);
const definitions = await this.tokenDefinitionsResolver.getTokenDefinitions(this.network);
return definitions.map(v => ({ ...v, address: v.tenderToken }));
}

async getAddresses({ definitions }: GetAddressesParams<TenderizeTokenDefinition>): Promise<string[]> {
return definitions.map(v => v.address);
return definitions.map(v => v.tenderToken);
}

async getUnderlyingTokenDefinitions({
Expand Down
39 changes: 38 additions & 1 deletion src/apps/tenderize/common/tenderize.token-definition-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export type TenderTokenFetcherResponse = {
symbol: string;
steak: string;
tenderSwap: string;
tenderFarm: string;
}[];
};

export type RewardRateFetcherResponse = {
rewardsAddedEvents: {
amount: string;
timestamp: string;
}[];
};

Expand All @@ -35,6 +43,7 @@ export const TOKEN_QUERY = gql`
symbol
steak
tenderSwap
tenderFarm
}
}
`;
Expand Down Expand Up @@ -62,10 +71,11 @@ export class TenderizeTokenDefinitionsResolver {
const tokenDefinitions = definitionsData.configs.map(token => {
return {
id: token.id.toLowerCase(),
address: token.tenderToken.toLowerCase(),
steak: token.steak.toLowerCase(),
lpToken: token.lpToken.toLowerCase(),
tenderToken: token.tenderToken.toLowerCase(),
tenderSwap: token.tenderSwap.toLowerCase(),
tenderFarm: token.tenderFarm.toLowerCase(),
};
});
return tokenDefinitions;
Expand All @@ -88,4 +98,31 @@ export class TenderizeTokenDefinitionsResolver {

return Number(apyRaw);
}

@Cache({
key: (network, tenderFarm) => `studio:tenderize:${network}:${tenderFarm}-reward-data`,
ttl: 5 * 60, // 5 minutes
})
private async getRewardRateData(network: Network, tenderFarm: string) {
return gqlFetch<RewardRateFetcherResponse>({
endpoint: `https://api.thegraph.com/subgraphs/name/tenderize/tenderize-${network}`,
variables: { tenderFarm },
query: gql`
query ($tenderFarm: String!) {
rewardsAddedEvents(where: { tenderFarm: $tenderFarm }, first: 2, orderBy: timestamp, orderDirection: desc) {
amount
timestamp
}
}
`,
});
}

async getRewardRate(network: Network, tenderFarm: string) {
const { rewardsAddedEvents } = await this.getRewardRateData(network, tenderFarm.toLowerCase());
if (rewardsAddedEvents.length != 2) return 0;

const [{ amount, timestamp }, { timestamp: prevTimestamp }] = rewardsAddedEvents;
return BigInt(amount) / (BigInt(timestamp) - BigInt(prevTimestamp));
}
}
192 changes: 192 additions & 0 deletions src/apps/tenderize/contracts/abis/tender-farm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
[
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "account", "type": "address" },
{ "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "Farm",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "account", "type": "address" },
{ "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "Harvest",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }],
"name": "RewardsAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "internalType": "address", "name": "account", "type": "address" },
{ "indexed": false, "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "Unfarm",
"type": "event"
},
{
"inputs": [],
"name": "CRF",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }],
"name": "addRewards",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "_for", "type": "address" }],
"name": "availableRewards",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }],
"name": "farm",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "_for", "type": "address" },
{ "internalType": "uint256", "name": "_amount", "type": "uint256" }
],
"name": "farmFor",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "uint256", "name": "_amount", "type": "uint256" },
{ "internalType": "uint256", "name": "_deadline", "type": "uint256" },
{ "internalType": "uint8", "name": "_v", "type": "uint8" },
{ "internalType": "bytes32", "name": "_r", "type": "bytes32" },
{ "internalType": "bytes32", "name": "_s", "type": "bytes32" }
],
"name": "farmWithPermit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{ "inputs": [], "name": "harvest", "outputs": [], "stateMutability": "nonpayable", "type": "function" },
{
"inputs": [
{ "internalType": "contract IERC20", "name": "_stakeToken", "type": "address" },
{ "internalType": "contract ITenderToken", "name": "_rewardToken", "type": "address" },
{ "internalType": "contract ITenderizer", "name": "_tenderizer", "type": "address" }
],
"name": "initialize",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "nextTotalStake",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "rewardToken",
"outputs": [{ "internalType": "contract ITenderToken", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "uint256", "name": "_value", "type": "uint256" },
{ "internalType": "uint256", "name": "_deadline", "type": "uint256" },
{ "internalType": "uint8", "name": "_v", "type": "uint8" },
{ "internalType": "bytes32", "name": "_r", "type": "bytes32" },
{ "internalType": "bytes32", "name": "_s", "type": "bytes32" }
],
"name": "selfPermit",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{ "internalType": "address", "name": "_token", "type": "address" },
{ "internalType": "uint256", "name": "_value", "type": "uint256" },
{ "internalType": "uint256", "name": "_deadline", "type": "uint256" },
{ "internalType": "uint8", "name": "_v", "type": "uint8" },
{ "internalType": "bytes32", "name": "_r", "type": "bytes32" },
{ "internalType": "bytes32", "name": "_s", "type": "bytes32" }
],
"name": "selfPermitIfNecessary",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [{ "internalType": "contract ITenderizer", "name": "_tenderizer", "type": "address" }],
"name": "setTenderizer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "_of", "type": "address" }],
"name": "stakeOf",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "", "type": "address" }],
"name": "stakes",
"outputs": [
{ "internalType": "uint256", "name": "stake", "type": "uint256" },
{ "internalType": "uint256", "name": "lastCRF", "type": "uint256" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tenderizer",
"outputs": [{ "internalType": "contract ITenderizer", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "token",
"outputs": [{ "internalType": "contract IERC20", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalStake",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "uint256", "name": "_amount", "type": "uint256" }],
"name": "unfarm",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]
Loading

0 comments on commit e7f33ae

Please sign in to comment.