Skip to content

Commit 7c6e27d

Browse files
authored
Merge pull request #569 from graphprotocol/pcv/rewards-supply-fixes
Rewards: fix calculation by using a snapshot of token supply at the time that signal was last updated
2 parents 639f9cd + 7d00cbf commit 7c6e27d

File tree

7 files changed

+147
-32
lines changed

7 files changed

+147
-32
lines changed

config/graph.mainnet.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ contracts:
106106
proxy: true
107107
init:
108108
controller: "${{Controller.address}}"
109-
issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13
109+
calls:
110+
- fn: "setIssuanceRate"
111+
_issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13
110112
AllocationExchange:
111113
init:
112114
graphToken: "${{GraphToken.address}}"

contracts/rewards/RewardsManager.sol

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import "./IRewardsManager.sol";
2727
* These functions may overestimate the actual rewards due to changes in the total supply
2828
* until the actual takeRewards function is called.
2929
*/
30-
contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsManager {
30+
contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager {
3131
using SafeMath for uint256;
3232

3333
uint256 private constant TOKEN_DECIMALS = 1e18;
@@ -68,11 +68,8 @@ contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsMa
6868
/**
6969
* @dev Initialize this contract.
7070
*/
71-
function initialize(address _controller, uint256 _issuanceRate) external onlyImpl {
71+
function initialize(address _controller) external onlyImpl {
7272
Managed._initialize(_controller);
73-
74-
// Settings
75-
_setIssuanceRate(_issuanceRate);
7673
}
7774

7875
// -- Config --
@@ -224,7 +221,7 @@ contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsMa
224221
}
225222

226223
uint256 r = issuanceRate;
227-
uint256 p = graphToken.totalSupply();
224+
uint256 p = tokenSupplySnapshot;
228225
uint256 a = p.mul(_pow(r, t, TOKEN_DECIMALS)).div(TOKEN_DECIMALS);
229226

230227
// New issuance of tokens during time steps
@@ -315,6 +312,7 @@ contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsMa
315312
function updateAccRewardsPerSignal() public override returns (uint256) {
316313
accRewardsPerSignal = getAccRewardsPerSignal();
317314
accRewardsPerSignalLastBlockUpdated = block.number;
315+
tokenSupplySnapshot = graphToken().totalSupply();
318316
return accRewardsPerSignal;
319317
}
320318

contracts/rewards/RewardsManagerStorage.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage {
2626
// Minimum amount of signaled tokens on a subgraph required to accrue rewards
2727
uint256 public minimumSubgraphSignal;
2828
}
29+
30+
contract RewardsManagerV3Storage is RewardsManagerV2Storage {
31+
// Snapshot of the total supply of GRT when accRewardsPerSignal was last updated
32+
uint256 public tokenSupplySnapshot;
33+
}

test/lib/deployment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export async function deployRewardsManager(
226226
return network.deployContractWithProxy(
227227
proxyAdmin,
228228
'RewardsManager',
229-
[controller, defaults.rewards.issuanceRate],
229+
[controller],
230230
deployer,
231231
) as unknown as RewardsManager
232232
}

test/lib/fixtures.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export class NetworkFixture {
7575
await staking.connect(deployer).setSlasher(slasherAddress, true)
7676
await grt.connect(deployer).addMinter(rewardsManager.address)
7777
await gns.connect(deployer).approveAll()
78+
await rewardsManager.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate)
7879

7980
// Unpause the protocol
8081
await controller.connect(deployer).setPaused(false)
@@ -95,6 +96,7 @@ export class NetworkFixture {
9596

9697
async setUp(): Promise<void> {
9798
this.lastSnapshotId = await evmSnapshot()
99+
provider().send('evm_setAutomine', [true])
98100
}
99101

100102
async tearDown(): Promise<void> {

test/rewards/rewards.test.ts

Lines changed: 131 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
formatGRT,
2424
Account,
2525
advanceToNextEpoch,
26+
provider,
2627
} from '../lib/testHelpers'
2728

2829
const MAX_PPM = 1000000
@@ -50,12 +51,15 @@ describe('Rewards', () => {
5051
let rewardsManagerMock: RewardsManagerMock
5152

5253
// Derive some channel keys for each indexer used to sign attestations
53-
const channelKey = deriveChannelKey()
54+
const channelKey1 = deriveChannelKey()
55+
const channelKey2 = deriveChannelKey()
5456

5557
const subgraphDeploymentID1 = randomHexBytes()
5658
const subgraphDeploymentID2 = randomHexBytes()
5759

58-
const allocationID = channelKey.address
60+
const allocationID1 = channelKey1.address
61+
const allocationID2 = channelKey2.address
62+
5963
const metadata = HashZero
6064

6165
const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 5% rewards
@@ -97,6 +101,10 @@ describe('Rewards', () => {
97101

98102
async accrued() {
99103
const nBlocks = await this.elapsedBlocks()
104+
return this.accruedByElapsed(nBlocks)
105+
}
106+
107+
async accruedByElapsed(nBlocks: BigNumber | number) {
100108
const n = getRewardsPerSignal(
101109
new BN(this.totalSupply.toString()),
102110
new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18),
@@ -395,9 +403,9 @@ describe('Rewards', () => {
395403
indexer1.address,
396404
subgraphDeploymentID1,
397405
tokensToAllocate,
398-
allocationID,
406+
allocationID1,
399407
metadata,
400-
await channelKey.generateProof(indexer1.address),
408+
await channelKey1.generateProof(indexer1.address),
401409
)
402410

403411
// Jump
@@ -433,9 +441,9 @@ describe('Rewards', () => {
433441
indexer1.address,
434442
subgraphDeploymentID1,
435443
tokensToAllocate,
436-
allocationID,
444+
allocationID1,
437445
metadata,
438-
await channelKey.generateProof(indexer1.address),
446+
await channelKey1.generateProof(indexer1.address),
439447
)
440448

441449
// Jump
@@ -477,16 +485,16 @@ describe('Rewards', () => {
477485
indexer1.address,
478486
subgraphDeploymentID1,
479487
tokensToAllocate,
480-
allocationID,
488+
allocationID1,
481489
metadata,
482-
await channelKey.generateProof(indexer1.address),
490+
await channelKey1.generateProof(indexer1.address),
483491
)
484492

485493
// Jump
486494
await advanceBlocks(ISSUANCE_RATE_PERIODS)
487495

488496
// Rewards
489-
const contractRewards = await rewardsManager.getRewards(allocationID)
497+
const contractRewards = await rewardsManager.getRewards(allocationID1)
490498

491499
// We trust using this function in the test because we tested it
492500
// standalone in a previous test
@@ -523,9 +531,9 @@ describe('Rewards', () => {
523531
indexer1.address,
524532
subgraphDeploymentID1,
525533
tokensToAllocate,
526-
allocationID,
534+
allocationID1,
527535
metadata,
528-
await channelKey.generateProof(indexer1.address),
536+
await channelKey1.generateProof(indexer1.address),
529537
)
530538
}
531539

@@ -566,9 +574,9 @@ describe('Rewards', () => {
566574
indexer1.address,
567575
subgraphDeploymentID1,
568576
tokensToAllocate,
569-
allocationID,
577+
allocationID1,
570578
metadata,
571-
await channelKey.generateProof(indexer1.address),
579+
await channelKey1.generateProof(indexer1.address),
572580
)
573581
}
574582

@@ -599,11 +607,11 @@ describe('Rewards', () => {
599607
// Close allocation. At this point rewards should be collected for that indexer
600608
const tx = await staking
601609
.connect(indexer1.signer)
602-
.closeAllocation(allocationID, randomHexBytes())
610+
.closeAllocation(allocationID1, randomHexBytes())
603611
const receipt = await tx.wait()
604612
const event = rewardsManager.interface.parseLog(receipt.logs[1]).args
605613
expect(event.indexer).eq(indexer1.address)
606-
expect(event.allocationID).eq(allocationID)
614+
expect(event.allocationID).eq(allocationID1)
607615
expect(event.epoch).eq(await epochManager.currentEpoch())
608616
expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards))
609617

@@ -658,11 +666,11 @@ describe('Rewards', () => {
658666
// Close allocation. At this point rewards should be collected for that indexer
659667
const tx = await staking
660668
.connect(indexer1.signer)
661-
.closeAllocation(allocationID, randomHexBytes())
669+
.closeAllocation(allocationID1, randomHexBytes())
662670
const receipt = await tx.wait()
663671
const event = rewardsManager.interface.parseLog(receipt.logs[1]).args
664672
expect(event.indexer).eq(indexer1.address)
665-
expect(event.allocationID).eq(allocationID)
673+
expect(event.allocationID).eq(allocationID1)
666674
expect(event.epoch).eq(await epochManager.currentEpoch())
667675
expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards))
668676

@@ -710,7 +718,7 @@ describe('Rewards', () => {
710718
const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address)
711719

712720
// Close allocation. At this point rewards should be collected for that indexer
713-
await staking.connect(indexer1.signer).closeAllocation(allocationID, randomHexBytes())
721+
await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes())
714722

715723
// After state
716724
const afterTokenSupply = await grt.totalSupply()
@@ -756,10 +764,10 @@ describe('Rewards', () => {
756764
await advanceToNextEpoch(epochManager)
757765

758766
// Close allocation. At this point rewards should be collected for that indexer
759-
const tx = staking.connect(indexer1.signer).closeAllocation(allocationID, randomHexBytes())
767+
const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes())
760768
await expect(tx)
761769
.emit(rewardsManager, 'RewardsDenied')
762-
.withArgs(indexer1.address, allocationID, await epochManager.currentEpoch())
770+
.withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch())
763771
})
764772
})
765773
})
@@ -791,9 +799,9 @@ describe('Rewards', () => {
791799
indexer1.address,
792800
subgraphDeploymentID1,
793801
tokensToAllocate,
794-
allocationID,
802+
allocationID1,
795803
metadata,
796-
await channelKey.generateProof(indexer1.address),
804+
await channelKey1.generateProof(indexer1.address),
797805
)
798806

799807
// Jump
@@ -804,7 +812,107 @@ describe('Rewards', () => {
804812
await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0)
805813

806814
// Close allocation. At this point rewards should be collected for that indexer
807-
await staking.connect(indexer1.signer).closeAllocation(allocationID, randomHexBytes())
815+
await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes())
816+
})
817+
})
818+
819+
describe('multiple allocations', function () {
820+
it('two allocations in the same block with a GRT burn in the middle should succeed', async function () {
821+
// If rewards are not monotonically increasing, this can trigger
822+
// a subtraction overflow error as seen in mainnet tx:
823+
// 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab
824+
await advanceToNextEpoch(epochManager)
825+
826+
// Setup
827+
await epochManager.setEpochLength(10)
828+
829+
// Update total signalled
830+
const signalled1 = toGRT('1500')
831+
await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0)
832+
833+
// Stake
834+
const tokensToStake = toGRT('12500')
835+
await staking.connect(indexer1.signer).stake(tokensToStake)
836+
837+
// Allocate simultaneously, burning in the middle
838+
const tokensToAlloc = toGRT('5000')
839+
await provider().send('evm_setAutomine', [false])
840+
const tx1 = await staking
841+
.connect(indexer1.signer)
842+
.allocateFrom(
843+
indexer1.address,
844+
subgraphDeploymentID1,
845+
tokensToAlloc,
846+
allocationID1,
847+
metadata,
848+
await channelKey1.generateProof(indexer1.address),
849+
)
850+
const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1))
851+
const tx3 = await staking
852+
.connect(indexer1.signer)
853+
.allocateFrom(
854+
indexer1.address,
855+
subgraphDeploymentID1,
856+
tokensToAlloc,
857+
allocationID2,
858+
metadata,
859+
await channelKey2.generateProof(indexer1.address),
860+
)
861+
862+
await provider().send('evm_mine', [])
863+
await provider().send('evm_setAutomine', [true])
864+
865+
await expect(tx1).emit(staking, 'AllocationCreated')
866+
await expect(tx2).emit(grt, 'Transfer')
867+
await expect(tx3).emit(staking, 'AllocationCreated')
868+
})
869+
it('two simultanous-similar allocations should get same amount of rewards', async function () {
870+
await advanceToNextEpoch(epochManager)
871+
872+
// Setup
873+
await epochManager.setEpochLength(10)
874+
875+
// Update total signalled
876+
const signalled1 = toGRT('1500')
877+
await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0)
878+
879+
// Stake
880+
const tokensToStake = toGRT('12500')
881+
await staking.connect(indexer1.signer).stake(tokensToStake)
882+
883+
// Allocate simultaneously
884+
const tokensToAlloc = toGRT('5000')
885+
const tx1 = await staking.populateTransaction.allocateFrom(
886+
indexer1.address,
887+
subgraphDeploymentID1,
888+
tokensToAlloc,
889+
allocationID1,
890+
metadata,
891+
await channelKey1.generateProof(indexer1.address),
892+
)
893+
const tx2 = await staking.populateTransaction.allocateFrom(
894+
indexer1.address,
895+
subgraphDeploymentID1,
896+
tokensToAlloc,
897+
allocationID2,
898+
metadata,
899+
await channelKey2.generateProof(indexer1.address),
900+
)
901+
await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data])
902+
903+
// Jump
904+
await advanceToNextEpoch(epochManager)
905+
906+
// Close allocations simultaneously
907+
const tx3 = await staking.populateTransaction.closeAllocation(allocationID1, randomHexBytes())
908+
const tx4 = await staking.populateTransaction.closeAllocation(allocationID2, randomHexBytes())
909+
const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data])
910+
911+
// Both allocations should receive the same amount of rewards
912+
const receipt = await tx5.wait()
913+
const event1 = rewardsManager.interface.parseLog(receipt.logs[1]).args
914+
const event2 = rewardsManager.interface.parseLog(receipt.logs[5]).args
915+
expect(event1.amount).eq(event2.amount)
808916
})
809917
})
810918
})

test/staking/allocation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
advanceEpochs,
1919
} from '../lib/testHelpers'
2020

21-
const { AddressZero, HashZero } = constants
21+
const { AddressZero } = constants
2222

2323
const MAX_PPM = toBN('1000000')
2424

0 commit comments

Comments
 (0)