Skip to content

Commit 1478f47

Browse files
Updates etherFi logic to include queued to withdrawal shares (#4445)
* Updates etherFi logic to include queued to withdrawal shares
1 parent 9498d0a commit 1478f47

File tree

11 files changed

+549
-8
lines changed

11 files changed

+549
-8
lines changed

.changeset/calm-plants-heal.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/token-balance-adapter': minor
3+
---
4+
5+
Updates etherFi logic to include queued to withdrawal shares
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[
2+
{
3+
"inputs": [{ "internalType": "address", "name": "staker", "type": "address" }],
4+
"name": "getQueuedWithdrawals",
5+
"outputs": [
6+
{
7+
"components": [
8+
{ "internalType": "address", "name": "staker", "type": "address" },
9+
{ "internalType": "address", "name": "delegatedTo", "type": "address" },
10+
{ "internalType": "address", "name": "withdrawer", "type": "address" },
11+
{ "internalType": "uint256", "name": "nonce", "type": "uint256" },
12+
{ "internalType": "uint32", "name": "startBlock", "type": "uint32" },
13+
{ "internalType": "address[]", "name": "strategies", "type": "address[]" },
14+
{ "internalType": "uint256[]", "name": "scaledShares", "type": "uint256[]" }
15+
],
16+
"internalType": "struct IDelegationManager.Withdrawal[]",
17+
"name": "withdrawals",
18+
"type": "tuple[]"
19+
},
20+
{ "internalType": "uint256[][]", "name": "shares", "type": "uint256[][]" }
21+
],
22+
"stateMutability": "view",
23+
"type": "function"
24+
}
25+
]

packages/sources/token-balance/src/endpoint/etherFi.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@ export const inputParameters = new InputParameters(
2525
type: 'string',
2626
description: 'Input to eigenStrategy contract',
2727
},
28+
eigenPodManager: {
29+
type: 'string',
30+
description: 'EigenPodManager contract address used to query queued withdrawals',
31+
default: '0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A',
32+
},
2833
},
2934
[
3035
{
3136
splitMain: '0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE',
3237
splitMainAccount: '',
3338
eigenStrategy: '0x93c4b944D05dfe6df7645A86cd2206016c51564D',
3439
eigenStrategyUser: '',
40+
eigenPodManager: '0x39053D51B77DC0d36036Fc1fCc8Cb819df8Ef37A',
3541
},
3642
],
3743
)

packages/sources/token-balance/src/transport/etherFi.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
12
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
2-
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
33
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
4-
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
4+
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
55
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
6-
import { BaseEndpointTypes, inputParameters } from '../endpoint/etherFi'
76
import { ethers } from 'ethers'
7+
import EigenPodManager from '../config/EigenPodManager.json'
88
import SplitMain from '../config/SplitMain.json'
99
import StrategyBaseTVLLimits from '../config/StrategyBaseTVLLimits.json'
10+
import { BaseEndpointTypes, inputParameters } from '../endpoint/etherFi'
1011

1112
const logger = makeLogger('Token Balances - EtherFi')
1213

@@ -74,7 +75,23 @@ export class EtherFiBalanceTransport extends SubscriptionTransport<BaseEndpointT
7475
this.provider,
7576
)
7677
const shares = await eigenContract.shares(param.eigenStrategyUser)
77-
const eigenBalance = await eigenContract.sharesToUnderlyingView(shares)
78+
79+
const eigenPodManagerContract = new ethers.Contract(
80+
param.eigenPodManager,
81+
EigenPodManager,
82+
this.provider,
83+
)
84+
const { shares: queuedWithdrawalShares } = (await eigenPodManagerContract.getQueuedWithdrawals(
85+
param.eigenStrategyUser,
86+
)) as { shares?: ethers.BigNumberish[][] }
87+
88+
const queuedSharesTotal = (queuedWithdrawalShares?.flat() ?? []).reduce(
89+
(acc: bigint, val: ethers.BigNumberish) => acc + BigInt(val),
90+
0n,
91+
)
92+
93+
const totalShares = shares + queuedSharesTotal
94+
const eigenBalance = await eigenContract.sharesToUnderlyingView(totalShares)
7895

