Skip to content

Commit d45afda

Browse files
authored
fix: return fees calculation for Thorchain (#4769)
* Add THORChain fee adapter with fetch logic for supported chains * Update THORSwap fee adapter to include v5 fee collector and clarify revenue distribution for THOR holders.
1 parent 4075b8a commit d45afda

File tree

2 files changed

+155
-2
lines changed

2 files changed

+155
-2
lines changed

fees/thorchain/index.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import BigNumber from "bignumber.js";
2+
import { FetchOptions, SimpleAdapter } from "../../adapters/types"
3+
import { CHAIN } from "../../helpers/chains"
4+
import { httpGet } from "../../utils/fetchURL";
5+
import { getTimestampAtStartOfDayUTC } from "../../utils/date";
6+
7+
const chainMapping: any = {
8+
ETH: CHAIN.ETHEREUM,
9+
BTC: CHAIN.BITCOIN,
10+
AVAX: CHAIN.AVAX,
11+
BSC: CHAIN.BSC,
12+
LTC: CHAIN.LITECOIN,
13+
BCH: CHAIN.BITCOIN_CASH,
14+
DOGE: CHAIN.DOGECHAIN,
15+
GAIA: CHAIN.COSMOS,
16+
BASE: CHAIN.BASE,
17+
THOR: CHAIN.THORCHAIN,
18+
XRP: CHAIN.RIPPLE,
19+
}
20+
21+
const THORCHAIN_SUPPORTED_CHAINS = ['BTC', 'ETH', 'LTC', 'DOGE', 'GAIA', 'AVAX', 'BSC', 'BCH', 'BASE', 'THOR', 'XRP']
22+
23+
interface Pool {
24+
assetLiquidityFees: string
25+
earnings: string
26+
pool: string
27+
rewards: string
28+
runeLiquidityFees: string
29+
saverEarning: string
30+
totalLiquidityFeesRune: string
31+
}
32+
33+
const assetFromString = (s: string) => {
34+
35+
const NATIVE_ASSET_DELIMITER = '.'
36+
const SYNTH_ASSET_DELIMITER = '/'
37+
const TRADE_ASSET_DELIMITER = '~'
38+
39+
const isSynth = s.includes(SYNTH_ASSET_DELIMITER)
40+
const isTrade = s.includes(TRADE_ASSET_DELIMITER)
41+
const delimiter = isSynth ? SYNTH_ASSET_DELIMITER : isTrade ? TRADE_ASSET_DELIMITER : NATIVE_ASSET_DELIMITER
42+
43+
const data = s.split(delimiter)
44+
if (data.length <= 1 || !data[1]) return null
45+
46+
const chain = data[0].trim()
47+
const symbol = data[1].trim()
48+
const ticker = symbol.split('-')[0]
49+
50+
if (!symbol || !chain) return null
51+
52+
return { chain, symbol, ticker }
53+
}
54+
55+
const findInterval = (timestamp: number, intervals: any) => {
56+
for (const interval of intervals) {
57+
if (interval.startTime <= timestamp && timestamp < interval.endTime) {
58+
return interval;
59+
}
60+
}
61+
return null;
62+
};
63+
64+
type IRequest = {
65+
[key: string]: Promise<any>;
66+
}
67+
const requests: IRequest = {}
68+
69+
export async function fetchCacheURL(url: string) {
70+
const key = url;
71+
if (!requests[key])
72+
requests[key] = httpGet(url, { headers: {"x-client-id": "defillama"}});
73+
return requests[key]
74+
}
75+
76+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
77+
78+
79+
// New function to generate fetch logic for a single chain
80+
const getFetchForChain = (chainShortName: string) => {
81+
return async (_a:any, _b:any, options: FetchOptions) => {
82+
const startOfDay = getTimestampAtStartOfDayUTC(options.startOfDay);
83+
const earningsUrl = `https://midgard.ninerealms.com/v2/history/earnings?interval=day&from=${options.startTimestamp}&to=${options.endTimestamp}`;
84+
const reserveUrl = `https://midgard.ninerealms.com/v2/history/reserve?interval=day&from=${options.startTimestamp}&to=${options.endTimestamp}`;
85+
const poolsUrl = `https://midgard.ninerealms.com/v2/pools?period=24h`;
86+
87+
const earnings = await fetchCacheURL(earningsUrl);
88+
await sleep(3000);
89+
const revenue = await fetchCacheURL(reserveUrl);
90+
await sleep(2000);
91+
const pools = await fetchCacheURL(poolsUrl);
92+
await sleep(2000);
93+
94+
const selectedEarningInterval = findInterval(startOfDay, earnings.intervals);
95+
const selectedRevenueInterval = findInterval(startOfDay, revenue.intervals);
96+
97+
98+
const poolsByChainEarnings: Pool[] = selectedEarningInterval.pools.filter((pool: any) => assetFromString(pool.pool)?.chain === chainShortName);
99+
100+
const totalRuneDepth = pools.reduce((acum: BigNumber, pool: any) => acum.plus(pool.runeDepth), BigNumber(0));
101+
const poolsByChainData = pools.filter((pool: any) => assetFromString(pool.asset)?.chain === chainShortName);
102+
const runeDepthPerChain = poolsByChainData.reduce((acum: BigNumber, pool: any) => acum.plus(pool.runeDepth), BigNumber(0));
103+
104+
const protocolRevenue = BigNumber(selectedRevenueInterval.gasFeeOutbound || 0).minus(BigNumber(selectedRevenueInterval.gasReimbursement || 0));
105+
106+
const runePercentagePerChain = totalRuneDepth.isZero() ? BigNumber(0) : runeDepthPerChain.div(totalRuneDepth);
107+
const bondingEarnings = selectedEarningInterval.bondingEarnings ? BigNumber(selectedEarningInterval.bondingEarnings) : BigNumber(0);
108+
const bondingRewardPerChainBasedOnRuneDepth = bondingEarnings.times(runePercentagePerChain); // TODO: Artificial distribution according to the liquidity of the pools. But it is a protocol level data
109+
const protocolRevenuePerChainBasedOnRuneDepth = protocolRevenue.times(runePercentagePerChain);
110+
111+
const dailyFees = poolsByChainEarnings.reduce((acum, pool) => {
112+
const liquidityFeesPerPoolInDollars = BigNumber(pool.totalLiquidityFeesRune).div(1e8).times(BigNumber(selectedEarningInterval.runePriceUSD));
113+
const saverLiquidityFeesPerPoolInDollars = BigNumber(pool.saverEarning).div(1e8).times(BigNumber(selectedEarningInterval.runePriceUSD));
114+
const totalLiquidityFees = liquidityFeesPerPoolInDollars.plus(saverLiquidityFeesPerPoolInDollars);
115+
return acum.plus(totalLiquidityFees);
116+
}, BigNumber(0));
117+
118+
const dailySupplysideRevenue = poolsByChainEarnings.reduce((acum, pool) => {
119+
const liquidityFeesPerPoolInDollars = BigNumber(pool.totalLiquidityFeesRune).div(1e8).times(BigNumber(selectedEarningInterval.runePriceUSD));
120+
const saverLiquidityFeesPerPoolInDollars = BigNumber(pool.saverEarning).div(1e8).times(BigNumber(selectedEarningInterval.runePriceUSD));
121+
const rewardsInDollars = BigNumber(pool.rewards).div(1e8).times(BigNumber(selectedEarningInterval.runePriceUSD));
122+
const totalLiquidityFees = liquidityFeesPerPoolInDollars.plus(saverLiquidityFeesPerPoolInDollars).plus(rewardsInDollars);
123+
return acum.plus(totalLiquidityFees);
124+
}, BigNumber(0));
125+
126+
const runePriceUSDNum = selectedEarningInterval.runePriceUSD ? Number(selectedEarningInterval.runePriceUSD) : 0;
127+
const protocolRevenueByChainInDollars = protocolRevenuePerChainBasedOnRuneDepth.div(1e8).times(runePriceUSDNum);
128+
const dailyHoldersRevenue = bondingRewardPerChainBasedOnRuneDepth.div(1e8).times(runePriceUSDNum);
129+
// if (dailyFees.isZero()) throw new Error("No fees found for this day");
130+
131+
return {
132+
dailyFees,
133+
dailyUserFees: dailyFees,
134+
dailyRevenue: `${dailyHoldersRevenue.plus(protocolRevenueByChainInDollars)}`,
135+
dailyProtocolRevenue: protocolRevenueByChainInDollars.gt(0) ? protocolRevenueByChainInDollars : 0,
136+
dailyHoldersRevenue: dailyHoldersRevenue,
137+
dailySupplySideRevenue: dailySupplysideRevenue,
138+
timestamp: startOfDay
139+
};
140+
};
141+
};
142+
143+
const adapters: SimpleAdapter = {
144+
adapter: THORCHAIN_SUPPORTED_CHAINS.reduce((acc, chainKey) => {
145+
(acc as any)[chainMapping[chainKey]] = {
146+
fetch: getFetchForChain(chainKey) as any,
147+
// runAtCurrTime: true,
148+
};
149+
return acc;
150+
}, {}),
151+
};
152+
153+
export default adapters

fees/thorswap/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { addTokensReceived } from "../../helpers/token";
66
const graph = (_chain: Chain): any => {
77
return async (timestamp: number, _: any, options: FetchOptions): Promise<FetchResultFees> => {
88
const dailyFees = await addTokensReceived({
9-
targets: ['0x546e7b1f4b4Df6CDb19fbDdFF325133EBFE04BA7', '0x6Ee1f539DDf1515eE49B58A5E9ae84C2E7643490'], // v3 and v4 fee collectors
9+
targets: ['0x546e7b1f4b4Df6CDb19fbDdFF325133EBFE04BA7', '0x6Ee1f539DDf1515eE49B58A5E9ae84C2E7643490', '0x6d1eff1aFF1dc9978d851D09d9d15f2938Da7BD7'], // v3, v4, v5 fee collectors
1010
tokens: ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'],
1111
options
1212
})
@@ -29,7 +29,7 @@ const adapter: Adapter = {
2929
Fees: 'Swap fees paid by users.',
3030
Revenue: 'Swap fees paid by users.',
3131
ProtocolRevenue: '25% of revenue goes to protocol treasury.',
32-
HoldersRevenue: '75% of revenue goes to THOR stakers. Stakers can choose in which token they want to receive their rewards either THOR or USDC.',
32+
HoldersRevenue: '75% of revenue goes to THOR holders. 20% goes to buy THOR from market and burn. Rest is going to stakers. Stakers can choose in which token they want to receive their rewards either THOR or USDC.',
3333
}
3434
}
3535

0 commit comments

Comments
 (0)