Skip to content

Commit 2379cba

Browse files
authored
staking: fix slash underflow condition when stake gets overallocated (#191)
* staking: allocate with channel public key, advertise in event and calculate Ethereum address of the channel signer using the public key * staking: ignore Staking.sol temporarily as it is using array slicing that linter does not support * staking: prettier-solidity fixed the issue we submitted, bump to newest version and remove prettier ignore line * staking: emit total tokens slashed instead of tokens burned * staking: fix over allocation issue and add more tests * staking: rename channel comments and remove delegation state vars * staking: add more tests for slashing and comments
1 parent 7db2bc3 commit 2379cba

File tree

7 files changed

+1269
-524
lines changed

7 files changed

+1269
-524
lines changed

.soliumignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules
2+
contracts/Staking.sol
23
contracts/Migrations.sol
34
contracts/bancor
45
contracts/openzeppelin

contracts/Staking.sol

Lines changed: 84 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ contract Staking is Governed {
5252
// IndexNode stake tracking : indexNode => Stake
5353
mapping(address => Stakes.IndexNode) public stakes;
5454

55-
// Payment channels : channelID => Channel
55+
// Channels : channelID => Channel
5656
mapping(address => Channel) public channels;
5757

5858
// Rebate pools : epoch => Pool
@@ -96,19 +96,22 @@ contract Staking is Governed {
9696

9797
/**
9898
* @dev Emitted when `indexNode` allocated `tokens` amount to `subgraphID`
99-
* during `epoch` and registered `channelID` as payment channel.
99+
* during `epoch`.
100+
* `channelID` is the address of the index node in the channel multisig.
101+
* `channelPubKey` is the public key used for routing payments to the index node channel.
100102
*/
101103
event AllocationCreated(
102104
address indexed indexNode,
103105
bytes32 indexed subgraphID,
104106
uint256 epoch,
105107
uint256 tokens,
106-
address channelID
108+
address channelID,
109+
bytes channelPubKey
107110
);
108111

109112
/**
110113
* @dev Emitted when `indexNode` settled an allocation of `tokens` amount to `subgraphID`
111-
* during `epoch` using `channelID` as payment channel.
114+
* during `epoch` using `channelID` as channel.
112115
*
113116
* NOTE: `from` tracks the multisig contract from where it was settled.
114117
*/
@@ -153,10 +156,12 @@ contract Staking is Governed {
153156
* @param _epochManager Address of the EpochManager contract
154157
* @param _curation Address of the Curation contract
155158
*/
156-
constructor(address _governor, address _token, address _epochManager, address _curation)
157-
public
158-
Governed(_governor)
159-
{
159+
constructor(
160+
address _governor,
161+
address _token,
162+
address _epochManager,
163+
address _curation
164+
) public Governed(_governor) {
160165
token = GraphToken(_token);
161166
epochManager = EpochManager(_epochManager);
162167
curation = Curation(_curation);
@@ -216,7 +221,7 @@ contract Staking is Governed {
216221

217222
/**
218223
* @dev Return if channelID (address) is already used
219-
* @param _channelID Address used as signer for index node in payment channel
224+
* @param _channelID Address used as signer for index node in channel
220225
* @return True if channelID already used
221226
*/
222227
function isChannel(address _channelID) public view returns (bool) {
@@ -262,26 +267,37 @@ contract Staking is Governed {
262267
* @param _reward Amount of reward tokens to send to a beneficiary
263268
* @param _beneficiary Address of a beneficiary to receive a reward for the slashing
264269
*/
265-
function slash(address _indexNode, uint256 _tokens, uint256 _reward, address _beneficiary)
266-
external
267-
onlySlasher
268-
{
269-
uint256 tokensToSlash = _tokens;
270+
function slash(
271+
address _indexNode,
272+
uint256 _tokens,
273+
uint256 _reward,
274+
address _beneficiary
275+
) external onlySlasher {
270276
Stakes.IndexNode storage stake = stakes[_indexNode];
271277

272278
require(stake.hasTokens(), "Slashing: index node has no stakes");
273279
require(_beneficiary != address(0), "Slashing: beneficiary must not be an empty address");
274-
require(tokensToSlash >= _reward, "Slashing: reward cannot be higher than slashed amount");
280+
require(_tokens >= _reward, "Slashing: reward cannot be higher than slashed amount");
275281
require(
276-
tokensToSlash <= stake.tokensIndexNode,
277-
"Slashing: cannot slash more than available stake"
282+
_tokens <= stake.tokensSlashable(),
283+
"Slashing: cannot slash more than staked amount"
278284
);
279285

280-
// Slash stake
281-
stake.release(tokensToSlash);
286+
// Slashing more tokens than freely available (over allocation condition)
287+
// Unlock locked tokens to avoid the indexer to withdraw them
288+
if (_tokens > stake.tokensAvailable() && stake.tokensLocked > 0) {
289+
uint256 tokensOverAllocated = _tokens.sub(stake.tokensAvailable());
290+
uint256 tokensToUnlock = (tokensOverAllocated > stake.tokensLocked)
291+
? stake.tokensLocked
292+
: tokensOverAllocated;
293+
stake.unlockTokens(tokensToUnlock);
294+
}
295+
296+
// Remove tokens to slash from the stake
297+
stake.release(_tokens);
282298

283299
// Set apart the reward for the beneficiary and burn remaining slashed stake
284-
uint256 tokensToBurn = tokensToSlash.sub(_reward);
300+
uint256 tokensToBurn = _tokens.sub(_reward);
285301
if (tokensToBurn > 0) {
286302
token.burn(tokensToBurn);
287303
}
@@ -294,18 +310,19 @@ contract Staking is Governed {
294310
);
295311
}
296312

297-
emit StakeSlashed(_indexNode, tokensToBurn, _reward, _beneficiary);
313+
emit StakeSlashed(_indexNode, _tokens, _reward, _beneficiary);
298314
}
299315

300316
/**
301317
* @dev Accept tokens and handle staking registration functions
302318
* @param _from Token holder's address
303319
* @param _value Amount of Graph Tokens
304320
*/
305-
function tokensReceived(address _from, uint256 _value, bytes calldata _data)
306-
external
307-
returns (bool)
308-
{
321+
function tokensReceived(
322+
address _from,
323+
uint256 _value,
324+
bytes calldata _data
325+
) external returns (bool) {
309326
// Make sure the token is the caller of this function
310327
require(msg.sender == address(token), "Caller is not the GRT token contract");
311328

@@ -328,36 +345,47 @@ contract Staking is Governed {
328345
* @dev Allocate available tokens to a subgraph
329346
* @param _subgraphID ID of the subgraph where tokens will be allocated
330347
* @param _tokens Amount of tokens to allocate
331-
* @param _channelID The signer address used by the IndexNode to setup the off-chain payment channel
348+
* @param _channelPubKey The public key used by the IndexNode to setup the off-chain channel
332349
*/
333-
function allocate(bytes32 _subgraphID, uint256 _tokens, address _channelID) external {
350+
function allocate(
351+
bytes32 _subgraphID,
352+
uint256 _tokens,
353+
bytes calldata _channelPubKey
354+
) external {
334355
address indexNode = msg.sender;
335356
Stakes.IndexNode storage stake = stakes[indexNode];
336357

358+
// Only allocations with a token amount are allowed
337359
require(_tokens > 0, "Allocation: cannot allocate zero tokens");
360+
// Need to have tokens in our stake to be able to allocate
338361
require(stake.hasTokens(), "Allocation: index node has no stakes");
362+
// Need to have free tokens not used for other purposes to allocate
339363
require(
340364
stake.tokensAvailable() >= _tokens,
341365
"Allocation: not enough tokens available to allocate"
342366
);
367+
// Can only allocate tokens to a subgraph if not currently allocated
343368
require(
344369
stake.hasAllocation(_subgraphID) == false,
345370
"Allocation: cannot allocate if already allocated"
346371
);
347-
require(isChannel(_channelID) == false, "Allocation: channel ID already in use");
372+
// Cannot reuse a channelID that has been used in the past
373+
address channelID = publicKeyToAddress(bytes(_channelPubKey[1:])); // solium-disable-line
374+
require(isChannel(channelID) == false, "Allocation: channel ID already in use");
348375

349376
// Allocate and setup channel
350377
Stakes.Allocation storage alloc = stake.allocateTokens(_subgraphID, _tokens);
351-
alloc.channelID = _channelID;
378+
alloc.channelID = channelID;
352379
alloc.createdAtEpoch = epochManager.currentEpoch();
353-
channels[_channelID] = Channel(indexNode, _subgraphID);
380+
channels[channelID] = Channel(indexNode, _subgraphID);
354381

355382
emit AllocationCreated(
356383
indexNode,
357384
_subgraphID,
358385
alloc.createdAtEpoch,
359386
alloc.tokens,
360-
_channelID
387+
channelID,
388+
_channelPubKey
361389
);
362390
}
363391

@@ -401,7 +429,11 @@ contract Staking is Governed {
401429
* @param _subgraphID Subgraph we are claiming tokens from
402430
* @param _restake True if restake fees instead of transfer to index node
403431
*/
404-
function claim(uint256 _epoch, bytes32 _subgraphID, bool _restake) external {
432+
function claim(
433+
uint256 _epoch,
434+
bytes32 _subgraphID,
435+
bool _restake
436+
) external {
405437
address indexNode = msg.sender;
406438
Rebates.Pool storage pool = rebates[_epoch];
407439
Rebates.Settlement storage settlement = pool.settlements[indexNode][_subgraphID];
@@ -460,7 +492,11 @@ contract Staking is Governed {
460492
* @param _from Multisig channel address that triggered settlement
461493
* @param _tokens Amount of tokens to settle
462494
*/
463-
function _settle(address _channelID, address _from, uint256 _tokens) private {
495+
function _settle(
496+
address _channelID,
497+
address _from,
498+
uint256 _tokens
499+
) private {
464500
address indexNode = channels[_channelID].indexNode;
465501
bytes32 subgraphID = channels[_channelID].subgraphID;
466502
Stakes.IndexNode storage stake = stakes[indexNode];
@@ -508,6 +544,14 @@ contract Staking is Governed {
508544
emit AllocationSettled(indexNode, subgraphID, currentEpoch, _tokens, _channelID, _from);
509545
}
510546

547+
/**
548+
* @dev Get whether curation rewards are active or not
549+
* @return true if curation fees are enabled
550+
*/
551+
function isCurationEnabled() private view returns (bool) {
552+
return curationPercentage > 0 && address(curation) != address(0);
553+
}
554+
511555
/**
512556
* @dev Get the running network chain ID
513557
* @return The chain ID
@@ -521,10 +565,13 @@ contract Staking is Governed {
521565
}
522566

523567
/**
524-
* @dev Get whether curation rewards are active or not
525-
* @return true if curation fees are enabled
568+
* @dev Convert an uncompressed public key to an Ethereum address
569+
* @param _publicKey Public key in uncompressed format without the 1 byte prefix
570+
* @return An Ethereum address corresponding to the public key
526571
*/
527-
function isCurationEnabled() private view returns (bool) {
528-
return curationPercentage > 0 && address(curation) != address(0);
572+
function publicKeyToAddress(bytes memory _publicKey) private pure returns (address) {
573+
uint256 mask = 2**(8 * 21) - 1;
574+
uint256 value = uint256(keccak256(_publicKey));
575+
return address(value & mask);
529576
}
530577
}

contracts/libs/Stakes.sol

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,11 @@ library Stakes {
1717
struct Allocation {
1818
uint256 tokens; // Tokens allocated to a subgraph
1919
uint256 createdAtEpoch; // Epoch when it was created
20-
address channelID; // IndexNode payment channel ID used off chain
20+
address channelID; // IndexNode channel ID used off chain
2121
}
2222

2323
struct IndexNode {
2424
uint256 tokensIndexNode; // Tokens on the IndexNode stake (staked by the index node)
25-
uint256 tokensDelegated; // Tokens on the Delegated stake (staked by the delegators)
2625
uint256 tokensAllocated; // Tokens used in subgraph allocations
2726
uint256 tokensLocked; // Tokens locked for withdrawal subject to thawing period
2827
uint256 tokensLockedUntil; // Time when locked tokens can be withdrawn
@@ -63,7 +62,7 @@ library Stakes {
6362
}
6463

6564
/**
66-
* @dev Deposit tokens to the index node stake balance
65+
* @dev Deposit tokens to the index node stake
6766
* @param stake Stake data
6867
* @param _tokens Amount of tokens to deposit
6968
*/
@@ -72,7 +71,7 @@ library Stakes {
7271
}
7372

7473
/**
75-
* @dev Release tokens from the index node stake balance
74+
* @dev Release tokens from the index node stake
7675
* @param stake Stake data
7776
* @param _tokens Amount of tokens to release
7877
*/
@@ -100,6 +99,18 @@ library Stakes {
10099
stake.tokensLockedUntil = block.number.add(lockingPeriod);
101100
}
102101

102+
/**
103+
* @dev Unlock tokens
104+
* @param stake Stake data
105+
* @param _tokens Amount of tokens to unkock
106+
*/
107+
function unlockTokens(Stakes.IndexNode storage stake, uint256 _tokens) internal {
108+
stake.tokensLocked = stake.tokensLocked.sub(_tokens);
109+
if (stake.tokensLocked == 0) {
110+
stake.tokensLockedUntil = 0;
111+
}
112+
}
113+
103114
/**
104115
* @dev Take all tokens out from the locked stack for withdrawal
105116
* @param stake Stake data
@@ -111,8 +122,7 @@ library Stakes {
111122

112123
if (tokensToWithdraw > 0) {
113124
// Reset locked tokens
114-
stake.tokensLocked = 0;
115-
stake.tokensLockedUntil = 0;
125+
stake.unlockTokens(tokensToWithdraw);
116126

117127
// Decrease index node stake
118128
stake.release(tokensToWithdraw);
@@ -167,22 +177,29 @@ library Stakes {
167177
}
168178

169179
/**
170-
* @dev Total tokens staked both from IndexNode and Delegators
180+
* @dev Total tokens staked from IndexNode
171181
* @param stake Stake data
172182
* @return Token amount
173183
*/
174184
function tokensStaked(Stakes.IndexNode storage stake) internal view returns (uint256) {
175-
return stake.tokensIndexNode.add(stake.tokensDelegated);
185+
return stake.tokensIndexNode;
176186
}
177187

178188
/**
179189
* @dev Tokens available for use in allocations
180-
* @dev tokensIndexNode + tokensDelegated - tokensAllocated - tokensLocked
190+
* @dev tokensIndexNode - tokensAllocated - tokensLocked
181191
* @param stake Stake data
182192
* @return Token amount
183193
*/
184194
function tokensAvailable(Stakes.IndexNode storage stake) internal view returns (uint256) {
185-
return stake.tokensStaked().sub(stake.tokensAllocated).sub(stake.tokensLocked);
195+
uint256 _tokensStaked = stake.tokensStaked();
196+
uint256 tokensUsed = stake.tokensAllocated.add(stake.tokensLocked);
197+
// Stake is over allocated: return 0 to avoid stake to be used until the overallocation
198+
// is restored by staking more tokens or unallocating tokens
199+
if (tokensUsed > _tokensStaked) {
200+
return 0;
201+
}
202+
return _tokensStaked.sub(tokensUsed);
186203
}
187204

188205
/**
@@ -216,7 +233,7 @@ library Stakes {
216233
/**
217234
* @dev Return if channel for an allocation is active
218235
* @param alloc Allocation data
219-
* @return True if payment channel related to allocation is active
236+
* @return True if channel related to allocation is active
220237
*/
221238
function hasChannel(Stakes.Allocation storage alloc) internal view returns (bool) {
222239
return alloc.channelID != address(0);

0 commit comments

Comments
 (0)