1
1
/**
2
- * Incentive Service (WORK IN PROGRESS)
2
+ * Incentive Service
3
3
*
4
- * Calculates staking incentives using:
5
- * - Total supply from cosmos bank module
6
- * - Inflation rate from cosmos mint module
7
- * - Bonded token supply from staking pool
8
- * - Community tax from distribution params
9
- *
10
- * Formula: Incentive = ((1 + [(inflation × total_supply ÷ bonded) × (1 − tax)] ÷ 365) ^ 365) − 1
4
+ * Calculates liquid staking incentives including:
5
+ * - Base staking rewards (inflation × total_supply ÷ bonded_tokens)
6
+ * - Community tax deduction
7
+ * - Validator commission deduction (weighted average from delegated validators)
11
8
*/
12
9
13
10
import { HttpClient , HttpClientResponse } from "@effect/platform"
14
- import { BigDecimal , Data , Effect , pipe , Schema } from "effect"
11
+ import { Array , BigDecimal , Data , Effect , pipe , Schema } from "effect"
15
12
16
13
const REST_BASE_URL = "https://rest.union.build"
17
14
15
+ // Staker address that delegates to validators on behalf of liquid stakers
16
+ const LIQUID_STAKING_STAKER_ADDRESS = "union19ydrfy0d80vgpvs6p0cljlahgxwrkz54ps8455q7jfdfape7ld7quaq69v"
17
+
18
18
export class IncentiveError extends Data . TaggedError ( "IncentiveError" ) < {
19
19
message : string
20
20
cause ?: unknown
@@ -47,13 +47,44 @@ const CirculatingSupplyResponse = Schema.Struct({
47
47
} ) ,
48
48
} )
49
49
50
- // Schema for the incentive calculation result
50
+ const ValidatorsResponse = Schema . Struct ( {
51
+ validators : Schema . Array ( Schema . Struct ( {
52
+ operator_address : Schema . String ,
53
+ tokens : Schema . BigDecimal ,
54
+ commission : Schema . Struct ( {
55
+ commission_rates : Schema . Struct ( {
56
+ rate : Schema . BigDecimal ,
57
+ } ) ,
58
+ } ) ,
59
+ status : Schema . String ,
60
+ jailed : Schema . Boolean ,
61
+ } ) ) ,
62
+ } )
63
+
64
+ const DelegatorDelegationsResponse = Schema . Struct ( {
65
+ delegation_responses : Schema . Array ( Schema . Struct ( {
66
+ delegation : Schema . Struct ( {
67
+ delegator_address : Schema . String ,
68
+ validator_address : Schema . String ,
69
+ shares : Schema . BigDecimal ,
70
+ } ) ,
71
+ balance : Schema . Struct ( {
72
+ denom : Schema . String ,
73
+ amount : Schema . BigDecimal ,
74
+ } ) ,
75
+ } ) ) ,
76
+ } )
77
+
51
78
export const IncentiveResult = Schema . Struct ( {
52
79
rates : Schema . Struct ( {
53
80
yearly : Schema . BigDecimalFromSelf ,
54
81
} ) ,
55
82
incentiveNominal : Schema . BigDecimalFromSelf ,
56
83
incentiveAfterTax : Schema . BigDecimalFromSelf ,
84
+ incentiveAfterCommission : Schema . BigDecimalFromSelf ,
85
+ communityTaxAmount : Schema . BigDecimalFromSelf ,
86
+ validatorCommissionAmount : Schema . BigDecimalFromSelf ,
87
+ weightedAverageCommission : Schema . BigDecimalFromSelf ,
57
88
inflation : Schema . BigDecimalFromSelf ,
58
89
totalSupply : Schema . BigDecimalFromSelf ,
59
90
bondedTokens : Schema . BigDecimalFromSelf ,
@@ -131,17 +162,53 @@ const getCirculatingSupply = pipe(
131
162
) ,
132
163
)
133
164
165
+ const getValidators = pipe (
166
+ HttpClient . HttpClient ,
167
+ Effect . map ( HttpClient . withTracerDisabledWhen ( ( ) => true ) ) ,
168
+ Effect . andThen ( ( client ) =>
169
+ pipe (
170
+ client . get ( `${ REST_BASE_URL } /cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED` ) ,
171
+ Effect . flatMap ( HttpClientResponse . schemaBodyJson ( ValidatorsResponse ) ) ,
172
+ Effect . mapError ( ( cause ) =>
173
+ new IncentiveError ( {
174
+ message : "Failed to fetch validators" ,
175
+ cause,
176
+ } )
177
+ ) ,
178
+ )
179
+ ) ,
180
+ )
181
+
182
+ const getDelegatorDelegations = ( delegatorAddress : string ) => pipe (
183
+ HttpClient . HttpClient ,
184
+ Effect . map ( HttpClient . withTracerDisabledWhen ( ( ) => true ) ) ,
185
+ Effect . andThen ( ( client ) =>
186
+ pipe (
187
+ client . get ( `${ REST_BASE_URL } /cosmos/staking/v1beta1/delegations/${ delegatorAddress } ` ) ,
188
+ Effect . flatMap ( HttpClientResponse . schemaBodyJson ( DelegatorDelegationsResponse ) ) ,
189
+ Effect . mapError ( ( cause ) =>
190
+ new IncentiveError ( {
191
+ message : "Failed to fetch delegator delegations" ,
192
+ cause,
193
+ } )
194
+ ) ,
195
+ )
196
+ ) ,
197
+ )
198
+
134
199
export const calculateIncentive : Effect . Effect <
135
200
IncentiveResult ,
136
201
IncentiveError ,
137
202
HttpClient . HttpClient
138
203
> = Effect . gen ( function * ( ) {
139
- const [ inflationData , stakingPoolData , distributionData , circulatingSupplyData ] = yield * Effect
204
+ const [ inflationData , stakingPoolData , distributionData , circulatingSupplyData , validatorsData , delegationsData ] = yield * Effect
140
205
. all ( [
141
206
getInflation ,
142
207
getStakingPool ,
143
208
getDistributionParams ,
144
209
getCirculatingSupply ,
210
+ getValidators ,
211
+ getDelegatorDelegations ( LIQUID_STAKING_STAKER_ADDRESS ) ,
145
212
] , { concurrency : "unbounded" } )
146
213
147
214
const inflation = inflationData . inflation
@@ -181,7 +248,6 @@ export const calculateIncentive: Effect.Effect<
181
248
)
182
249
}
183
250
184
- // Step 1: Calculate nominal incentive rate
185
251
const incentiveNominal = yield * pipe (
186
252
BigDecimal . multiply ( inflation , totalSupply ) ,
187
253
BigDecimal . divide ( bondedTokens ) ,
@@ -192,12 +258,48 @@ export const calculateIncentive: Effect.Effect<
192
258
) ,
193
259
)
194
260
195
- // Step 2: Apply community tax
196
- const incentiveAfterTax = BigDecimal . multiply (
197
- incentiveNominal ,
198
- BigDecimal . subtract ( BigDecimal . fromBigInt ( 1n ) , communityTax ) ,
261
+ const communityTaxAmount = BigDecimal . multiply ( incentiveNominal , communityTax )
262
+ const incentiveAfterTax = BigDecimal . subtract ( incentiveNominal , communityTaxAmount )
263
+
264
+ // Calculate weighted average validator commission
265
+ const validatorMap = new Map ( validatorsData . validators . map ( v => [ v . operator_address , v ] ) )
266
+
267
+ const validDelegations = pipe (
268
+ delegationsData . delegation_responses ,
269
+ Array . filter ( delegation => {
270
+ const validator = validatorMap . get ( delegation . delegation . validator_address )
271
+ return Boolean ( validator && ! validator . jailed && validator . status === "BOND_STATUS_BONDED" )
272
+ } ) ,
273
+ Array . map ( delegation => ( {
274
+ amount : delegation . balance . amount ,
275
+ commission : validatorMap . get ( delegation . delegation . validator_address ) ! . commission . commission_rates . rate
276
+ } ) )
277
+ )
278
+
279
+ const { totalAmount, weightedSum } = pipe (
280
+ validDelegations ,
281
+ Array . reduce (
282
+ { totalAmount : BigDecimal . fromBigInt ( 0n ) , weightedSum : BigDecimal . fromBigInt ( 0n ) } ,
283
+ ( acc , { amount, commission } ) => ( {
284
+ totalAmount : BigDecimal . sum ( acc . totalAmount , amount ) ,
285
+ weightedSum : BigDecimal . sum ( acc . weightedSum , BigDecimal . multiply ( amount , commission ) )
286
+ } )
287
+ )
199
288
)
200
289
290
+ const weightedAverageCommission = BigDecimal . isZero ( totalAmount )
291
+ ? BigDecimal . fromBigInt ( 0n )
292
+ : yield * BigDecimal . divide ( weightedSum , totalAmount ) . pipe (
293
+ Effect . mapError ( ( ) =>
294
+ new IncentiveError ( {
295
+ message : "Could not calculate weighted average commission" ,
296
+ } )
297
+ ) ,
298
+ )
299
+
300
+ const validatorCommissionAmount = BigDecimal . multiply ( incentiveAfterTax , weightedAverageCommission )
301
+ const incentiveAfterCommission = BigDecimal . subtract ( incentiveAfterTax , validatorCommissionAmount )
302
+
201
303
const bondedRatio = yield * BigDecimal . divide ( bondedTokens , totalSupply ) . pipe (
202
304
Effect . mapError ( ( ) =>
203
305
new IncentiveError ( {
@@ -208,10 +310,14 @@ export const calculateIncentive: Effect.Effect<
208
310
209
311
return {
210
312
rates : {
211
- yearly : incentiveAfterTax ,
313
+ yearly : incentiveAfterCommission ,
212
314
} ,
213
315
incentiveNominal,
214
316
incentiveAfterTax,
317
+ incentiveAfterCommission,
318
+ communityTaxAmount,
319
+ validatorCommissionAmount,
320
+ weightedAverageCommission,
215
321
inflation,
216
322
totalSupply,
217
323
bondedTokens,
0 commit comments