-
Notifications
You must be signed in to change notification settings - Fork 167
Expand file tree
/
Copy pathAllocationManager.sol
More file actions
501 lines (448 loc) · 21.7 KB
/
AllocationManager.sol
File metadata and controls
501 lines (448 loc) · 21.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.27;
import { IGraphPayments } from "@graphprotocol/horizon/contracts/interfaces/IGraphPayments.sol";
import { IGraphToken } from "@graphprotocol/contracts/contracts/token/IGraphToken.sol";
import { IHorizonStakingTypes } from "@graphprotocol/horizon/contracts/interfaces/internal/IHorizonStakingTypes.sol";
import { GraphDirectory } from "@graphprotocol/horizon/contracts/utilities/GraphDirectory.sol";
import { AllocationManagerV1Storage } from "./AllocationManagerStorage.sol";
import { TokenUtils } from "@graphprotocol/contracts/contracts/utils/TokenUtils.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { EIP712Upgradeable } from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol";
import { Allocation } from "../libraries/Allocation.sol";
import { LegacyAllocation } from "../libraries/LegacyAllocation.sol";
import { PPMMath } from "@graphprotocol/horizon/contracts/libraries/PPMMath.sol";
import { ProvisionTracker } from "@graphprotocol/horizon/contracts/data-service/libraries/ProvisionTracker.sol";
/**
* @title AllocationManager contract
* @notice A helper contract implementing allocation lifecycle management.
* Allows opening, resizing, and closing allocations, as well as collecting indexing rewards by presenting a Proof
* of Indexing (POI).
*/
abstract contract AllocationManager is EIP712Upgradeable, GraphDirectory, AllocationManagerV1Storage {
using ProvisionTracker for mapping(address => uint256);
using Allocation for mapping(address => Allocation.State);
using Allocation for Allocation.State;
using LegacyAllocation for mapping(address => LegacyAllocation.State);
using PPMMath for uint256;
using TokenUtils for IGraphToken;
///@dev EIP712 typehash for allocation proof
bytes32 private constant EIP712_ALLOCATION_PROOF_TYPEHASH =
keccak256("AllocationIdProof(address indexer,address allocationId)");
/**
* @notice Emitted when an indexer creates an allocation
* @param indexer The address of the indexer
* @param allocationId The id of the allocation
* @param subgraphDeploymentId The id of the subgraph deployment
* @param tokens The amount of tokens allocated
*/
event AllocationCreated(
address indexed indexer,
address indexed allocationId,
bytes32 indexed subgraphDeploymentId,
uint256 tokens
);
/**
* @notice Emitted when an indexer collects indexing rewards for an allocation
* @param indexer The address of the indexer
* @param allocationId The id of the allocation
* @param subgraphDeploymentId The id of the subgraph deployment
* @param tokensRewards The amount of tokens collected
* @param tokensIndexerRewards The amount of tokens collected for the indexer
* @param tokensDelegationRewards The amount of tokens collected for delegators
* @param poi The POI presented
* @param currentEpoch The current epoch
*/
event IndexingRewardsCollected(
address indexed indexer,
address indexed allocationId,
bytes32 indexed subgraphDeploymentId,
uint256 tokensRewards,
uint256 tokensIndexerRewards,
uint256 tokensDelegationRewards,
bytes32 poi,
uint256 currentEpoch
);
/**
* @notice Emitted when an indexer resizes an allocation
* @param indexer The address of the indexer
* @param allocationId The id of the allocation
* @param subgraphDeploymentId The id of the subgraph deployment
* @param newTokens The new amount of tokens allocated
* @param oldTokens The old amount of tokens allocated
*/
event AllocationResized(
address indexed indexer,
address indexed allocationId,
bytes32 indexed subgraphDeploymentId,
uint256 newTokens,
uint256 oldTokens
);
/**
* @dev Emitted when an indexer closes an allocation
* @param indexer The address of the indexer
* @param allocationId The id of the allocation
* @param subgraphDeploymentId The id of the subgraph deployment
* @param tokens The amount of tokens allocated
*/
event AllocationClosed(
address indexed indexer,
address indexed allocationId,
bytes32 indexed subgraphDeploymentId,
uint256 tokens
);
/**
* @notice Emitted when a legacy allocation is migrated into the subgraph service
* @param indexer The address of the indexer
* @param allocationId The id of the allocation
* @param subgraphDeploymentId The id of the subgraph deployment
*/
event LegacyAllocationMigrated(
address indexed indexer,
address indexed allocationId,
bytes32 indexed subgraphDeploymentId
);
/**
* @notice Emitted when an indexer sets a new indexing rewards destination
* @param indexer The address of the indexer
* @param rewardsDestination The address where indexing rewards should be sent
*/
event RewardsDestinationSet(address indexed indexer, address indexed rewardsDestination);
/**
* @notice Emitted when the maximum POI staleness is updated
* @param maxPOIStaleness The max POI staleness in seconds
*/
event MaxPOIStalenessSet(uint256 maxPOIStaleness);
/**
* @notice Thrown when an allocation proof is invalid
* Both `signer` and `allocationId` should match for a valid proof.
* @param signer The address that signed the proof
* @param allocationId The id of the allocation
*/
error AllocationManagerInvalidAllocationProof(address signer, address allocationId);
/**
* @notice Thrown when attempting to create an allocation with a zero allocation id
*/
error AllocationManagerInvalidZeroAllocationId();
/**
* @notice Thrown when attempting to collect indexing rewards on a closed allocationl
* @param allocationId The id of the allocation
*/
error AllocationManagerAllocationClosed(address allocationId);
/**
* @notice Thrown when attempting to resize an allocation with the same size
* @param allocationId The id of the allocation
* @param tokens The amount of tokens
*/
error AllocationManagerAllocationSameSize(address allocationId, uint256 tokens);
/**
* @notice Initializes the contract and parent contracts
*/
// solhint-disable-next-line func-name-mixedcase
function __AllocationManager_init(string memory _name, string memory _version) internal onlyInitializing {
__EIP712_init(_name, _version);
__AllocationManager_init_unchained(_name, _version);
}
/**
* @notice Initializes the contract
*/
// solhint-disable-next-line func-name-mixedcase
function __AllocationManager_init_unchained(
string memory _name,
string memory _version
) internal onlyInitializing {}
/**
* @notice Imports a legacy allocation id into the subgraph service
* This is a governor only action that is required to prevent indexers from re-using allocation ids from the
* legacy staking contract. It will revert with LegacyAllocationAlreadyMigrated if the allocation has already been migrated.
* @param _indexer The address of the indexer
* @param _allocationId The id of the allocation
* @param _subgraphDeploymentId The id of the subgraph deployment
*/
function _migrateLegacyAllocation(address _indexer, address _allocationId, bytes32 _subgraphDeploymentId) internal {
_legacyAllocations.migrate(_indexer, _allocationId, _subgraphDeploymentId);
emit LegacyAllocationMigrated(_indexer, _allocationId, _subgraphDeploymentId);
}
/**
* @notice Create an allocation
* @dev The `_allocationProof` is a 65-bytes Ethereum signed message of `keccak256(indexerAddress,allocationId)`
*
* Requirements:
* - `_allocationId` must not be the zero address
*
* Emits a {AllocationCreated} event
*
* @param _indexer The address of the indexer
* @param _allocationId The id of the allocation to be created
* @param _subgraphDeploymentId The subgraph deployment Id
* @param _tokens The amount of tokens to allocate
* @param _allocationProof Signed proof of allocation id address ownership
* @param _delegationRatio The delegation ratio to consider when locking tokens
*/
function _allocate(
address _indexer,
address _allocationId,
bytes32 _subgraphDeploymentId,
uint256 _tokens,
bytes memory _allocationProof,
uint32 _delegationRatio
) internal returns (Allocation.State memory) {
require(_allocationId != address(0), AllocationManagerInvalidZeroAllocationId());
_verifyAllocationProof(_indexer, _allocationId, _allocationProof);
// Ensure allocation id is not reused
// need to check both subgraph service (on allocations.create()) and legacy allocations
_legacyAllocations.revertIfExists(_graphStaking(), _allocationId);
Allocation.State memory allocation = _allocations.create(
_indexer,
_allocationId,
_subgraphDeploymentId,
_tokens,
_graphRewardsManager().onSubgraphAllocationUpdate(_subgraphDeploymentId)
);
// Check that the indexer has enough tokens available
// Note that the delegation ratio ensures overdelegation cannot be used
allocationProvisionTracker.lock(_graphStaking(), _indexer, _tokens, _delegationRatio);
// Update total allocated tokens for the subgraph deployment
_subgraphAllocatedTokens[allocation.subgraphDeploymentId] =
_subgraphAllocatedTokens[allocation.subgraphDeploymentId] +
allocation.tokens;
emit AllocationCreated(_indexer, _allocationId, _subgraphDeploymentId, allocation.tokens);
return allocation;
}
/**
* @notice Present a POI to collect indexing rewards for an allocation
* This function will mint indexing rewards using the {RewardsManager} and distribute them to the indexer and delegators.
*
* To qualify for indexing rewards:
* - POI must be non-zero
* - POI must not be stale, i.e: older than `maxPOIStaleness`
* - allocation must not be altruistic (allocated tokens = 0)
*
* Note that indexers are required to periodically (at most every `maxPOIStaleness`) present POIs to collect rewards.
* Rewards will not be issued to stale POIs, which means that indexers are advised to present a zero POI if they are
* unable to present a valid one to prevent being locked out of future rewards.
*
* Emits a {IndexingRewardsCollected} event.
*
* @param _allocationId The id of the allocation to collect rewards for
* @param _poi The POI being presented
*/
function _collectIndexingRewards(
address _allocationId,
bytes32 _poi,
uint32 _delegationRatio
) internal returns (uint256) {
Allocation.State memory allocation = _allocations.get(_allocationId);
require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId));
// Mint indexing rewards if all conditions are met
uint256 tokensRewards = (!allocation.isStale(maxPOIStaleness) &&
!allocation.isAltruistic() &&
_poi != bytes32(0))
? _graphRewardsManager().takeRewards(_allocationId)
: 0;
// ... but we still take a snapshot to ensure the rewards are not accumulated for the next valid POI
_allocations.snapshotRewards(
_allocationId,
_graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId)
);
_allocations.presentPOI(_allocationId);
// Any pending rewards should have been collected now
_allocations.clearPendingRewards(_allocationId);
uint256 tokensIndexerRewards = 0;
uint256 tokensDelegationRewards = 0;
if (tokensRewards != 0) {
// Distribute rewards to delegators
uint256 delegatorCut = _graphStaking().getDelegationFeeCut(
allocation.indexer,
address(this),
IGraphPayments.PaymentTypes.IndexingRewards
);
IHorizonStakingTypes.DelegationPool memory delegationPool = _graphStaking().getDelegationPool(
allocation.indexer,
address(this)
);
// If delegation pool has no shares then we don't need to distribute rewards to delegators
tokensDelegationRewards = delegationPool.shares > 0 ? tokensRewards.mulPPM(delegatorCut) : 0;
if (tokensDelegationRewards > 0) {
_graphToken().approve(address(_graphStaking()), tokensDelegationRewards);
_graphStaking().addToDelegationPool(allocation.indexer, address(this), tokensDelegationRewards);
}
// Distribute rewards to indexer
tokensIndexerRewards = tokensRewards - tokensDelegationRewards;
if (tokensIndexerRewards > 0) {
address rewardsDestination = rewardsDestination[allocation.indexer];
if (rewardsDestination == address(0)) {
_graphToken().approve(address(_graphStaking()), tokensIndexerRewards);
_graphStaking().stakeToProvision(allocation.indexer, address(this), tokensIndexerRewards);
} else {
_graphToken().pushTokens(rewardsDestination, tokensIndexerRewards);
}
}
}
emit IndexingRewardsCollected(
allocation.indexer,
_allocationId,
allocation.subgraphDeploymentId,
tokensRewards,
tokensIndexerRewards,
tokensDelegationRewards,
_poi,
_graphEpochManager().currentEpoch()
);
// Check if the indexer is over-allocated and close the allocation if necessary
if (_isOverAllocated(allocation.indexer, _delegationRatio)) {
_closeAllocation(_allocationId);
}
return tokensRewards;
}
/**
* @notice Resize an allocation
* @dev Will lock or release tokens in the provision tracker depending on the new allocation size.
* Rewards accrued but not issued before the resize will be accounted for as pending rewards.
* These will be paid out when the indexer presents a POI.
*
* Requirements:
* - `_indexer` must be the owner of the allocation
* - Allocation must be open
* - `_tokens` must be different from the current allocation size
*
* Emits a {AllocationResized} event.
*
* @param _allocationId The id of the allocation to be resized
* @param _tokens The new amount of tokens to allocate
* @param _delegationRatio The delegation ratio to consider when locking tokens
*/
function _resizeAllocation(
address _allocationId,
uint256 _tokens,
uint32 _delegationRatio
) internal returns (Allocation.State memory) {
Allocation.State memory allocation = _allocations.get(_allocationId);
require(allocation.isOpen(), AllocationManagerAllocationClosed(_allocationId));
require(_tokens != allocation.tokens, AllocationManagerAllocationSameSize(_allocationId, _tokens));
// Update provision tracker
uint256 oldTokens = allocation.tokens;
if (_tokens > oldTokens) {
allocationProvisionTracker.lock(_graphStaking(), allocation.indexer, _tokens - oldTokens, _delegationRatio);
} else {
allocationProvisionTracker.release(allocation.indexer, oldTokens - _tokens);
}
// Calculate rewards that have been accrued since the last snapshot but not yet issued
uint256 accRewardsPerAllocatedToken = _graphRewardsManager().onSubgraphAllocationUpdate(
allocation.subgraphDeploymentId
);
uint256 accRewardsPerAllocatedTokenPending = !allocation.isAltruistic()
? accRewardsPerAllocatedToken - allocation.accRewardsPerAllocatedToken
: 0;
// Update the allocation
_allocations[_allocationId].tokens = _tokens;
_allocations[_allocationId].accRewardsPerAllocatedToken = accRewardsPerAllocatedToken;
_allocations[_allocationId].accRewardsPending += _graphRewardsManager().calcRewards(
oldTokens,
accRewardsPerAllocatedTokenPending
);
// Update total allocated tokens for the subgraph deployment
if (_tokens > oldTokens) {
_subgraphAllocatedTokens[allocation.subgraphDeploymentId] += (_tokens - oldTokens);
} else {
_subgraphAllocatedTokens[allocation.subgraphDeploymentId] -= (oldTokens - _tokens);
}
emit AllocationResized(allocation.indexer, _allocationId, allocation.subgraphDeploymentId, _tokens, oldTokens);
return _allocations[_allocationId];
}
/**
* @notice Close an allocation
* Does not require presenting a POI, use {_collectIndexingRewards} to present a POI and collect rewards
* @dev Note that allocations are nowlong lived. All service payments, including indexing rewards, should be collected periodically
* without the need of closing the allocation. Allocations should only be closed when indexers want to reclaim the allocated
* tokens for other purposes.
*
* Emits a {AllocationClosed} event
*
* @param _allocationId The id of the allocation to be closed
*/
function _closeAllocation(address _allocationId) internal {
Allocation.State memory allocation = _allocations.get(_allocationId);
// Take rewards snapshot to prevent other allos from counting tokens from this allo
_allocations.snapshotRewards(
_allocationId,
_graphRewardsManager().onSubgraphAllocationUpdate(allocation.subgraphDeploymentId)
);
_allocations.close(_allocationId);
allocationProvisionTracker.release(allocation.indexer, allocation.tokens);
// Update total allocated tokens for the subgraph deployment
_subgraphAllocatedTokens[allocation.subgraphDeploymentId] =
_subgraphAllocatedTokens[allocation.subgraphDeploymentId] -
allocation.tokens;
emit AllocationClosed(allocation.indexer, _allocationId, allocation.subgraphDeploymentId, allocation.tokens);
}
/**
* @notice Sets the rewards destination for an indexer to receive indexing rewards
* @dev Emits a {RewardsDestinationSet} event
* @param _rewardsDestination The address where indexing rewards should be sent
*/
function _setRewardsDestination(address _indexer, address _rewardsDestination) internal {
rewardsDestination[_indexer] = _rewardsDestination;
emit RewardsDestinationSet(_indexer, _rewardsDestination);
}
/**
* @notice Sets the maximum amount of time, in seconds, allowed between presenting POIs to qualify for indexing rewards
* @dev Emits a {MaxPOIStalenessSet} event
* @param _maxPOIStaleness The max POI staleness in seconds
*/
function _setMaxPOIStaleness(uint256 _maxPOIStaleness) internal {
maxPOIStaleness = _maxPOIStaleness;
emit MaxPOIStalenessSet(_maxPOIStaleness);
}
/**
* @notice Gets the details of an allocation
* @param _allocationId The id of the allocation
*/
function _getAllocation(address _allocationId) internal view returns (Allocation.State memory) {
return _allocations.get(_allocationId);
}
/**
* @notice Gets the details of a legacy allocation
* @param _allocationId The id of the legacy allocation
*/
function _getLegacyAllocation(address _allocationId) internal view returns (LegacyAllocation.State memory) {
return _legacyAllocations.get(_allocationId);
}
/**
* @notice Encodes the allocation proof for EIP712 signing
* @param _indexer The address of the indexer
* @param _allocationId The id of the allocation
*/
function _encodeAllocationProof(address _indexer, address _allocationId) internal view returns (bytes32) {
return _hashTypedDataV4(keccak256(abi.encode(EIP712_ALLOCATION_PROOF_TYPEHASH, _indexer, _allocationId)));
}
/**
* @notice Checks if an allocation is over-allocated
* @param _indexer The address of the indexer
* @param _delegationRatio The delegation ratio to consider when locking tokens
* @return True if the allocation is over-allocated, false otherwise
*/
function _isOverAllocated(address _indexer, uint32 _delegationRatio) internal view returns (bool) {
return !allocationProvisionTracker.check(_graphStaking(), _indexer, _delegationRatio);
}
/**
* @notice Returns the number of free tokens for an indexer
* @param _indexer The address of the indexer
* @param _delegationRatio The delegation ratio to consider when locking tokens
* @return The number of free tokens
*/
function _getAllocationTokensFree(address _indexer, uint32 _delegationRatio) internal view returns (uint256) {
return allocationProvisionTracker.getTokensFree(_graphStaking(), _indexer, _delegationRatio);
}
/**
* @notice Verifies ownership of an allocation id by verifying an EIP712 allocation proof
* @dev Requirements:
* - Signer must be the allocation id address
* @param _indexer The address of the indexer
* @param _allocationId The id of the allocation
* @param _proof The EIP712 proof, an EIP712 signed message of (indexer,allocationId)
*/
function _verifyAllocationProof(address _indexer, address _allocationId, bytes memory _proof) private view {
address signer = ECDSA.recover(_encodeAllocationProof(_indexer, _allocationId), _proof);
require(signer == _allocationId, AllocationManagerInvalidAllocationProof(signer, _allocationId));
}
}