Skip to content

Commit 4052b9d

Browse files
committed
staking: mark in the AllocationClosed emitted event if the caller is a delegator
closeAllocation() can be called by an indexer's delegator to force close an allocation held open potentially indefinitely by the indexer. When a delegator calls closeAllocation() it can't present the POI, so the indexer should not collect rewards nor be slashed. To advertise external actors that the caller was not the indexer but a delegator we included a `bool isDelegator` attribute in `AllocationClosed`.
1 parent 0b80863 commit 4052b9d

File tree

2 files changed

+69
-22
lines changed

2 files changed

+69
-22
lines changed

contracts/staking/Staking.sol

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
129129
* An amount of `tokens` get unallocated from `subgraphDeploymentID`.
130130
* The `effectiveAllocation` are the tokens allocated from creation to closing.
131131
* This event also emits the POI (proof of indexing) submitted by the indexer.
132+
* `isDelegator` is true if the sender was one of the indexer's delegators.
132133
*/
133134
event AllocationClosed(
134135
address indexed indexer,
@@ -138,7 +139,8 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
138139
address indexed allocationID,
139140
uint256 effectiveAllocation,
140141
address sender,
141-
bytes32 poi
142+
bytes32 poi,
143+
bool isDelegator
142144
);
143145

144146
/**
@@ -189,13 +191,6 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
189191
return msg.sender == _indexer || isOperator(msg.sender, _indexer) == true;
190192
}
191193

192-
/**
193-
* @dev Check if the caller is authorized (indexer, operator or delegator)
194-
*/
195-
function _isAuthOrDelegator(address _indexer) private view returns (bool) {
196-
return _isAuth(_indexer) || delegationPools[_indexer].delegators[msg.sender].shares > 0;
197-
}
198-
199194
/**
200195
* @dev Initialize this contract.
201196
*/
@@ -584,6 +579,16 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
584579
return delegationPools[_indexer].delegators[_delegator];
585580
}
586581

582+
/**
583+
* @dev Return whether the delegator has delegated to the indexer.
584+
* @param _indexer Address of the indexer where funds have been delegated
585+
* @param _delegator Address of the delegator
586+
* @return True if delegator of indexer
587+
*/
588+
function isDelegator(address _indexer, address _delegator) public view returns (bool) {
589+
return delegationPools[_indexer].delegators[_delegator].shares > 0;
590+
}
591+
587592
/**
588593
* @dev Get the total amount of tokens staked by the indexer.
589594
* @param _indexer Address of the indexer
@@ -1118,15 +1123,12 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
11181123
* @param _poi Proof of indexing submitted for the allocated period
11191124
*/
11201125
function _closeAllocation(address _allocationID, bytes32 _poi) private {
1121-
// Get allocation
1122-
Allocation storage alloc = allocations[_allocationID];
1123-
AllocationState allocState = _getAllocationState(_allocationID);
1124-
11251126
// Allocation must exist and be active
1127+
AllocationState allocState = _getAllocationState(_allocationID);
11261128
require(allocState == AllocationState.Active, "!active");
11271129

1128-
// Get indexer stakes
1129-
Stakes.Indexer storage indexerStake = stakes[alloc.indexer];
1130+
// Get allocation
1131+
Allocation storage alloc = allocations[_allocationID];
11301132

11311133
// Validate that an allocation cannot be closed before one epoch
11321134
uint256 currentEpoch = epochManager().currentEpoch();
@@ -1135,13 +1137,13 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
11351137
: 0;
11361138
require(epochs > 0, "<epochs");
11371139

1138-
// Validate ownership
1140+
// Indexer or operator can close an allocation
1141+
// Delegators are also allowed but only after maxAllocationEpochs passed
1142+
bool isIndexer = _isAuth(alloc.indexer);
11391143
if (epochs > maxAllocationEpochs) {
1140-
// Verify that the allocation owner or delegator is closing
1141-
require(_isAuthOrDelegator(alloc.indexer), "!auth-or-del");
1144+
require(isIndexer || isDelegator(alloc.indexer, msg.sender), "!auth-or-del");
11421145
} else {
1143-
// Verify that the allocation owner is closing
1144-
require(_isAuth(alloc.indexer), "!auth");
1146+
require(isIndexer, "!auth");
11451147
}
11461148

11471149
// Close the allocation and start counting a period to settle remaining payments from
@@ -1157,12 +1159,12 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
11571159
rebatePool.addToPool(alloc.collectedFees, alloc.effectiveAllocation);
11581160

11591161
// Distribute rewards if proof of indexing was presented by the indexer or operator
1160-
if (_isAuth(msg.sender) && _poi != 0) {
1162+
if (isIndexer && _poi != 0) {
11611163
_distributeRewards(_allocationID, alloc.indexer);
11621164
}
11631165

11641166
// Free allocated tokens from use
1165-
indexerStake.unallocate(alloc.tokens);
1167+
stakes[alloc.indexer].unallocate(alloc.tokens);
11661168

11671169
// Track total allocations per subgraph
11681170
// Used for rewards calculations
@@ -1178,7 +1180,8 @@ contract Staking is StakingV1Storage, GraphUpgradeable, IStaking {
11781180
_allocationID,
11791181
alloc.effectiveAllocation,
11801182
msg.sender,
1181-
_poi
1183+
_poi,
1184+
!isIndexer
11821185
);
11831186
}
11841187

test/staking/allocation.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ describe('Staking:Allocation', () => {
522522
effectiveAllocation,
523523
indexer.address,
524524
poi,
525+
false,
525526
)
526527

527528
// After state
@@ -558,6 +559,49 @@ describe('Staking:Allocation', () => {
558559
await staking.connect(me.signer).closeAllocation(allocationID, poi)
559560
})
560561

562+
it('should close an allocation (by delegator)', async function () {
563+
// Move max allocation epochs to close by delegator
564+
const maxAllocationEpochs = await staking.maxAllocationEpochs()
565+
for (let i = 0; i < maxAllocationEpochs + 1; i++) {
566+
await advanceToNextEpoch(epochManager)
567+
}
568+
569+
// Reject to close if the address is not delegator
570+
const tx1 = staking.connect(me.signer).closeAllocation(allocationID, poi)
571+
await expect(tx1).revertedWith('!auth')
572+
573+
// Calculations
574+
const beforeAlloc = await staking.getAllocation(allocationID)
575+
const currentEpoch = await epochManager.currentEpoch()
576+
const epochs = currentEpoch.sub(beforeAlloc.createdAtEpoch)
577+
const effectiveAllocation = calculateEffectiveAllocation(
578+
beforeAlloc.tokens,
579+
epochs,
580+
toBN(maxAllocationEpochs),
581+
)
582+
583+
// Setup
584+
await grt.connect(governor.signer).mint(me.address, toGRT('1'))
585+
await grt.connect(me.signer).approve(staking.address, toGRT('1'))
586+
await staking.connect(me.signer).delegate(indexer.address, toGRT('1'))
587+
588+
// Should close by delegator
589+
const tx = staking.connect(me.signer).closeAllocation(allocationID, poi)
590+
await expect(tx)
591+
.emit(staking, 'AllocationClosed')
592+
.withArgs(
593+
indexer.address,
594+
subgraphDeploymentID,
595+
currentEpoch,
596+
beforeAlloc.tokens,
597+
allocationID,
598+
effectiveAllocation,
599+
me.address,
600+
poi,
601+
true,
602+
)
603+
})
604+
561605
it('should close many allocations in batch', async function () {
562606
// Setup a second allocation
563607
await staking.connect(indexer.signer).stake(tokensToAllocate)

0 commit comments

Comments
 (0)