@chorus-one/polygon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mocharc.json +6 -0
- package/LICENSE +13 -0
- package/README.md +233 -0
- package/dist/cjs/constants.d.ts +187 -0
- package/dist/cjs/constants.js +141 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/referrer.d.ts +2 -0
- package/dist/cjs/referrer.js +15 -0
- package/dist/cjs/staker.d.ts +335 -0
- package/dist/cjs/staker.js +716 -0
- package/dist/cjs/types.d.ts +40 -0
- package/dist/cjs/types.js +2 -0
- package/dist/mjs/constants.d.ts +187 -0
- package/dist/mjs/constants.js +138 -0
- package/dist/mjs/index.d.ts +4 -0
- package/dist/mjs/index.js +2 -0
- package/dist/mjs/package.json +3 -0
- package/dist/mjs/referrer.d.ts +2 -0
- package/dist/mjs/referrer.js +11 -0
- package/dist/mjs/staker.d.ts +335 -0
- package/dist/mjs/staker.js +712 -0
- package/dist/mjs/types.d.ts +40 -0
- package/dist/mjs/types.js +1 -0
- package/hardhat.config.ts +27 -0
- package/package.json +50 -0
- package/src/constants.ts +151 -0
- package/src/index.ts +14 -0
- package/src/referrer.ts +15 -0
- package/src/staker.ts +878 -0
- package/src/types.ts +45 -0
- package/test/fixtures/expected-data.ts +17 -0
- package/test/integration/localSigner.spec.ts +128 -0
- package/test/integration/setup.ts +41 -0
- package/test/integration/staker.spec.ts +587 -0
- package/test/integration/testStaker.ts +130 -0
- package/test/integration/utils.ts +263 -0
- package/test/lib/networks.json +14 -0
- package/test/staker.spec.ts +154 -0
- package/tsconfig.cjs.json +9 -0
- package/tsconfig.json +13 -0
- package/tsconfig.mjs.json +9 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { PolygonStaker, EXCHANGE_RATE_HIGH_PRECISION, VALIDATOR_SHARE_ABI } from '@chorus-one/polygon'
|
|
2
|
+
import {
|
|
3
|
+
type PublicClient,
|
|
4
|
+
type WalletClient,
|
|
5
|
+
type Address,
|
|
6
|
+
parseEther,
|
|
7
|
+
formatEther,
|
|
8
|
+
createWalletClient,
|
|
9
|
+
http,
|
|
10
|
+
maxUint256
|
|
11
|
+
} from 'viem'
|
|
12
|
+
import { hardhat } from 'viem/chains'
|
|
13
|
+
import { use, expect, assert } from 'chai'
|
|
14
|
+
import chaiAsPromised from 'chai-as-promised'
|
|
15
|
+
import {
|
|
16
|
+
prepareTests,
|
|
17
|
+
fundWithStakingToken,
|
|
18
|
+
approve,
|
|
19
|
+
approveAndStake,
|
|
20
|
+
unstake,
|
|
21
|
+
sendTx,
|
|
22
|
+
advanceEpoch,
|
|
23
|
+
impersonate,
|
|
24
|
+
getStakingTokenBalance,
|
|
25
|
+
getWithdrawalDelay
|
|
26
|
+
} from './utils'
|
|
27
|
+
import { restoreToInitialState } from './setup'
|
|
28
|
+
|
|
29
|
+
use(chaiAsPromised)
|
|
30
|
+
|
|
31
|
+
const AMOUNT = '100'
|
|
32
|
+
|
|
33
|
+
// Existing Chorus One validator delegator with accrued rewards at block 24382010
|
|
34
|
+
const WHALE_DELEGATOR = '0xf382c7202ff9fa88f5ee4054b124fbb9cc196c6e' as Address
|
|
35
|
+
|
|
36
|
+
describe('PolygonStaker', () => {
|
|
37
|
+
let delegatorAddress: Address
|
|
38
|
+
let validatorShareAddress: Address
|
|
39
|
+
let walletClient: WalletClient
|
|
40
|
+
let publicClient: PublicClient
|
|
41
|
+
let staker: PolygonStaker
|
|
42
|
+
|
|
43
|
+
afterEach(async () => {
|
|
44
|
+
await restoreToInitialState()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('query methods', () => {
|
|
48
|
+
beforeEach(async () => {
|
|
49
|
+
const setup = await prepareTests()
|
|
50
|
+
validatorShareAddress = setup.validatorShareAddress
|
|
51
|
+
publicClient = setup.publicClient
|
|
52
|
+
staker = setup.staker
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('reads current epoch', async () => {
|
|
56
|
+
const epoch = await staker.getEpoch()
|
|
57
|
+
assert.equal(epoch, 96822n)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('reads stake info', async () => {
|
|
61
|
+
const stakeInfo = await staker.getStake({ delegatorAddress: WHALE_DELEGATOR, validatorShareAddress })
|
|
62
|
+
assert.equal(stakeInfo.balance, formatEther(135000000000000000000000n))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('reads allowance', async () => {
|
|
66
|
+
const allowance = await staker.getAllowance(WHALE_DELEGATOR)
|
|
67
|
+
assert.equal(allowance, '0')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('reads unbond nonce', async () => {
|
|
71
|
+
const nonce = await staker.getUnbondNonce({ delegatorAddress: WHALE_DELEGATOR, validatorShareAddress })
|
|
72
|
+
assert.equal(nonce, 0n)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('reads liquid rewards', async () => {
|
|
76
|
+
const rewards = await staker.getLiquidRewards({ delegatorAddress: WHALE_DELEGATOR, validatorShareAddress })
|
|
77
|
+
assert.equal(rewards, '45.307877957471003709')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('reads withdrawal delay', async () => {
|
|
81
|
+
const delay = await staker.getWithdrawalDelay()
|
|
82
|
+
assert.equal(delay, 80n)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('caches withdrawal delay on subsequent calls', async () => {
|
|
86
|
+
const delay1 = await staker.getWithdrawalDelay()
|
|
87
|
+
const delay2 = await staker.getWithdrawalDelay()
|
|
88
|
+
assert.equal(delay1, delay2)
|
|
89
|
+
assert.equal(delay1, 80n)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('reads exchange rate precision for non-foundation validator', async () => {
|
|
93
|
+
const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
|
|
94
|
+
assert.equal(precision, EXCHANGE_RATE_HIGH_PRECISION)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('staking lifecycle', () => {
|
|
100
|
+
beforeEach(async () => {
|
|
101
|
+
const setup = await prepareTests()
|
|
102
|
+
delegatorAddress = setup.delegatorAddress
|
|
103
|
+
validatorShareAddress = setup.validatorShareAddress
|
|
104
|
+
walletClient = setup.walletClient
|
|
105
|
+
publicClient = setup.publicClient
|
|
106
|
+
staker = setup.staker
|
|
107
|
+
|
|
108
|
+
await fundWithStakingToken({
|
|
109
|
+
publicClient,
|
|
110
|
+
recipientAddress: delegatorAddress,
|
|
111
|
+
amount: parseEther('10000')
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('approves staking token and verifies allowance', async () => {
|
|
116
|
+
await approve({ delegatorAddress, amount: AMOUNT, staker, walletClient, publicClient })
|
|
117
|
+
|
|
118
|
+
const allowance = await staker.getAllowance(delegatorAddress)
|
|
119
|
+
assert.equal(allowance, AMOUNT)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('approves max (unlimited) allowance', async () => {
|
|
123
|
+
await approve({ delegatorAddress, amount: 'max', staker, walletClient, publicClient })
|
|
124
|
+
|
|
125
|
+
const allowance = await staker.getAllowance(delegatorAddress)
|
|
126
|
+
assert.equal(parseEther(allowance), maxUint256)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('stakes and verifies on-chain state', async () => {
|
|
130
|
+
await approveAndStake({
|
|
131
|
+
delegatorAddress,
|
|
132
|
+
validatorShareAddress,
|
|
133
|
+
amount: AMOUNT,
|
|
134
|
+
staker,
|
|
135
|
+
walletClient,
|
|
136
|
+
publicClient
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const stakeInfo = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
140
|
+
assert.equal(stakeInfo.balance, AMOUNT)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('stakes with referrer tracking in tx data', async () => {
|
|
144
|
+
await approve({ delegatorAddress, amount: AMOUNT, staker, walletClient, publicClient })
|
|
145
|
+
|
|
146
|
+
const { tx } = await staker.buildStakeTx({
|
|
147
|
+
delegatorAddress,
|
|
148
|
+
validatorShareAddress,
|
|
149
|
+
amount: AMOUNT,
|
|
150
|
+
minSharesToMint: 0n
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
assert.isTrue(tx.data.includes('c1c1'), 'tx data should contain referrer marker')
|
|
154
|
+
|
|
155
|
+
const request = await walletClient.prepareTransactionRequest({ ...tx, chain: undefined })
|
|
156
|
+
const hash = await walletClient.sendTransaction({ ...request, account: delegatorAddress })
|
|
157
|
+
|
|
158
|
+
const onChainTx = await publicClient.getTransaction({ hash })
|
|
159
|
+
assert.isTrue(onChainTx.input.includes('c1c1'), 'on-chain tx input should contain referrer marker')
|
|
160
|
+
|
|
161
|
+
const receipt = await publicClient.getTransactionReceipt({ hash })
|
|
162
|
+
assert.equal(receipt.status, 'success')
|
|
163
|
+
|
|
164
|
+
const stakeInfo = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
165
|
+
assert.equal(stakeInfo.balance, AMOUNT)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('unstakes and creates unbond request with amount and isWithdrawable fields', async () => {
|
|
169
|
+
await approveAndStake({
|
|
170
|
+
delegatorAddress,
|
|
171
|
+
validatorShareAddress,
|
|
172
|
+
amount: AMOUNT,
|
|
173
|
+
staker,
|
|
174
|
+
walletClient,
|
|
175
|
+
publicClient
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const nonceBefore = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
|
|
179
|
+
const currentEpoch = await staker.getEpoch()
|
|
180
|
+
|
|
181
|
+
const stakeBefore = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
182
|
+
await unstake({
|
|
183
|
+
delegatorAddress,
|
|
184
|
+
validatorShareAddress,
|
|
185
|
+
amount: AMOUNT,
|
|
186
|
+
maximumSharesToBurn: stakeBefore.shares,
|
|
187
|
+
staker,
|
|
188
|
+
walletClient,
|
|
189
|
+
publicClient
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const nonceAfter = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
|
|
193
|
+
assert.equal(nonceAfter, nonceBefore + 1n)
|
|
194
|
+
|
|
195
|
+
const unbond = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonceAfter })
|
|
196
|
+
assert.isTrue(unbond.withdrawEpoch === currentEpoch)
|
|
197
|
+
assert.equal(unbond.amount, AMOUNT)
|
|
198
|
+
assert.equal(unbond.isWithdrawable, false)
|
|
199
|
+
|
|
200
|
+
const stakeAfter = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
201
|
+
assert.equal(stakeAfter.balance, '0')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('stakes with slippageBps and verifies minSharesToMint calculation matches contract formula', async () => {
|
|
205
|
+
await approve({ delegatorAddress, amount: AMOUNT, staker, walletClient, publicClient })
|
|
206
|
+
|
|
207
|
+
const amountWei = parseEther(AMOUNT)
|
|
208
|
+
const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
|
|
209
|
+
|
|
210
|
+
// Get exchange rate from contract (same way the SDK does)
|
|
211
|
+
const exchangeRate = await publicClient
|
|
212
|
+
.readContract({
|
|
213
|
+
address: validatorShareAddress,
|
|
214
|
+
abi: VALIDATOR_SHARE_ABI,
|
|
215
|
+
functionName: 'getTotalStake',
|
|
216
|
+
args: [validatorShareAddress]
|
|
217
|
+
})
|
|
218
|
+
.then(([, rate]) => rate)
|
|
219
|
+
|
|
220
|
+
// Contract formula: shares = amount * precision / exchangeRate
|
|
221
|
+
const expectedShares = (amountWei * precision) / exchangeRate
|
|
222
|
+
|
|
223
|
+
// SDK slippage formula: minSharesToMint = expectedShares - (expectedShares * slippageBps / 10000)
|
|
224
|
+
const slippageBps = 100n // 1%
|
|
225
|
+
const expectedMinShares = expectedShares - (expectedShares * slippageBps) / 10000n
|
|
226
|
+
|
|
227
|
+
const { tx } = await staker.buildStakeTx({
|
|
228
|
+
delegatorAddress,
|
|
229
|
+
validatorShareAddress,
|
|
230
|
+
amount: AMOUNT,
|
|
231
|
+
slippageBps: Number(slippageBps)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// Decode the calldata to verify minSharesToMint
|
|
235
|
+
const decodedMinShares = BigInt('0x' + tx.data.slice(74, 138))
|
|
236
|
+
assert.equal(decodedMinShares, expectedMinShares, 'minSharesToMint should match calculated value')
|
|
237
|
+
|
|
238
|
+
// Verify transaction succeeds
|
|
239
|
+
await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
|
|
240
|
+
|
|
241
|
+
const stakeInfo = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
242
|
+
assert.equal(stakeInfo.balance, AMOUNT)
|
|
243
|
+
|
|
244
|
+
// Verify actual shares received are >= minSharesToMint
|
|
245
|
+
assert.isTrue(stakeInfo.shares >= expectedMinShares, 'Actual shares should be >= minSharesToMint')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('stakes with 0 slippageBps sets minSharesToMint to exact expected shares', async () => {
|
|
249
|
+
await approve({ delegatorAddress, amount: AMOUNT, staker, walletClient, publicClient })
|
|
250
|
+
|
|
251
|
+
const amountWei = parseEther(AMOUNT)
|
|
252
|
+
const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
|
|
253
|
+
|
|
254
|
+
const exchangeRate = await publicClient
|
|
255
|
+
.readContract({
|
|
256
|
+
address: validatorShareAddress,
|
|
257
|
+
abi: VALIDATOR_SHARE_ABI,
|
|
258
|
+
functionName: 'getTotalStake',
|
|
259
|
+
args: [validatorShareAddress]
|
|
260
|
+
})
|
|
261
|
+
.then(([, rate]) => rate)
|
|
262
|
+
|
|
263
|
+
const expectedShares = (amountWei * precision) / exchangeRate
|
|
264
|
+
|
|
265
|
+
const { tx } = await staker.buildStakeTx({
|
|
266
|
+
delegatorAddress,
|
|
267
|
+
validatorShareAddress,
|
|
268
|
+
amount: AMOUNT,
|
|
269
|
+
slippageBps: 0
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const decodedMinShares = BigInt('0x' + tx.data.slice(74, 138))
|
|
273
|
+
assert.equal(decodedMinShares, expectedShares, 'With 0 slippage, minSharesToMint should equal expected shares')
|
|
274
|
+
|
|
275
|
+
await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
|
|
276
|
+
const stakeInfo = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
277
|
+
assert.equal(stakeInfo.shares, expectedShares, 'Actual shares should equal expected shares')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('unstakes with slippageBps and verifies maximumSharesToBurn calculation matches contract formula', async () => {
|
|
281
|
+
await approveAndStake({
|
|
282
|
+
delegatorAddress,
|
|
283
|
+
validatorShareAddress,
|
|
284
|
+
amount: AMOUNT,
|
|
285
|
+
staker,
|
|
286
|
+
walletClient,
|
|
287
|
+
publicClient
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const stake = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
291
|
+
const amountWei = parseEther(AMOUNT)
|
|
292
|
+
const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
|
|
293
|
+
|
|
294
|
+
// Contract formula: shares = claimAmount * precision / exchangeRate
|
|
295
|
+
const expectedShares = (amountWei * precision) / stake.exchangeRate
|
|
296
|
+
|
|
297
|
+
// SDK slippage formula: maximumSharesToBurn = expectedShares + (expectedShares * slippageBps / 10000)
|
|
298
|
+
const slippageBps = 100n // 1%
|
|
299
|
+
const expectedMaxShares = expectedShares + (expectedShares * slippageBps) / 10000n
|
|
300
|
+
|
|
301
|
+
const { tx } = await staker.buildUnstakeTx({
|
|
302
|
+
delegatorAddress,
|
|
303
|
+
validatorShareAddress,
|
|
304
|
+
amount: AMOUNT,
|
|
305
|
+
slippageBps: Number(slippageBps)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Decode the calldata to verify maximumSharesToBurn (second arg after 4 byte selector + 32 byte amount)
|
|
309
|
+
const decodedMaxShares = BigInt('0x' + tx.data.slice(74, 138))
|
|
310
|
+
assert.equal(decodedMaxShares, expectedMaxShares, 'maximumSharesToBurn should match calculated value')
|
|
311
|
+
|
|
312
|
+
await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
|
|
313
|
+
|
|
314
|
+
const stakeAfter = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
315
|
+
assert.equal(stakeAfter.balance, '0')
|
|
316
|
+
|
|
317
|
+
const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
|
|
318
|
+
const unbond = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
|
|
319
|
+
assert.isTrue(unbond.shares <= expectedMaxShares, 'Actual shares burned should be at most maximumSharesToBurn')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('unstakes with 0 slippageBps sets maximumSharesToBurn to exact expected shares', async () => {
|
|
323
|
+
await approveAndStake({
|
|
324
|
+
delegatorAddress,
|
|
325
|
+
validatorShareAddress,
|
|
326
|
+
amount: AMOUNT,
|
|
327
|
+
staker,
|
|
328
|
+
walletClient,
|
|
329
|
+
publicClient
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
const stake = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
333
|
+
const amountWei = parseEther(AMOUNT)
|
|
334
|
+
const precision = await staker.getExchangeRatePrecision(validatorShareAddress)
|
|
335
|
+
|
|
336
|
+
const expectedShares = (amountWei * precision) / stake.exchangeRate
|
|
337
|
+
|
|
338
|
+
const { tx } = await staker.buildUnstakeTx({
|
|
339
|
+
delegatorAddress,
|
|
340
|
+
validatorShareAddress,
|
|
341
|
+
amount: AMOUNT,
|
|
342
|
+
slippageBps: 0
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const decodedMaxShares = BigInt('0x' + tx.data.slice(74, 138))
|
|
346
|
+
assert.equal(
|
|
347
|
+
decodedMaxShares,
|
|
348
|
+
expectedShares,
|
|
349
|
+
'With 0 slippage, maximumSharesToBurn should equal expected shares'
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
|
|
353
|
+
|
|
354
|
+
const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
|
|
355
|
+
const unbond = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
|
|
356
|
+
assert.equal(unbond.shares, expectedShares, 'With 0 slippage, actual shares should equal expected shares')
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('fetches multiple unbonds with getUnbonds batch method', async () => {
|
|
360
|
+
await approveAndStake({
|
|
361
|
+
delegatorAddress,
|
|
362
|
+
validatorShareAddress,
|
|
363
|
+
amount: AMOUNT,
|
|
364
|
+
staker,
|
|
365
|
+
walletClient,
|
|
366
|
+
publicClient
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
const stakeBefore = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
370
|
+
|
|
371
|
+
await unstake({
|
|
372
|
+
delegatorAddress,
|
|
373
|
+
validatorShareAddress,
|
|
374
|
+
amount: '30',
|
|
375
|
+
maximumSharesToBurn: (stakeBefore.shares * 30n) / 100n,
|
|
376
|
+
staker,
|
|
377
|
+
walletClient,
|
|
378
|
+
publicClient
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const stakeAfterFirstUnstake = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
382
|
+
await unstake({
|
|
383
|
+
delegatorAddress,
|
|
384
|
+
validatorShareAddress,
|
|
385
|
+
amount: '70',
|
|
386
|
+
maximumSharesToBurn: stakeAfterFirstUnstake.shares,
|
|
387
|
+
staker,
|
|
388
|
+
walletClient,
|
|
389
|
+
publicClient
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
|
|
393
|
+
assert.equal(nonce, 2n)
|
|
394
|
+
|
|
395
|
+
const unbonds = await staker.getUnbonds({
|
|
396
|
+
delegatorAddress,
|
|
397
|
+
validatorShareAddress,
|
|
398
|
+
unbondNonces: [1n, 2n]
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
assert.lengthOf(unbonds, 2)
|
|
402
|
+
assert.equal(unbonds[0].amount, '30')
|
|
403
|
+
assert.equal(unbonds[1].amount, '70')
|
|
404
|
+
assert.equal(unbonds[0].isWithdrawable, false)
|
|
405
|
+
assert.equal(unbonds[1].isWithdrawable, false)
|
|
406
|
+
assert.isTrue(unbonds[0].shares > 0n)
|
|
407
|
+
assert.isTrue(unbonds[1].shares > 0n)
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('withdraws after unbonding period and verifies isWithdrawable becomes true', async () => {
|
|
411
|
+
await approveAndStake({
|
|
412
|
+
delegatorAddress,
|
|
413
|
+
validatorShareAddress,
|
|
414
|
+
amount: AMOUNT,
|
|
415
|
+
staker,
|
|
416
|
+
walletClient,
|
|
417
|
+
publicClient
|
|
418
|
+
})
|
|
419
|
+
const stakeBefore = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
420
|
+
await unstake({
|
|
421
|
+
delegatorAddress,
|
|
422
|
+
validatorShareAddress,
|
|
423
|
+
amount: AMOUNT,
|
|
424
|
+
maximumSharesToBurn: stakeBefore.shares,
|
|
425
|
+
staker,
|
|
426
|
+
walletClient,
|
|
427
|
+
publicClient
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
|
|
431
|
+
const unbondBefore = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
|
|
432
|
+
assert.equal(unbondBefore.isWithdrawable, false)
|
|
433
|
+
|
|
434
|
+
const withdrawalDelay = await getWithdrawalDelay({ publicClient })
|
|
435
|
+
await advanceEpoch({ publicClient, staker, targetEpoch: unbondBefore.withdrawEpoch + withdrawalDelay })
|
|
436
|
+
|
|
437
|
+
const unbondAfter = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
|
|
438
|
+
assert.equal(unbondAfter.isWithdrawable, true)
|
|
439
|
+
|
|
440
|
+
const balanceBefore = await getStakingTokenBalance({ publicClient, address: delegatorAddress })
|
|
441
|
+
|
|
442
|
+
const { tx } = await staker.buildWithdrawTx({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
|
|
443
|
+
await sendTx({ tx, walletClient, publicClient, senderAddress: delegatorAddress })
|
|
444
|
+
|
|
445
|
+
const balanceAfter = await getStakingTokenBalance({ publicClient, address: delegatorAddress })
|
|
446
|
+
assert.equal(balanceAfter - balanceBefore, parseEther(AMOUNT))
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('rejects withdraw for non-existent unbond', async () => {
|
|
450
|
+
await expect(
|
|
451
|
+
staker.buildWithdrawTx({ delegatorAddress, validatorShareAddress, unbondNonce: 999n })
|
|
452
|
+
).to.be.rejectedWith('No unbond request found for nonce 999')
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('rejects withdraw before unbonding period completes (withdrawEpoch + withdrawalDelay)', async () => {
|
|
456
|
+
await approveAndStake({
|
|
457
|
+
delegatorAddress,
|
|
458
|
+
validatorShareAddress,
|
|
459
|
+
amount: AMOUNT,
|
|
460
|
+
staker,
|
|
461
|
+
walletClient,
|
|
462
|
+
publicClient
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const stakeBefore = await staker.getStake({ delegatorAddress, validatorShareAddress })
|
|
466
|
+
await unstake({
|
|
467
|
+
delegatorAddress,
|
|
468
|
+
validatorShareAddress,
|
|
469
|
+
amount: AMOUNT,
|
|
470
|
+
maximumSharesToBurn: stakeBefore.shares,
|
|
471
|
+
staker,
|
|
472
|
+
walletClient,
|
|
473
|
+
publicClient
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
const nonce = await staker.getUnbondNonce({ delegatorAddress, validatorShareAddress })
|
|
477
|
+
const unbond = await staker.getUnbond({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
|
|
478
|
+
const withdrawalDelay = await getWithdrawalDelay({ publicClient })
|
|
479
|
+
|
|
480
|
+
await expect(
|
|
481
|
+
staker.buildWithdrawTx({ delegatorAddress, validatorShareAddress, unbondNonce: nonce })
|
|
482
|
+
).to.be.rejectedWith(
|
|
483
|
+
`Unbonding not complete. Current epoch: ${unbond.withdrawEpoch}, Required epoch: ${unbond.withdrawEpoch + withdrawalDelay}`
|
|
484
|
+
)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it('rejects claim rewards when none available', async () => {
|
|
488
|
+
await approveAndStake({
|
|
489
|
+
delegatorAddress,
|
|
490
|
+
validatorShareAddress,
|
|
491
|
+
amount: AMOUNT,
|
|
492
|
+
staker,
|
|
493
|
+
walletClient,
|
|
494
|
+
publicClient
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
await expect(staker.buildClaimRewardsTx({ delegatorAddress, validatorShareAddress })).to.be.rejectedWith(
|
|
498
|
+
'No rewards available to claim'
|
|
499
|
+
)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('rejects compound when no rewards available', async () => {
|
|
503
|
+
await approveAndStake({
|
|
504
|
+
delegatorAddress,
|
|
505
|
+
validatorShareAddress,
|
|
506
|
+
amount: AMOUNT,
|
|
507
|
+
staker,
|
|
508
|
+
walletClient,
|
|
509
|
+
publicClient
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
await expect(staker.buildCompoundTx({ delegatorAddress, validatorShareAddress })).to.be.rejectedWith(
|
|
513
|
+
'No rewards available to compound'
|
|
514
|
+
)
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
describe('whale delegator operations', () => {
|
|
519
|
+
let whaleWallet: WalletClient
|
|
520
|
+
|
|
521
|
+
beforeEach(async () => {
|
|
522
|
+
const setup = await prepareTests()
|
|
523
|
+
validatorShareAddress = setup.validatorShareAddress
|
|
524
|
+
publicClient = setup.publicClient
|
|
525
|
+
staker = setup.staker
|
|
526
|
+
|
|
527
|
+
await impersonate({ publicClient, address: WHALE_DELEGATOR })
|
|
528
|
+
whaleWallet = createWalletClient({ account: WHALE_DELEGATOR, chain: hardhat, transport: http() })
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it('claims rewards and verifies POL balance increase', async () => {
|
|
532
|
+
const rewardsBefore = await staker.getLiquidRewards({
|
|
533
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
534
|
+
validatorShareAddress
|
|
535
|
+
})
|
|
536
|
+
assert.isTrue(parseEther(rewardsBefore) > 0n, 'Whale should have accrued rewards')
|
|
537
|
+
|
|
538
|
+
const balanceBefore = await getStakingTokenBalance({ publicClient, address: WHALE_DELEGATOR })
|
|
539
|
+
|
|
540
|
+
const { tx } = await staker.buildClaimRewardsTx({
|
|
541
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
542
|
+
validatorShareAddress
|
|
543
|
+
})
|
|
544
|
+
await sendTx({ tx, walletClient: whaleWallet, publicClient, senderAddress: WHALE_DELEGATOR })
|
|
545
|
+
|
|
546
|
+
const rewardsAfter = await staker.getLiquidRewards({
|
|
547
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
548
|
+
validatorShareAddress
|
|
549
|
+
})
|
|
550
|
+
assert.equal(rewardsAfter, '0')
|
|
551
|
+
|
|
552
|
+
const balanceAfter = await getStakingTokenBalance({ publicClient, address: WHALE_DELEGATOR })
|
|
553
|
+
assert.equal(balanceAfter - balanceBefore, parseEther(rewardsBefore))
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it('compounds rewards and verifies stake increase', async () => {
|
|
557
|
+
const rewardsBefore = await staker.getLiquidRewards({
|
|
558
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
559
|
+
validatorShareAddress
|
|
560
|
+
})
|
|
561
|
+
assert.isTrue(parseEther(rewardsBefore) > 0n, 'Whale should have accrued rewards')
|
|
562
|
+
|
|
563
|
+
const stakeBefore = await staker.getStake({
|
|
564
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
565
|
+
validatorShareAddress
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
const { tx } = await staker.buildCompoundTx({
|
|
569
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
570
|
+
validatorShareAddress
|
|
571
|
+
})
|
|
572
|
+
await sendTx({ tx, walletClient: whaleWallet, publicClient, senderAddress: WHALE_DELEGATOR })
|
|
573
|
+
|
|
574
|
+
const rewardsAfter = await staker.getLiquidRewards({
|
|
575
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
576
|
+
validatorShareAddress
|
|
577
|
+
})
|
|
578
|
+
assert.equal(rewardsAfter, '0')
|
|
579
|
+
|
|
580
|
+
const stakeAfter = await staker.getStake({
|
|
581
|
+
delegatorAddress: WHALE_DELEGATOR,
|
|
582
|
+
validatorShareAddress
|
|
583
|
+
})
|
|
584
|
+
assert.equal(parseEther(stakeAfter.balance) - parseEther(stakeBefore.balance), parseEther(rewardsBefore))
|
|
585
|
+
})
|
|
586
|
+
})
|
|
587
|
+
})
|