forked from OffchainLabs/nitro-contracts
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathRollupUserLogic.sol
More file actions
793 lines (709 loc) · 31.4 KB
/
RollupUserLogic.sol
File metadata and controls
793 lines (709 loc) · 31.4 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
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
// Copyright 2021-2022, Offchain Labs, Inc.
// For license information, see https://github.com/OffchainLabs/nitro-contracts/blob/main/LICENSE
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
import {IRollupUser} from "./IRollupLogic.sol";
import "../libraries/UUPSNotUpgradeable.sol";
import "./RollupCore.sol";
import "./IRollupLogic.sol";
import {ETH_POS_BLOCK_TIME} from "../libraries/Constants.sol";
abstract contract AbsRollupUserLogic is
RollupCore,
UUPSNotUpgradeable,
IRollupUserAbs,
IChallengeResultReceiver
{
using NodeLib for Node;
using GlobalStateLib for GlobalState;
modifier onlyValidator() {
require(isValidator[msg.sender] || validatorWhitelistDisabled, "NOT_VALIDATOR");
_;
}
modifier whenNotPausedOrDeprecated() {
require(!paused() || address(bridge.rollup()) != address(this), "PAUSED_AND_ACTIVE");
_;
}
uint256 internal immutable deployTimeChainId = block.chainid;
function _chainIdChanged() internal view returns (bool) {
return deployTimeChainId != block.chainid;
}
/**
* @notice Extra number of blocks the validator can remain inactive before considered inactive
* This is 7 days assuming a 13.2 seconds block time
*/
uint256 public constant VALIDATOR_AFK_BLOCKS = 45818;
function _validatorIsAfk() internal view returns (bool) {
Node memory latestNode = getNodeStorage(latestNodeCreated());
if (latestNode.createdAtBlock == 0) return false;
if (latestNode.createdAtBlock + confirmPeriodBlocks + VALIDATOR_AFK_BLOCKS < block.number) {
return true;
}
return false;
}
function removeWhitelistAfterFork() external {
require(!validatorWhitelistDisabled, "WHITELIST_DISABLED");
require(_chainIdChanged(), "CHAIN_ID_NOT_CHANGED");
validatorWhitelistDisabled = true;
}
function removeWhitelistAfterValidatorAfk() external {
require(!validatorWhitelistDisabled, "WHITELIST_DISABLED");
require(_validatorIsAfk(), "VALIDATOR_NOT_AFK");
validatorWhitelistDisabled = true;
}
function isERC20Enabled() public view override returns (bool) {
return stakeToken != address(0);
}
/**
* @notice Reject the next unresolved node
* @param stakerAddress Example staker staked on sibling, used to prove a node is on an unconfirmable branch and can be rejected
*/
function rejectNextNode(address stakerAddress) external onlyValidator whenNotPaused {
requireUnresolvedExists();
uint64 latestConfirmedNodeNum = latestConfirmed();
uint64 firstUnresolvedNodeNum = firstUnresolvedNode();
Node storage firstUnresolvedNode_ = getNodeStorage(firstUnresolvedNodeNum);
if (firstUnresolvedNode_.prevNum == latestConfirmedNodeNum) {
/**If the first unresolved node is a child of the latest confirmed node, to prove it can be rejected, we show:
* a) Its deadline has expired
* b) *Some* staker is staked on a sibling
* The following three checks are sufficient to prove b:
*/
// 1. StakerAddress is indeed a staker
require(isStakedOnLatestConfirmed(stakerAddress), "NOT_STAKED");
// 2. Staker's latest staked node hasn't been resolved; this proves that staker's latest staked node can't be a parent of firstUnresolvedNode
requireUnresolved(latestStakedNode(stakerAddress));
// 3. staker isn't staked on first unresolved node; this proves staker's latest staked can't be a child of firstUnresolvedNode (recall staking on node requires staking on all of its parents)
require(!nodeHasStaker(firstUnresolvedNodeNum, stakerAddress), "STAKED_ON_TARGET");
// If a staker is staked on a node that is neither a child nor a parent of firstUnresolvedNode, it must be a sibling, QED
// Verify the block's deadline has passed
firstUnresolvedNode_.requirePastDeadline();
getNodeStorage(latestConfirmedNodeNum).requirePastChildConfirmDeadline();
removeOldZombies(0);
// Verify that no staker is staked on this node
require(
firstUnresolvedNode_.stakerCount == countStakedZombies(firstUnresolvedNodeNum),
"HAS_STAKERS"
);
}
// Simpler case: if the first unreseolved node doesn't point to the last confirmed node, another branch was confirmed and can simply reject it outright
_rejectNextNode();
emit NodeRejected(firstUnresolvedNodeNum);
}
function _confirmNextNode(
bytes32 blockHash,
bytes32 sendRoot,
bool isFastConfirm
) internal whenNotPaused {
requireUnresolvedExists();
uint64 nodeNum = firstUnresolvedNode();
Node storage node = getNodeStorage(nodeNum);
if (!isFastConfirm) {
// Verify the block's deadline has passed
node.requirePastDeadline();
}
// Check that prev is latest confirmed
assert(node.prevNum == latestConfirmed());
Node storage prevNode = getNodeStorage(node.prevNum);
if (!isFastConfirm) {
prevNode.requirePastChildConfirmDeadline();
}
removeOldZombies(0);
// Require only zombies are staked on siblings to this node, and there's at least one non-zombie staked on this node
uint256 stakedZombies = countStakedZombies(nodeNum);
uint256 zombiesStakedOnOtherChildren = countZombiesStakedOnChildren(node.prevNum) -
stakedZombies;
require(node.stakerCount > stakedZombies, "NO_STAKERS");
require(
prevNode.childStakerCount == node.stakerCount + zombiesStakedOnOtherChildren,
"NOT_ALL_STAKED"
);
confirmNode(nodeNum, blockHash, sendRoot);
}
/**
* @notice Confirm the next unresolved node
* @param blockHash The block hash at the end of the assertion
* @param sendRoot The send root at the end of the assertion
*/
function confirmNextNode(bytes32 blockHash, bytes32 sendRoot) external onlyValidator {
_confirmNextNode(blockHash, sendRoot, false);
}
/**
* @notice This allow anyTrustFastConfirmer to confirm next node regardless of deadline
* the anyTrustFastConfirmer is supposed to be set only on an AnyTrust chain to
* a contract that can call this function when received sufficient signatures
* node hash must be match the node to be confirmed to protect against reorgs
*/
function fastConfirmNextNode(
bytes32 blockHash,
bytes32 sendRoot,
bytes32 nodeHash
) external {
require(msg.sender == anyTrustFastConfirmer, "NFC");
require(nodeHash == getNodeStorage(firstUnresolvedNode()).nodeHash, "WH");
_confirmNextNode(blockHash, sendRoot, true);
}
/**
* @notice Create a new stake
* @param depositAmount The amount of either eth or tokens staked
*/
function _newStake(uint256 depositAmount) internal onlyValidator whenNotPaused {
// Verify that sender is not already a staker
require(!isStaked(msg.sender), "ALREADY_STAKED");
require(!isZombie(msg.sender), "STAKER_IS_ZOMBIE");
require(depositAmount >= currentRequiredStake(), "NOT_ENOUGH_STAKE");
createNewStake(msg.sender, depositAmount);
}
/**
* @notice Move stake onto existing child node
* @param nodeNum Index of the node to move stake to. This must by a child of the node the staker is currently staked on
* @param nodeHash Node hash of nodeNum (protects against reorgs)
*/
function stakeOnExistingNode(uint64 nodeNum, bytes32 nodeHash)
public
onlyValidator
whenNotPaused
{
require(isStakedOnLatestConfirmed(msg.sender), "NOT_STAKED");
require(
nodeNum >= firstUnresolvedNode() && nodeNum <= latestNodeCreated(),
"NODE_NUM_OUT_OF_RANGE"
);
Node storage node = getNodeStorage(nodeNum);
require(node.nodeHash == nodeHash, "NODE_REORG");
require(latestStakedNode(msg.sender) == node.prevNum, "NOT_STAKED_PREV");
stakeOnNode(msg.sender, nodeNum);
}
/**
* @notice Create a new node and move stake onto it
* @param assertion The assertion data
* @param expectedNodeHash The hash of the node being created (protects against reorgs)
*/
function stakeOnNewNode(
Assertion calldata assertion,
bytes32 expectedNodeHash,
uint256 prevNodeInboxMaxCount
) public onlyValidator whenNotPaused {
require(isStakedOnLatestConfirmed(msg.sender), "NOT_STAKED");
// Ensure staker is staked on the previous node
uint64 prevNode = latestStakedNode(msg.sender);
{
uint256 timeSinceLastNode = block.number - getNode(prevNode).createdAtBlock;
// Verify that assertion meets the minimum Delta time requirement
require(timeSinceLastNode >= minimumAssertionPeriod, "TIME_DELTA");
// Minimum size requirement: any assertion must consume at least all inbox messages
// put into L1 inbox before the prev node’s L1 blocknum.
// We make an exception if the machine enters the errored state,
// as it can't consume future batches.
require(
assertion.afterState.machineStatus == MachineStatus.ERRORED ||
assertion.afterState.globalState.getInboxPosition() >= prevNodeInboxMaxCount,
"TOO_SMALL"
);
// Minimum size requirement: any assertion must contain at least one block
require(assertion.numBlocks > 0, "EMPTY_ASSERTION");
// The rollup cannot advance normally from an errored state
require(
assertion.beforeState.machineStatus == MachineStatus.FINISHED,
"BAD_PREV_STATUS"
);
}
createNewNode(assertion, prevNode, prevNodeInboxMaxCount, expectedNodeHash);
stakeOnNode(msg.sender, latestNodeCreated());
}
/**
* @notice Refund a staker that is currently staked on or before the latest confirmed node
* @dev Since a staker is initially placed in the latest confirmed node, if they don't move it
* a griefer can remove their stake. It is recomended to batch together the txs to place a stake
* and move it to the desired node.
* @param stakerAddress Address of the staker whose stake is refunded
*/
function returnOldDeposit(address stakerAddress)
external
override
onlyValidator
whenNotPausedOrDeprecated
{
require(latestStakedNode(stakerAddress) <= latestConfirmed(), "TOO_RECENT");
requireUnchallengedStaker(stakerAddress);
withdrawStaker(stakerAddress);
}
/**
* @notice Increase the amount staked for the given staker
* @param stakerAddress Address of the staker whose stake is increased
* @param depositAmount The amount of either eth or tokens deposited
*/
function _addToDeposit(address stakerAddress, uint256 depositAmount)
internal
onlyValidator
whenNotPaused
{
requireUnchallengedStaker(stakerAddress);
increaseStakeBy(stakerAddress, depositAmount);
}
/**
* @notice Reduce the amount staked for the sender (difference between initial amount staked and target is creditted back to the sender).
* @param target Target amount of stake for the staker. If this is below the current minimum, it will be set to minimum instead
*/
function reduceDeposit(uint256 target) external onlyValidator whenNotPausedOrDeprecated {
requireUnchallengedStaker(msg.sender);
uint256 currentRequired = currentRequiredStake();
if (target < currentRequired) {
target = currentRequired;
}
reduceStakeTo(msg.sender, target);
}
/**
* @notice Start a challenge between the given stakers over the node created by the first staker assuming that the two are staked on conflicting nodes. N.B.: challenge creator does not necessarily need to be one of the two asserters.
* @param stakers Stakers engaged in the challenge. The first staker should be staked on the first node
* @param nodeNums Nodes of the stakers engaged in the challenge. The first node should be the earliest and is the one challenged
* @param machineStatuses The before and after machine status for the first assertion
* @param globalStates The before and after global state for the first assertion
* @param numBlocks The number of L2 blocks contained in the first assertion
* @param secondExecutionHash The execution hash of the second assertion
* @param proposedBlocks L1 block numbers that the two nodes were proposed at
* @param wasmModuleRoots The wasm module roots at the time of the creation of each assertion
*/
function createChallenge(
address[2] calldata stakers,
uint64[2] calldata nodeNums,
MachineStatus[2] calldata machineStatuses,
GlobalState[2] calldata globalStates,
uint64 numBlocks,
bytes32 secondExecutionHash,
uint256[2] calldata proposedBlocks,
bytes32[2] calldata wasmModuleRoots
) external onlyValidator whenNotPaused {
require(nodeNums[0] < nodeNums[1], "WRONG_ORDER");
require(nodeNums[1] <= latestNodeCreated(), "NOT_PROPOSED");
require(latestConfirmed() < nodeNums[0], "ALREADY_CONFIRMED");
Node storage node1 = getNodeStorage(nodeNums[0]);
Node storage node2 = getNodeStorage(nodeNums[1]);
// ensure nodes staked on the same parent (and thus in conflict)
require(node1.prevNum == node2.prevNum, "DIFF_PREV");
// ensure both stakers aren't currently in challenge
requireUnchallengedStaker(stakers[0]);
requireUnchallengedStaker(stakers[1]);
require(nodeHasStaker(nodeNums[0], stakers[0]), "STAKER1_NOT_STAKED");
require(nodeHasStaker(nodeNums[1], stakers[1]), "STAKER2_NOT_STAKED");
// Check param data against challenge hash
require(
node1.challengeHash ==
RollupLib.challengeRootHash(
RollupLib.executionHash(machineStatuses, globalStates, numBlocks),
proposedBlocks[0],
wasmModuleRoots[0]
),
"CHAL_HASH1"
);
require(
node2.challengeHash ==
RollupLib.challengeRootHash(
secondExecutionHash,
proposedBlocks[1],
wasmModuleRoots[1]
),
"CHAL_HASH2"
);
// Calculate upper limit for allowed node proposal time:
uint256 commonEndBlock = getNodeStorage(node1.prevNum).firstChildBlock +
// Dispute start: dispute timer for a node starts when its first child is created
(node1.deadlineBlock - proposedBlocks[0]) +
extraChallengeTimeBlocks; // add dispute window to dispute start time
if (commonEndBlock < proposedBlocks[1]) {
// The 2nd node was created too late; loses challenge automatically.
completeChallengeImpl(stakers[0], stakers[1]);
return;
}
// Start a challenge between staker1 and staker2. Staker1 will defend the correctness of node1, and staker2 will challenge it.
uint64 challengeIndex = createChallengeHelper(
stakers,
machineStatuses,
globalStates,
numBlocks,
wasmModuleRoots,
// convert from block counts to real second based timestamps
(commonEndBlock - proposedBlocks[0]) * ETH_POS_BLOCK_TIME,
(commonEndBlock - proposedBlocks[1]) * ETH_POS_BLOCK_TIME
); // trusted external call
challengeStarted(stakers[0], stakers[1], challengeIndex);
emit RollupChallengeStarted(challengeIndex, stakers[0], stakers[1], nodeNums[0]);
}
function createChallengeHelper(
address[2] calldata stakers,
MachineStatus[2] calldata machineStatuses,
GlobalState[2] calldata globalStates,
uint64 numBlocks,
bytes32[2] calldata wasmModuleRoots,
uint256 asserterTimeLeft,
uint256 challengerTimeLeft
) internal returns (uint64) {
return
challengeManager.createChallenge(
wasmModuleRoots[0],
machineStatuses,
globalStates,
numBlocks,
stakers[0],
stakers[1],
asserterTimeLeft,
challengerTimeLeft
);
}
/**
* @notice Inform the rollup that the challenge between the given stakers is completed
* @param winningStaker Address of the winning staker
* @param losingStaker Address of the losing staker
*/
function completeChallenge(
uint256 challengeIndex,
address winningStaker,
address losingStaker
) external override whenNotPaused {
// Only the challenge manager contract can call this to declare the winner and loser
require(msg.sender == address(challengeManager), "WRONG_SENDER");
require(challengeIndex == inChallenge(winningStaker, losingStaker), "NOT_IN_CHAL");
completeChallengeImpl(winningStaker, losingStaker);
}
function completeChallengeImpl(address winningStaker, address losingStaker) private {
uint256 remainingLoserStake = amountStaked(losingStaker);
uint256 winnerStake = amountStaked(winningStaker);
if (remainingLoserStake > winnerStake) {
// If loser has a higher stake than the winner, refund the difference
remainingLoserStake -= reduceStakeTo(losingStaker, winnerStake);
}
// Reward the winner with half the remaining stake
uint256 amountWon = remainingLoserStake / 2;
increaseStakeBy(winningStaker, amountWon);
remainingLoserStake -= amountWon;
// We deliberately leave loser in challenge state to prevent them from
// doing certain thing that are allowed only to parties not in a challenge
clearChallenge(winningStaker);
// Credit the other half to the loserStakeEscrow address
increaseWithdrawableFunds(loserStakeEscrow, remainingLoserStake);
// Turning loser into zombie renders the loser's remaining stake inaccessible
turnIntoZombie(losingStaker);
}
/**
* @notice Remove the given zombie from nodes it is staked on, moving backwards from the latest node it is staked on
* @param zombieNum Index of the zombie to remove
* @param maxNodes Maximum number of nodes to remove the zombie from (to limit the cost of this transaction)
*/
function removeZombie(uint256 zombieNum, uint256 maxNodes)
external
onlyValidator
whenNotPaused
{
require(zombieNum < zombieCount(), "NO_SUCH_ZOMBIE");
address zombieStakerAddress = zombieAddress(zombieNum);
uint64 latestNodeStaked = zombieLatestStakedNode(zombieNum);
uint256 nodesRemoved = 0;
uint256 latestConfirmedNum = latestConfirmed();
while (latestNodeStaked >= latestConfirmedNum && nodesRemoved < maxNodes) {
Node storage node = getNodeStorage(latestNodeStaked);
removeStaker(latestNodeStaked, zombieStakerAddress);
latestNodeStaked = node.prevNum;
nodesRemoved++;
}
if (latestNodeStaked < latestConfirmedNum) {
removeZombie(zombieNum);
} else {
zombieUpdateLatestStakedNode(zombieNum, latestNodeStaked);
}
}
/**
* @notice Remove any zombies whose latest stake is earlier than the latest confirmed node
* @param startIndex Index in the zombie list to start removing zombies from (to limit the cost of this transaction)
*/
function removeOldZombies(uint256 startIndex) public onlyValidator whenNotPaused {
uint256 currentZombieCount = zombieCount();
uint256 latestConfirmedNum = latestConfirmed();
for (uint256 i = startIndex; i < currentZombieCount; i++) {
while (zombieLatestStakedNode(i) < latestConfirmedNum) {
removeZombie(i);
currentZombieCount--;
if (i >= currentZombieCount) {
return;
}
}
}
}
/**
* @notice Calculate the current amount of funds required to place a new stake in the rollup
* @dev If the stake requirement get's too high, this function may start reverting due to overflow, but
* that only blocks operations that should be blocked anyway
* @return The current minimum stake requirement
*/
function currentRequiredStake(
uint256 _blockNumber,
uint64 _firstUnresolvedNodeNum,
uint256 _latestCreatedNode
) internal view returns (uint256) {
// If there are no unresolved nodes, then you can use the base stake
if (_firstUnresolvedNodeNum - 1 == _latestCreatedNode) {
return baseStake;
}
uint256 firstUnresolvedDeadline = getNodeStorage(_firstUnresolvedNodeNum).deadlineBlock;
if (_blockNumber < firstUnresolvedDeadline) {
return baseStake;
}
uint24[10] memory numerators = [
1,
122971,
128977,
80017,
207329,
114243,
314252,
129988,
224562,
162163
];
uint24[10] memory denominators = [
1,
114736,
112281,
64994,
157126,
80782,
207329,
80017,
128977,
86901
];
uint256 firstUnresolvedAge = _blockNumber - firstUnresolvedDeadline;
uint256 periodsPassed = (firstUnresolvedAge * 10) / confirmPeriodBlocks;
uint256 baseMultiplier = 2**(periodsPassed / 10);
uint256 withNumerator = baseMultiplier * numerators[periodsPassed % 10];
uint256 multiplier = withNumerator / denominators[periodsPassed % 10];
if (multiplier == 0) {
multiplier = 1;
}
return baseStake * multiplier;
}
/**
* @notice Calculate the current amount of funds required to place a new stake in the rollup
* @dev If the stake requirement get's too high, this function may start reverting due to overflow, but
* that only blocks operations that should be blocked anyway
* @return The current minimum stake requirement
*/
function requiredStake(
uint256 blockNumber,
uint64 firstUnresolvedNodeNum,
uint64 latestCreatedNode
) external view returns (uint256) {
return currentRequiredStake(blockNumber, firstUnresolvedNodeNum, latestCreatedNode);
}
function owner() external view returns (address) {
return _getAdmin();
}
function currentRequiredStake() public view returns (uint256) {
uint64 firstUnresolvedNodeNum = firstUnresolvedNode();
return currentRequiredStake(block.number, firstUnresolvedNodeNum, latestNodeCreated());
}
/**
* @notice Calculate the number of zombies staked on the given node
*
* @dev This function could be uncallable if there are too many zombies. However,
* removeZombie and removeOldZombies can be used to remove any zombies that exist
* so that this will then be callable
*
* @param nodeNum The node on which to count staked zombies
* @return The number of zombies staked on the node
*/
function countStakedZombies(uint64 nodeNum) public view override returns (uint256) {
uint256 currentZombieCount = zombieCount();
uint256 stakedZombieCount = 0;
for (uint256 i = 0; i < currentZombieCount; i++) {
if (nodeHasStaker(nodeNum, zombieAddress(i))) {
stakedZombieCount++;
}
}
return stakedZombieCount;
}
/**
* @notice Calculate the number of zombies staked on a child of the given node
*
* @dev This function could be uncallable if there are too many zombies. However,
* removeZombie and removeOldZombies can be used to remove any zombies that exist
* so that this will then be callable
*
* @param nodeNum The parent node on which to count zombies staked on children
* @return The number of zombies staked on children of the node
*/
function countZombiesStakedOnChildren(uint64 nodeNum) public view override returns (uint256) {
uint256 currentZombieCount = zombieCount();
uint256 stakedZombieCount = 0;
for (uint256 i = 0; i < currentZombieCount; i++) {
Zombie storage zombie = getZombieStorage(i);
// If this zombie is staked on this node, but its _latest_ staked node isn't this node,
// then it must be staked on a child of this node.
if (
zombie.latestStakedNode != nodeNum && nodeHasStaker(nodeNum, zombie.stakerAddress)
) {
stakedZombieCount++;
}
}
return stakedZombieCount;
}
/**
* @notice Verify that there are some number of nodes still unresolved
*/
function requireUnresolvedExists() public view override {
uint256 firstUnresolved = firstUnresolvedNode();
require(
firstUnresolved > latestConfirmed() && firstUnresolved <= latestNodeCreated(),
"NO_UNRESOLVED"
);
}
function requireUnresolved(uint256 nodeNum) public view override {
require(nodeNum >= firstUnresolvedNode(), "ALREADY_DECIDED");
require(nodeNum <= latestNodeCreated(), "DOESNT_EXIST");
}
/**
* @notice Verify that the given address is staked and not actively in a challenge
* @param stakerAddress Address to check
*/
function requireUnchallengedStaker(address stakerAddress) private view {
require(isStaked(stakerAddress), "NOT_STAKED");
require(currentChallenge(stakerAddress) == NO_CHAL_INDEX, "IN_CHAL");
}
}
contract RollupUserLogic is AbsRollupUserLogic, IRollupUser {
/// @dev the user logic just validated configuration and shouldn't write to state during init
/// this allows the admin logic to ensure consistency on parameters.
function initialize(address _stakeToken) external view override onlyProxy {
require(_stakeToken == address(0), "NO_TOKEN_ALLOWED");
require(!isERC20Enabled(), "FACET_NOT_ERC20");
}
/**
* @notice Create a new stake on an existing node
* @param nodeNum Number of the node your stake will be place one
* @param nodeHash Node hash of the node with the given nodeNum
*/
function newStakeOnExistingNode(uint64 nodeNum, bytes32 nodeHash) external payable override {
_newStake(msg.value);
stakeOnExistingNode(nodeNum, nodeHash);
}
/**
* @notice Create a new stake on a new node
* @param assertion Assertion describing the state change between the old node and the new one
* @param expectedNodeHash Node hash of the node that will be created
* @param prevNodeInboxMaxCount Total of messages in the inbox as of the previous node
*/
function newStakeOnNewNode(
Assertion calldata assertion,
bytes32 expectedNodeHash,
uint256 prevNodeInboxMaxCount
) external payable override {
_newStake(msg.value);
stakeOnNewNode(assertion, expectedNodeHash, prevNodeInboxMaxCount);
}
/**
* @notice Increase the amount staked eth for the given staker
* @param stakerAddress Address of the staker whose stake is increased
*/
function addToDeposit(address stakerAddress)
external
payable
override
onlyValidator
whenNotPaused
{
_addToDeposit(stakerAddress, msg.value);
}
/**
* @notice Withdraw uncommitted funds owned by sender from the rollup chain
*/
function withdrawStakerFunds()
external
override
onlyValidator
whenNotPausedOrDeprecated
returns (uint256)
{
uint256 amount = withdrawFunds(msg.sender);
// This is safe because it occurs after all checks and effects
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "TRANSFER_FAILED");
return amount;
}
}
contract ERC20RollupUserLogic is AbsRollupUserLogic, IRollupUserERC20 {
/// @dev the user logic just validated configuration and shouldn't write to state during init
/// this allows the admin logic to ensure consistency on parameters.
function initialize(address _stakeToken) external view override onlyProxy {
require(_stakeToken != address(0), "NEED_STAKE_TOKEN");
require(isERC20Enabled(), "FACET_NOT_ERC20");
}
/**
* @notice Create a new stake on an existing node
* @param tokenAmount Amount of the rollups staking token to stake
* @param nodeNum Number of the node your stake will be place one
* @param nodeHash Node hash of the node with the given nodeNum
*/
function newStakeOnExistingNode(
uint256 tokenAmount,
uint64 nodeNum,
bytes32 nodeHash
) external override {
_newStake(tokenAmount);
stakeOnExistingNode(nodeNum, nodeHash);
/// @dev This is an external call, safe because it's at the end of the function
receiveTokens(tokenAmount);
}
/**
* @notice Create a new stake on a new node
* @param tokenAmount Amount of the rollups staking token to stake
* @param assertion Assertion describing the state change between the old node and the new one
* @param expectedNodeHash Node hash of the node that will be created
* @param prevNodeInboxMaxCount Total of messages in the inbox as of the previous node
*/
function newStakeOnNewNode(
uint256 tokenAmount,
Assertion calldata assertion,
bytes32 expectedNodeHash,
uint256 prevNodeInboxMaxCount
) external override {
_newStake(tokenAmount);
stakeOnNewNode(assertion, expectedNodeHash, prevNodeInboxMaxCount);
/// @dev This is an external call, safe because it's at the end of the function
receiveTokens(tokenAmount);
}
/**
* @notice Increase the amount staked tokens for the given staker
* @param stakerAddress Address of the staker whose stake is increased
* @param tokenAmount the amount of tokens staked
*/
function addToDeposit(address stakerAddress, uint256 tokenAmount)
external
onlyValidator
whenNotPaused
{
_addToDeposit(stakerAddress, tokenAmount);
/// @dev This is an external call, safe because it's at the end of the function
receiveTokens(tokenAmount);
}
/**
* @notice Withdraw uncommitted funds owned by sender from the rollup chain
*/
function withdrawStakerFunds()
external
override
onlyValidator
whenNotPausedOrDeprecated
returns (uint256)
{
uint256 amount = withdrawFunds(msg.sender);
// This is safe because it occurs after all checks and effects
require(IERC20Upgradeable(stakeToken).transfer(msg.sender, amount), "TRANSFER_FAILED");
return amount;
}
function receiveTokens(uint256 tokenAmount) private {
require(
IERC20Upgradeable(stakeToken).transferFrom(msg.sender, address(this), tokenAmount),
"TRANSFER_FAIL"
);
}
}