Skip to content

Commit b94b694

Browse files
committed
delegation:
Delegators stake can get soft-locked. Add a validations in the _delegate function to check whether the indexer to whom the tokens are going to be delegated to has tokens currently staked in the protocol. If a delegator tries to send tokens by using an _indexer value as a parameter to one without stake the transaction will revert. This avoids the delegators sending funds to and address by mistake and needing to way the unbonding period to recover from that error.
1 parent 755fe78 commit b94b694

File tree

4 files changed

+90
-70
lines changed

4 files changed

+90
-70
lines changed

contracts/staking/Staking.sol

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -517,10 +517,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
517517
require(_tokens > 0, "Cannot stake zero tokens");
518518

519519
// Transfer tokens to stake from caller to this contract
520-
require(
521-
graphToken().transferFrom(msg.sender, address(this), _tokens),
522-
"Cannot transfer tokens to stake"
523-
);
520+
require(graphToken().transferFrom(msg.sender, address(this), _tokens), "!transfer");
524521

525522
// Stake the transferred tokens
526523
_stake(_indexer, _tokens);
@@ -557,7 +554,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
557554
require(tokensToWithdraw > 0, "No tokens available to withdraw");
558555

559556
// Return tokens to the indexer
560-
require(graphToken().transfer(indexer, tokensToWithdraw), "Cannot transfer tokens");
557+
require(graphToken().transfer(indexer, tokensToWithdraw), "!transfer");
561558

562559
emit StakeWithdrawn(indexer, tokensToWithdraw);
563560
}
@@ -611,7 +608,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
611608

612609
// Give the beneficiary a reward for slashing
613610
if (_reward > 0) {
614-
require(graphToken().transfer(_beneficiary, _reward), "Error sending dispute reward");
611+
require(graphToken().transfer(_beneficiary, _reward), "!transfer");
615612
}
616613

617614
emit StakeSlashed(_indexer, _tokens, _reward, _beneficiary);
@@ -626,10 +623,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
626623
address delegator = msg.sender;
627624

628625
// Transfer tokens to delegate to this contract
629-
require(
630-
graphToken().transferFrom(delegator, address(this), _tokens),
631-
"Cannot transfer tokens to stake"
632-
);
626+
require(graphToken().transferFrom(delegator, address(this), _tokens), "!transfer");
633627

634628
// Update state
635629
_delegate(delegator, _indexer, _tokens);
@@ -677,7 +671,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
677671
_delegate(delegator, _newIndexer, tokensToWithdraw);
678672
} else {
679673
// Return tokens to the delegator
680-
require(graphToken().transfer(delegator, tokensToWithdraw), "Cannot transfer tokens");
674+
require(graphToken().transfer(delegator, tokensToWithdraw), "!transfer");
681675
}
682676
}
683677

@@ -782,10 +776,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
782776
// require(alloc.assetHolder == msg.sender, "caller is not authorized");
783777

784778
// Transfer tokens to collect from the authorized sender
785-
require(
786-
graphToken().transferFrom(msg.sender, address(this), _tokens),
787-
"Cannot transfer tokens to collect"
788-
);
779+
require(graphToken().transferFrom(msg.sender, address(this), _tokens), "!transfer");
789780

790781
_collect(_allocationID, msg.sender, _tokens);
791782
}
@@ -843,10 +834,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
843834
_stake(alloc.indexer, tokensToClaim);
844835
} else {
845836
// Transfer funds back to the indexer
846-
require(
847-
graphToken().transfer(alloc.indexer, tokensToClaim),
848-
"Cannot transfer tokens"
849-
);
837+
require(graphToken().transfer(alloc.indexer, tokensToClaim), "!transfer");
850838
}
851839
}
852840

@@ -1105,10 +1093,12 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
11051093
address _indexer,
11061094
uint256 _tokens
11071095
) internal returns (uint256) {
1108-
// Can only delegate a non-zero amount of tokens
1096+
// Only delegate a non-zero amount of tokens
11091097
require(_tokens > 0, "Cannot delegate zero tokens");
1110-
// Can only delegate to non-empty address
1098+
// Only delegate to non-empty address
11111099
require(_indexer != address(0), "Cannot delegate to empty address");
1100+
// Only delegate to staked indexer
1101+
require(stakes[_indexer].hasTokens(), "Cannot delegate to non-staked indexer");
11121102

11131103
// Get the delegation pool of the indexer
11141104
DelegationPool storage pool = delegationPools[_indexer];

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@types/yargs": "^15.0.5",
3333
"@typescript-eslint/eslint-plugin": "^3.10.1",
3434
"@typescript-eslint/parser": "^3.10.1",
35+
"bignumber.js": "^9.0.0",
3536
"buidler-gas-reporter": "^0.1.3",
3637
"chai": "^4.2.0",
3738
"cli-table": "^0.3.1",

test/rewards/rewards.test.ts

Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect } from 'chai'
22
import { constants, BigNumber } from 'ethers'
3+
import { BigNumber as BN } from 'bignumber.js'
34

