From a2aa3f0eb4eb5875c4c6a3b33c6b073d100779c0 Mon Sep 17 00:00:00 2001 From: bronco Date: Tue, 27 Sep 2022 15:19:07 +0200 Subject: [PATCH 1/4] deducting protocol cut on yield tokens --- balancer-js/src/lib/constants/config.ts | 2 + balancer-js/src/modules/data/index.ts | 11 +++ .../modules/data/protocol-fees/provider.ts | 68 +++++++++++++++++++ balancer-js/src/modules/data/types.ts | 1 + balancer-js/src/modules/pools/apr/apr.ts | 32 ++++++++- balancer-js/src/types.ts | 4 +- 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 balancer-js/src/modules/data/protocol-fees/provider.ts diff --git a/balancer-js/src/lib/constants/config.ts b/balancer-js/src/lib/constants/config.ts index f39f8d4ed..62b8f4f7d 100644 --- a/balancer-js/src/lib/constants/config.ts +++ b/balancer-js/src/lib/constants/config.ts @@ -13,6 +13,8 @@ export const BALANCER_NETWORK_CONFIG: Record = { lidoRelayer: '0xdcdbf71A870cc60C6F9B621E28a7D3Ffd6Dd4965', gaugeController: '0xc128468b7ce63ea702c1f104d55a2566b13d3abd', feeDistributor: '0xD3cf852898b21fc233251427c2DC93d3d604F3BB', + protocolFeePercentagesProvider: + '0x97207B095e4D5C9a6e4cfbfcd2C3358E03B90c4A', }, tokens: { wrappedNativeAsset: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', diff --git a/balancer-js/src/modules/data/index.ts b/balancer-js/src/modules/data/index.ts index b6e1be73d..f92ccdfc0 100644 --- a/balancer-js/src/modules/data/index.ts +++ b/balancer-js/src/modules/data/index.ts @@ -6,6 +6,7 @@ export * from './token'; export * from './token-prices'; export * from './fee-distributor/repository'; export * from './fee-collector/repository'; +export * from './protocol-fees/provider'; export * from './token-yields/repository'; export * from './block-number'; @@ -18,6 +19,7 @@ import { LiquidityGaugeSubgraphRPCProvider } from './liquidity-gauges/provider'; import { FeeDistributorRepository } from './fee-distributor/repository'; import { FeeCollectorRepository } from './fee-collector/repository'; import { TokenYieldsRepository } from './token-yields/repository'; +import { ProtocolFeesProvider } from './protocol-fees/provider'; import { Provider } from '@ethersproject/providers'; // initialCoingeckoList are used to get the initial token list for coingecko @@ -32,6 +34,7 @@ export class Data implements BalancerDataRepositories { liquidityGauges; feeDistributor; feeCollector; + protocolFees; tokenYields; blockNumbers; @@ -103,6 +106,14 @@ export class Data implements BalancerDataRepositories { provider ); + if (networkConfig.addresses.contracts.protocolFeePercentagesProvider) { + this.protocolFees = new ProtocolFeesProvider( + networkConfig.addresses.contracts.multicall, + networkConfig.addresses.contracts.protocolFeePercentagesProvider, + provider + ); + } + this.tokenYields = new TokenYieldsRepository(); } } diff --git a/balancer-js/src/modules/data/protocol-fees/provider.ts b/balancer-js/src/modules/data/protocol-fees/provider.ts new file mode 100644 index 000000000..c8ce35091 --- /dev/null +++ b/balancer-js/src/modules/data/protocol-fees/provider.ts @@ -0,0 +1,68 @@ +// 0x97207B095e4D5C9a6e4cfbfcd2C3358E03B90c4A + +import { Interface } from '@ethersproject/abi'; +import { Provider } from '@ethersproject/providers'; +import { Contract } from '@ethersproject/contracts'; +import { formatUnits } from '@ethersproject/units'; +import { Multicall } from '@/modules/contracts/multicall'; + +const iProtocolFeePercentagesProvider = new Interface([ + 'function getSwapFeePercentage() view returns (uint)', +]); + +export interface ProtocolFees { + swapFee: number; + yieldFee: number; +} + +// Using singleton here, so subsequent calls will return the same promise +let feesPromise: Promise; + +export class ProtocolFeesProvider { + multicall: Contract; + protocolFees?: ProtocolFees; + + constructor( + multicallAddress: string, + private protocolFeePercentagesProviderAddress: string, + provider: Provider + ) { + this.multicall = Multicall(multicallAddress, provider); + } + + private async fetch(): Promise { + const payload = [ + [ + this.protocolFeePercentagesProviderAddress, + iProtocolFeePercentagesProvider.encodeFunctionData( + 'getFeeTypePercentage', + [0] + ), + ], + [ + this.protocolFeePercentagesProviderAddress, + iProtocolFeePercentagesProvider.encodeFunctionData( + 'getFeeTypePercentage', + [2] + ), + ], + ]; + const [, res] = await this.multicall.aggregate(payload); + + const fees = { + swapFee: parseFloat(formatUnits(res[0], 18)), + yieldFee: parseFloat(formatUnits(res[2], 18)), + }; + + return fees; + } + + async getFees(): Promise { + if (!feesPromise) { + feesPromise = this.fetch(); + } + this.protocolFees = await feesPromise; + + return this.protocolFees; + } +} diff --git a/balancer-js/src/modules/data/types.ts b/balancer-js/src/modules/data/types.ts index 3e7ae1360..bddb17ec7 100644 --- a/balancer-js/src/modules/data/types.ts +++ b/balancer-js/src/modules/data/types.ts @@ -1,6 +1,7 @@ export { LiquidityGauge } from './liquidity-gauges/provider'; export { PoolAttribute } from './pool/types'; export { TokenAttribute } from './token/types'; +export { ProtocolFees } from './protocol-fees/provider'; export interface Findable { find: (id: string) => Promise; diff --git a/balancer-js/src/modules/pools/apr/apr.ts b/balancer-js/src/modules/pools/apr/apr.ts index e5c41d950..cf1eca07f 100644 --- a/balancer-js/src/modules/pools/apr/apr.ts +++ b/balancer-js/src/modules/pools/apr/apr.ts @@ -9,7 +9,11 @@ import type { TokenAttribute, LiquidityGauge, } from '@/types'; -import { BaseFeeDistributor, RewardData } from '@/modules/data'; +import { + BaseFeeDistributor, + ProtocolFeesProvider, + RewardData, +} from '@/modules/data'; import { ProtocolRevenue } from './protocol-revenue'; import { Liquidity } from '@/modules/liquidity/liquidity.module'; import { identity, zipObject, pickBy } from 'lodash'; @@ -54,7 +58,8 @@ export class PoolApr { private feeCollector: Findable, private yesterdaysPools?: Findable, private liquidityGauges?: Findable, - private feeDistributor?: BaseFeeDistributor + private feeDistributor?: BaseFeeDistributor, + private protocolFees?: ProtocolFeesProvider ) {} /** @@ -106,7 +111,17 @@ export class PoolApr { const tokenYield = await this.tokenYields.find(token.address); if (tokenYield) { - apr = tokenYield; + if (pool.poolType === 'MetaStable') { + apr = tokenYield * (1 - (await this.protocolSwapFeePercentage())); + } else if (pool.poolType === 'ComposableStable') { + // TODO: add if(token.isTokenExemptFromYieldProtocolFee) once supported by subgraph + // apr = tokenYield; + + const fees = await this.protocolFeesPercentage(); + apr = tokenYield * (1 - fees.yieldFee); + } else { + apr = tokenYield; + } } else { // Handle subpool APRs with recursive call to get the subPool APR const subPool = await this.pools.findBy('address', token.address); @@ -383,6 +398,17 @@ export class PoolApr { return fee ? fee : 0; } + private async protocolFeesPercentage() { + if (this.protocolFees) { + return await this.protocolFees.getFees(); + } + + return { + swapFee: 0, + yieldFee: 0, + }; + } + private async rewardTokenApr(tokenAddress: string, rewardData: RewardData) { if (rewardData.period_finish.toNumber() < Date.now() / 1000) { return { diff --git a/balancer-js/src/types.ts b/balancer-js/src/types.ts index a95b321cc..c25650b83 100644 --- a/balancer-js/src/types.ts +++ b/balancer-js/src/types.ts @@ -13,7 +13,7 @@ import type { PoolAttribute, TokenAttribute, } from '@/modules/data/types'; -import type { BaseFeeDistributor } from './modules/data'; +import type { BaseFeeDistributor, ProtocolFeesProvider } from './modules/data'; import type { GraphQLArgs } from './lib/graphql'; import type { AprBreakdown } from '@/modules/pools/apr/apr'; @@ -50,6 +50,7 @@ export interface ContractAddresses { lidoRelayer?: string; gaugeController?: string; feeDistributor?: string; + protocolFeePercentagesProvider?: string; } export interface BalancerNetworkConfig { @@ -84,6 +85,7 @@ export interface BalancerDataRepositories { liquidityGauges?: Findable; feeDistributor?: BaseFeeDistributor; feeCollector: Findable; + protocolFees?: ProtocolFeesProvider; tokenYields: Findable; } From deb4030fec2bf4aa4b8caff4369751424d4615f5 Mon Sep 17 00:00:00 2001 From: bronco Date: Tue, 27 Sep 2022 19:35:56 +0200 Subject: [PATCH 2/4] debounce coingecko API calls --- .../modules/data/token-prices/coingecko.ts | 133 +++++++++++++----- 1 file changed, 97 insertions(+), 36 deletions(-) diff --git a/balancer-js/src/modules/data/token-prices/coingecko.ts b/balancer-js/src/modules/data/token-prices/coingecko.ts index 81b8e10c5..e4077b704 100644 --- a/balancer-js/src/modules/data/token-prices/coingecko.ts +++ b/balancer-js/src/modules/data/token-prices/coingecko.ts @@ -1,57 +1,126 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ import { Price, Findable, TokenPrices } from '@/types'; import { wrappedTokensMap as aaveWrappedMap } from '../token-yields/tokens/aave'; import axios from 'axios'; +// Conscious choice for a deferred promise since we have setTimeout that returns a promise +// Some reference for history buffs: https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns +interface PromisedTokenPrices { + promise: Promise; + resolve: (value: TokenPrices) => void; + reject: (reason: unknown) => void; +} + +const makePromise = (): PromisedTokenPrices => { + let resolve: (value: TokenPrices) => void = () => {}; + let reject: (reason: unknown) => void = () => {}; + const promise = new Promise((res, rej) => { + [resolve, reject] = [res, rej]; + }); + return { promise, reject, resolve }; +}; + /** * Simple coingecko price source implementation. Configurable by network and token addresses. */ export class CoingeckoPriceRepository implements Findable { prices: TokenPrices = {}; - fetching: { [address: string]: Promise } = {}; urlBase: string; baseTokenAddresses: string[]; + // Properties used for deferring API calls + // TODO: move this logic to hooks + requestedAddresses = new Set(); // Accumulates requested addresses + debounceWait = 200; // Debouncing waiting time [ms] + promisedCalls: PromisedTokenPrices[] = []; // When requesting a price we return a deferred promise + promisedCount = 0; // New request coming when setTimeout is executing will make a new promise + timeout?: ReturnType; + debounceCancel = (): void => {}; // Allow to cancel mid-flight requests + constructor(tokenAddresses: string[], chainId = 1) { - this.baseTokenAddresses = tokenAddresses.map((a) => a.toLowerCase()); + this.baseTokenAddresses = tokenAddresses + .map((a) => a.toLowerCase()) + .map((a) => unwrapToken(a)); this.urlBase = `https://api.coingecko.com/api/v3/simple/token_price/${this.platform( chainId )}?vs_currencies=usd,eth`; } - fetch(address: string): { [address: string]: Promise } { - console.time(`fetching coingecko ${address}`); - const addresses = this.addresses(address); - const prices = axios - .get(this.url(addresses)) + private fetch( + addresses: string[], + { signal }: { signal?: AbortSignal } = {} + ): Promise { + console.time(`fetching coingecko for ${addresses.length} tokens`); + return axios + .get(this.url(addresses), { signal }) .then(({ data }) => { - addresses.forEach((address) => { - delete this.fetching[address]; - }); - this.prices = { - ...this.prices, - ...(Object.keys(data).length == 0 ? { [address]: {} } : data), - }; - return this.prices; + return data; }) - .catch((error) => { - console.error(error); - return this.prices; + .finally(() => { + console.timeEnd(`fetching coingecko for ${addresses.length} tokens`); }); - console.timeEnd(`fetching coingecko ${address}`); - return Object.fromEntries(addresses.map((a) => [a, prices])); + } + + private debouncedFetch(): Promise { + if (!this.promisedCalls[this.promisedCount]) { + this.promisedCalls[this.promisedCount] = makePromise(); + } + + const { promise, resolve, reject } = this.promisedCalls[this.promisedCount]; + + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.timeout = setTimeout(() => { + this.promisedCount++; // any new call will get a new promise + this.fetch([...this.requestedAddresses]) + .then((results) => { + resolve(results); + this.debounceCancel = () => {}; + }) + .catch((reason) => { + console.error(reason); + }); + }, this.debounceWait); + + this.debounceCancel = () => { + if (this.timeout) { + clearTimeout(this.timeout); + } + reject('Cancelled'); + delete this.promisedCalls[this.promisedCount]; + }; + + return promise; } async find(address: string): Promise { const lowercaseAddress = address.toLowerCase(); const unwrapped = unwrapToken(lowercaseAddress); - if (Object.keys(this.fetching).includes(unwrapped)) { - await this.fetching[unwrapped]; - } else if (!Object.keys(this.prices).includes(unwrapped)) { - this.fetching = { - ...this.fetching, - ...this.fetch(unwrapped), - }; - await this.fetching[unwrapped]; + if (!this.prices[unwrapped]) { + try { + let init = false; + if (Object.keys(this.prices).length === 0) { + // Make initial call with all the tokens we want to preload + this.baseTokenAddresses.forEach( + this.requestedAddresses.add.bind(this.requestedAddresses) + ); + init = true; + } + this.requestedAddresses.add(unwrapped); + const promised = await this.debouncedFetch(); + this.prices[unwrapped] = promised[unwrapped]; + this.requestedAddresses.delete(unwrapped); + if (init) { + this.baseTokenAddresses.forEach((a) => { + this.prices[a] = promised[a]; + this.requestedAddresses.delete(a); + }); + } + } catch (error) { + console.error(error); + } } return this.prices[unwrapped]; @@ -83,14 +152,6 @@ export class CoingeckoPriceRepository implements Findable { private url(addresses: string[]): string { return `${this.urlBase}&contract_addresses=${addresses.join(',')}`; } - - private addresses(address: string): string[] { - if (this.baseTokenAddresses.includes(address)) { - return this.baseTokenAddresses; - } else { - return [address]; - } - } } const unwrapToken = (wrappedAddress: string) => { From f510d78dc29ceb40667010ea6602f4036ac14f1f Mon Sep 17 00:00:00 2001 From: Gareth Fuller Date: Wed, 28 Sep 2022 09:45:36 +0100 Subject: [PATCH 3/4] Add attributes to SubgraphPoolToken graphql type --- balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql b/balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql index 698127fa1..0725513b2 100644 --- a/balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql +++ b/balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql @@ -163,6 +163,11 @@ fragment SubgraphPoolToken on PoolToken { managedBalance weight priceRate + token { + pool { + poolType + } + } } query PoolHistoricalLiquidities( From 02b107bd4d527b12467253d73088c29f700c920f Mon Sep 17 00:00:00 2001 From: johngrantuk Date: Wed, 28 Sep 2022 13:11:51 +0100 Subject: [PATCH 4/4] Update version to 0.1.26. --- balancer-js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/balancer-js/package.json b/balancer-js/package.json index ac7bfe552..bb9aa2923 100644 --- a/balancer-js/package.json +++ b/balancer-js/package.json @@ -1,6 +1,6 @@ { "name": "@balancer-labs/sdk", - "version": "0.1.25", + "version": "0.1.26", "description": "JavaScript SDK for interacting with the Balancer Protocol V2", "license": "GPL-3.0-only", "homepage": "https://github.com/balancer-labs/balancer-sdk/balancer-js#readme",