7996
return {
8097
data: {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`execute etherFi endpoint happy path returns success including queued withdrawal shares 1`] = `
4+
{
5+
"data": {
6+
"decimals": 18,
7+
"result": "12345",
8+
},
9+
"result": "12345",
10+
"statusCode": 200,
11+
"timestamps": {
12+
"providerDataReceivedUnixMs": 978347471111,
13+
"providerDataRequestedUnixMs": 978347471111,
14+
},
15+
}
16+
`;
17+
18+
exports[`execute etherFi endpoint happy path returns success without queued withdrawals 1`] = `
19+
{
20+
"data": {
21+
"decimals": 18,
22+
"result": "4500000000000000000",
23+
},
24+
"result": "4500000000000000000",
25+
"statusCode": 200,
26+
"timestamps": {
27+
"providerDataReceivedUnixMs": 978347471111,
28+
"providerDataRequestedUnixMs": 978347471111,
29+
},
30+
}
31+
`;
32+
33+
exports[`execute etherFi endpoint validation errors fails when required fields are missing 1`] = `
34+
{
35+
"error": {
36+
"message": "[Param: splitMain] param is required but no value was provided",
37+
"name": "AdapterError",
38+
},
39+
"status": "errored",
40+
"statusCode": 400,
41+
}
42+
`;
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {
2+
setEnvVariables,
3+
TestAdapter,
4+
} from '@chainlink/external-adapter-framework/util/testing-utils'
5+
import * as nock from 'nock'
6+
7+
import { ethers } from 'ethers'
8+
9+
import {
10+
ETHERFI_SUCCESS_NO_QUEUED_CONFIG,
11+
ETHERFI_SUCCESS_WITH_QUEUED_CONFIG,
12+
ETHERFI_TEST_PARAMS,
13+
mockEtherFiSuccessNoQueued,
14+
mockEtherFiSuccessWithQueued,
15+
} from './fixtures'
16+
17+
describe('execute', () => {
18+
let testAdapter: TestAdapter
19+
let oldEnv: NodeJS.ProcessEnv
20+
let spy: jest.SpyInstance
21+
22+
const baseParams = {
23+
endpoint: 'etherFi',
24+
splitMain: ETHERFI_TEST_PARAMS.splitMain,
25+
splitMainAccount: ETHERFI_TEST_PARAMS.splitMainAccount,
26+
eigenStrategy: ETHERFI_TEST_PARAMS.eigenStrategy,
27+
eigenStrategyUser: ETHERFI_TEST_PARAMS.eigenStrategyUser,
28+
eigenPodManager: ETHERFI_TEST_PARAMS.eigenPodManager,
29+
}
30+
31+
beforeAll(async () => {
32+
oldEnv = JSON.parse(JSON.stringify(process.env))
33+
34+
process.env.ETHEREUM_RPC_URL =
35+
process.env.ETHEREUM_RPC_URL ?? 'http://localhost-eth-mainnet:8080'
36+
process.env.ETHEREUM_RPC_CHAIN_ID = process.env.ETHEREUM_RPC_CHAIN_ID ?? '1'
37+
process.env.BACKGROUND_EXECUTE_MS = '0'
38+
39+
const mockDate = new Date('2001-01-01T11:11:11.111Z')
40+
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
41+
42+
const adapter = (await import('./../../src')).adapter
43+
adapter.rateLimiting = undefined
44+
45+
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
46+
testAdapter: {} as TestAdapter<never>,
47+
})
48+
})
49+
50+
afterEach(async () => {
51+
testAdapter.mockCache?.cache.clear()
52+
nock.cleanAll()
53+
})
54+
55+
afterAll(async () => {
56+
nock.restore()
57+
spy.mockRestore()
58+
setEnvVariables(oldEnv)
59+
await testAdapter.api.close()
60+
})
61+
62+
describe('etherFi endpoint', () => {
63+
describe('happy path', () => {
64+
it('returns success without queued withdrawals', async () => {
65+
let sharesToUnderlyingCallData: string | undefined
66+
mockEtherFiSuccessNoQueued({
67+
onSharesToUnderlyingCall: (data) => {
68+
sharesToUnderlyingCallData = data
69+
},
70+
})
71+
72+
const response = await testAdapter.request(baseParams)
73+
74+
expect(sharesToUnderlyingCallData).toBeDefined()
75+
expect(response.statusCode).toBe(200)
76+
const expectedTotalShares = ETHERFI_SUCCESS_NO_QUEUED_CONFIG.strategyShares
77+
const expectedCallData = new ethers.Interface([
78+
'function sharesToUnderlyingView(uint256 amountShares) view returns (uint256)',
79+
]).encodeFunctionData('sharesToUnderlyingView', [expectedTotalShares])
80+
expect(sharesToUnderlyingCallData).toBe(expectedCallData)
81+
expect(response.json()).toMatchSnapshot()
82+
})
83+
84+
it('returns success including queued withdrawal shares', async () => {
85+
let sharesToUnderlyingCallData: string | undefined
86+
mockEtherFiSuccessWithQueued({
87+
onSharesToUnderlyingCall: (data) => {
88+
sharesToUnderlyingCallData = data
89+
},
90+
})
91+
92+
const response = await testAdapter.request(baseParams)
93+
94+
expect(response.statusCode).toBe(200)
95+
expect(response.json()).toMatchSnapshot()
96+
97+
expect(sharesToUnderlyingCallData).toBeDefined()
98+
const expectedTotalShares =
99+
ETHERFI_SUCCESS_WITH_QUEUED_CONFIG.strategyShares +
100+
ETHERFI_SUCCESS_WITH_QUEUED_CONFIG.queuedShares!.flat().reduce(
101+
(acc, val) => acc + val,
102+
0n,
103+
)
104+
const expectedCallData = new ethers.Interface([
105+
'function sharesToUnderlyingView(uint256 amountShares) view returns (uint256)',
106+
]).encodeFunctionData('sharesToUnderlyingView', [expectedTotalShares])
107+
expect(sharesToUnderlyingCallData).toBe(expectedCallData)
108+
})
109+
})
110+
111+
describe('validation errors', () => {
112+
it('fails when required fields are missing', async () => {
113+
const response = await testAdapter.request({ endpoint: 'etherfi' })
114+
115+
expect(response.statusCode).toBe(400)
116+
expect(response.json()).toMatchSnapshot()
117+
})
118+
})
119+
})
120+
})

0 commit comments

Comments
 (0)