45
import { NetworkFixture } from '../lib/fixtures'
56

@@ -25,8 +26,7 @@ const MAX_PPM = 1000000
2526

2627
const { HashZero, WeiPerEther } = constants
2728

28-
const toFloat = (n: BigNumber) => parseFloat(formatGRT(n))
29-
const toRound = (n: BigNumber) => Math.round(toFloat(n))
29+
const toRound = (n: BigNumber) => formatGRT(n).split('.')[0]
3030

3131
describe('Rewards', () => {
3232
let delegator: Account
@@ -55,9 +55,11 @@ describe('Rewards', () => {
5555
const ISSUANCE_RATE_PER_BLOCK = toBN('1012272234429039270') // % increase every block
5656

5757
// Core formula that gets accumulated rewards per signal for a period of time
58-
const getRewardsPerSignal = (p: BigNumber, r: BigNumber, t: BigNumber, s: BigNumber): number => {
59-
if (!toFloat(s)) return 0
60-
return (toRound(p) * toFloat(r) ** t.toNumber() - toFloat(p)) / toFloat(s)
58+
const getRewardsPerSignal = (p: BN, r: BN, t: BN, s: BN): string => {
59+
if (s.eq(0)) {
60+
return '0'
61+
}
62+
return p.times(r.pow(t)).minus(p).div(s).toPrecision(18).toString()
6163
}
6264

6365
// Tracks the accumulated rewards as totalSignalled or supply changes across snapshots
@@ -74,7 +76,7 @@ describe('Rewards', () => {
7476
}
7577

7678
async snapshot() {
77-
this.accumulated = this.accumulated.add(await this.accruedGRT())
79+
this.accumulated = this.accumulated.add(await this.accrued())
7880
this.totalSupply = await grt.totalSupply()
7981
this.totalSignalled = await grt.balanceOf(curation.address)
8082
this.lastUpdatedBlock = await latestBlock()
@@ -88,21 +90,13 @@ describe('Rewards', () => {
8890

8991
async accrued() {
9092
const nBlocks = await this.elapsedBlocks()
91-
return getRewardsPerSignal(
92-
this.totalSupply,
93-
ISSUANCE_RATE_PER_BLOCK,
94-
nBlocks,
95-
this.totalSignalled,
93+
const n = getRewardsPerSignal(
94+
new BN(this.totalSupply.toString()),
95+
new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18),
96+
new BN(nBlocks.toString()),
97+
new BN(this.totalSignalled.toString()),
9698
)
97-
}
98-
99-
async accruedGRT() {
100-
const n = await this.accrued()
101-
return toGRT(n.toString())
102-
}
103-
104-
async accruedRounded() {
105-
return Math.round(await this.accrued())
99+
return toGRT(n)
106100
}
107101
}
108102

@@ -119,10 +113,10 @@ describe('Rewards', () => {
119113
// Contract calculation
120114
const contractAccrued = await rewardsManager.getNewRewardsPerSignal()
121115
// Local calculation
122-
const expectedAccrued = await tracker.accruedRounded()
116+
const expectedAccrued = await tracker.accrued()
123117

124118
// Check
125-
expect(expectedAccrued).eq(toRound(contractAccrued))
119+
expect(toRound(expectedAccrued)).eq(toRound(contractAccrued))
126120
return expectedAccrued
127121
}
128122

@@ -268,8 +262,8 @@ describe('Rewards', () => {
268262
const contractAccrued = await rewardsManager.accRewardsPerSignal()
269263

270264
// Check
271-
const expectedAccrued = await tracker.accruedRounded()
272-
expect(expectedAccrued).eq(toRound(contractAccrued))
265+
const expectedAccrued = await tracker.accrued()
266+
expect(toRound(expectedAccrued)).eq(toRound(contractAccrued))
273267
})
274268

275269
it('update the accumulated rewards per signal state after many blocks', async function () {
@@ -286,8 +280,8 @@ describe('Rewards', () => {
286280
const contractAccrued = await rewardsManager.accRewardsPerSignal()
287281

288282
// Check
289-
const expectedAccrued = await tracker.accruedRounded()
290-
expect(expectedAccrued).eq(toRound(contractAccrued))
283+
const expectedAccrued = await tracker.accrued()
284+
expect(toRound(expectedAccrued)).eq(toRound(contractAccrued))
291285
})
292286
})
293287

@@ -350,13 +344,13 @@ describe('Rewards', () => {
350344
// Check
351345
const contractRewardsSG1 = (await rewardsManager.subgraphs(subgraphDeploymentID1))
352346
.accRewardsForSubgraph
353-
const rewardsPerSignal1 = await tracker1.accruedGRT()
347+
const rewardsPerSignal1 = await tracker1.accrued()
354348
const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther)
355349
expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1))
356350

357351
const contractAccrued = await rewardsManager.accRewardsPerSignal()
358-
const expectedAccrued = await tracker1.accruedRounded()
359-
expect(expectedAccrued).eq(toRound(contractAccrued))
352+
const expectedAccrued = await tracker1.accrued()
353+
expect(toRound(expectedAccrued)).eq(toRound(contractAccrued))
360354

361355
const contractBlockUpdated = await rewardsManager.accRewardsPerSignalLastBlockUpdated()
362356
const expectedBlockUpdated = await latestBlock()
@@ -426,8 +420,8 @@ describe('Rewards', () => {
426420
// Prepare expected results
427421
// NOTE: calculated the expected result manually as the above code has 1 off block difference
428422
// replace with a RewardsManagerMock
429-
const expectedSubgraphRewards = 891695471
430-
const expectedRewardsAT = 51572
423+
const expectedSubgraphRewards = toGRT('891695470')
424+
const expectedRewardsAT = toGRT('51571')
431425

432426
// Update
433427
await rewardsManager.onSubgraphAllocationUpdate(subgraphDeploymentID1)
@@ -439,8 +433,8 @@ describe('Rewards', () => {
439433
)
440434
const contractRewardsAT = subgraph.accRewardsPerAllocatedToken
441435

442-
expect(expectedSubgraphRewards).eq(toRound(contractSubgraphRewards))
443-
expect(expectedRewardsAT).eq(toRound(contractRewardsAT))
436+
expect(toRound(expectedSubgraphRewards)).eq(toRound(contractSubgraphRewards))
437+
expect(toRound(expectedRewardsAT)).eq(toRound(contractRewardsAT))
444438
})
445439
})
446440

@@ -486,37 +480,59 @@ describe('Rewards', () => {
486480
queryFeeCut: BigNumber
487481
cooldownBlocks: number
488482
}
483+
async function setupIndexerAllocation() {
484+
// Setup
485+
await epochManager.setEpochLength(10)
486+
487+
// Update total signalled
488+
const signalled1 = toGRT('1500')
489+
await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1)
489490

490-
async function setupIndexerDelegation(
491+
// Allocate
492+
const tokensToAllocate = toGRT('12500')
493+
await staking.connect(indexer1.signer).stake(tokensToAllocate)
494+
await staking
495+
.connect(indexer1.signer)
496+
.allocate(
497+
subgraphDeploymentID1,
498+
tokensToAllocate,
499+
channelPubKey,
500+
assetHolder.address,
501+
metadata,
502+
)
503+
}
504+
505+
async function setupIndexerAllocationWithDelegation(
491506
tokensToDelegate: BigNumber,
492507
delegationParams: DelegationParameters,
493508
) {
509+
const tokensToAllocate = toGRT('12500')
510+
511+
// Setup
512+
await epochManager.setEpochLength(10)
513+
494514
// Transfer some funds from the curator, I don't want to mint new tokens
495515
await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate)
496516
await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate)
497517

498-
// Delegate
518+
// Stake and set delegation parameters
519+
await staking.connect(indexer1.signer).stake(tokensToAllocate)
499520
await staking
500521
.connect(indexer1.signer)
501522
.setDelegationParameters(
502523
delegationParams.indexingRewardCut,
503524
delegationParams.queryFeeCut,
504525
delegationParams.cooldownBlocks,
505526
)
506-
await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate)
507-
}
508527

509-
async function setupIndexerAllocation() {
510-
// Setup
511-
await epochManager.setEpochLength(10)
528+
// Delegate
529+
await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate)
512530

513531
// Update total signalled
514532
const signalled1 = toGRT('1500')
515533
await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1)
516534

517535
// Allocate
518-
const tokensToAllocate = toGRT('12500')
519-
await staking.connect(indexer1.signer).stake(tokensToAllocate)
520536
await staking
521537
.connect(indexer1.signer)
522538
.allocate(
@@ -569,14 +585,13 @@ describe('Rewards', () => {
569585
it('should distribute rewards on closed allocation w/delegators', async function () {
570586
// Setup
571587
const delegationParams = {
572-
indexingRewardCut: toBN('50000'), // 5%
588+
indexingRewardCut: toBN('823000'), // 82.30%
573589
queryFeeCut: toBN('80000'), // 8%
574590
cooldownBlocks: 5,
575591
}
576592
const tokensToDelegate = toGRT('2000')
577593

578-
await setupIndexerDelegation(tokensToDelegate, delegationParams)
579-
await setupIndexerAllocation()
594+
await setupIndexerAllocationWithDelegation(tokensToDelegate, delegationParams)
580595

581596
// Jump
582597
await advanceBlocks(await epochManager.epochLength())
@@ -597,7 +612,7 @@ describe('Rewards', () => {
597612
// Check that rewards are put into indexer stake (only indexer cut)
598613
// Check that rewards are put into delegators pool accordingly
599614
// NOTE: calculated manually on a spreadsheet
600-
const expectedIndexingRewards = toGRT('1471954234')
615+
const expectedIndexingRewards = toGRT('1454109066')
601616
// Calculate delegators cut
602617
const indexerRewards = delegationParams.indexingRewardCut
603618
.mul(expectedIndexingRewards)

test/staking/delegation.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ describe('Staking::Delegation', () => {
177177
}
178178

179179
// Distribute test funds
180-
for (const wallet of [me, indexer, assetHolder]) {
180+
for (const wallet of [me, indexer, indexer2, assetHolder]) {
181181
await grt.connect(governor.signer).mint(wallet.address, toGRT('1000000'))
182182
await grt.connect(wallet.signer).approve(staking.address, toGRT('1000000'))
183183
}
@@ -302,19 +302,30 @@ describe('Staking::Delegation', () => {
302302
})
303303

304304
describe('lifecycle', function () {
305+
beforeEach(async function () {
306+
// Stake some funds as indexer
307+
await staking.connect(indexer.signer).stake(toGRT('1000'))
308+
})
309+
305310
describe('delegate', function () {
306-
it('reject to delegate zero tokens', async function () {
311+
it('reject delegate with zero tokens', async function () {
307312
const tokensToDelegate = toGRT('0')
308313
const tx = staking.connect(delegator.signer).delegate(indexer.address, tokensToDelegate)
309314
await expect(tx).revertedWith('Cannot delegate zero tokens')
310315
})
311316

312-
it('reject to delegate to empty address', async function () {
317+
it('reject delegate to empty address', async function () {
313318
const tokensToDelegate = toGRT('100')
314319
const tx = staking.connect(delegator.signer).delegate(AddressZero, tokensToDelegate)
315320
await expect(tx).revertedWith('Cannot delegate to empty address')
316321
})
317322

323+
it('reject delegate to non-staked indexer', async function () {
324+
const tokensToDelegate = toGRT('100')
325+
const tx = staking.connect(delegator.signer).delegate(me.address, tokensToDelegate)
326+
await expect(tx).revertedWith('Cannot delegate to non-staked indexer')
327+
})
328+
318329
it('should delegate tokens and account shares proportionally', async function () {
319330
// Multiple delegations should work
320331
await shouldDelegate(delegator, toGRT('1234'))
@@ -409,6 +420,9 @@ describe('Staking::Delegation', () => {
409420
await advanceToNextEpoch(epochManager) // epoch 1
410421
await advanceToNextEpoch(epochManager) // epoch 2
411422

423+
// We stake on indexer2 so the delegator is able to re-delegate to it
424+
// if we didn't do it, it will revert because of indexer2 not havings stake
425+
await staking.connect(indexer2.signer).stake(toGRT('1000'))
412426
// Withdraw
413427
await shouldWithdrawDelegated(delegator, indexer2.address, tokensToWithdraw)
414428
})

0 commit comments

Comments
 (0)