@web3dotorg/evm-slc-core-contracts 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1130 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
5
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6
+ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
7
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
8
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
9
+ import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
10
+ import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
11
+ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
12
+
13
+ import {PRECISION} from "@solarity/solidity-lib/utils/Globals.sol";
14
+ import {Paginator} from "@solarity/solidity-lib/libs/arrays/Paginator.sol";
15
+ import {PriorityQueue} from "@solarity/solidity-lib/libs/data-structures/PriorityQueue.sol";
16
+
17
+ import {Errors} from "../libs/Errors.sol";
18
+
19
+ import {DAOSLC_PARAM_NAME, TREASURY_ADDRESS_PARAM_NAME, MAX_BASIS_POINTS} from "../utils/Globals.sol";
20
+
21
+ import {IGovernance} from "../interfaces/IGovernance.sol";
22
+ import {IQParameters} from "../interfaces/IQParameters.sol";
23
+ import {NeutralsSelection} from "../libs/NeutralsSelection.sol";
24
+ import {INeutralsRegistry} from "../interfaces/INeutralsRegistry.sol";
25
+
26
+ /**
27
+ * @title NeutralsRegistry
28
+ * @notice This contract manages the registration, staking, delegation, reward distribution, and slashing of neutrals in the SLC system.
29
+ * Neutrals can stake tokens to participate, receive delegations, and earn rewards. The contract supports withdrawal announcements
30
+ * and integrates with governance and parameter registries. Only authorized roles (DAOSLC, Governance)
31
+ * can perform certain sensitive operations such as slashing and reward distribution.
32
+ */
33
+ contract NeutralsRegistry is INeutralsRegistry, ERC165, UUPSUpgradeable {
34
+ using SafeERC20 for IERC20;
35
+ using EnumerableSet for EnumerableSet.AddressSet;
36
+ using PriorityQueue for PriorityQueue.AddressQueue;
37
+
38
+ // keccak256(abi.encode(uint256(keccak256("evm-slc-core.storage.NeutralsRegistry")) - 1)) & ~bytes32(uint256(0xff))
39
+ bytes32 private constant NEUTRALS_REGISTRY_STORAGE_LOCATION =
40
+ 0x02eec0af57616a1cb2ef3538f733bfc9b20f30a56aced6911a05ed5520d14900;
41
+
42
+ string private constant MAX_ACTIVE_NEUTRALS_PARAM_NAME = "slc.neutrals.maxActiveNeutrals";
43
+
44
+ // solhint-disable-next-line gas-small-strings
45
+ string private constant MAX_DELEGATION_AMPLIFICATION_PARAM_NAME =
46
+ "slc.neutrals.maxDelegationAmplification";
47
+
48
+ string private constant MINIMUM_STAKE_PARAM_NAME = "slc.neutrals.minimumStake";
49
+
50
+ // solhint-disable-next-line gas-small-strings
51
+ string private constant NEUTRALS_WITHDRAWAL_PERIOD_PARAM_NAME =
52
+ "slc.neutrals.neutralsWithdrawalPeriod";
53
+
54
+ // solhint-disable-next-line gas-small-strings
55
+ string private constant DELEGATORS_WITHDRAWAL_PERIOD_PARAM_NAME =
56
+ "slc.neutrals.delegatorsWithdrawalPeriod";
57
+
58
+ string private constant NEUTRALS_SLASHING_RECIPIENT_PARAM_NAME =
59
+ "slc.neutrals.slashingRecipient";
60
+
61
+ struct DelegatorRewardPool {
62
+ uint256 cumulativeSum;
63
+ }
64
+
65
+ struct Delegation {
66
+ uint256 delegationAmount;
67
+ WithdrawalAnnouncement withdrawal;
68
+ uint256 owedValue;
69
+ uint256 cumulativeSum;
70
+ }
71
+
72
+ struct NeutralsInfo {
73
+ uint256 selfStake;
74
+ uint256 weight;
75
+ uint256 delegatedStake;
76
+ uint256 rewards;
77
+ uint256 delegationShare;
78
+ string externalLink;
79
+ bool isActive;
80
+ bool isStandby;
81
+ }
82
+
83
+ struct WithdrawalAnnouncement {
84
+ uint256 amount;
85
+ uint256 announcedAt;
86
+ uint256 availableAt;
87
+ }
88
+
89
+ struct NeutralsRegistryStorage {
90
+ IGovernance governance;
91
+ IERC20 stakeToken;
92
+ IQParameters parametersRegistry;
93
+ uint256 rewardsToDistribute;
94
+ PriorityQueue.AddressQueue activeNeutrals;
95
+ PriorityQueue.AddressQueue standbyNeutrals;
96
+ uint256 maxActiveNeutrals;
97
+ mapping(address neutral => NeutralsInfo info) neutrals;
98
+ mapping(address neutral => WithdrawalAnnouncement) neutralsWithdrawalAnnouncements;
99
+ mapping(address neutral => DelegatorRewardPool) delegatorsRewardPools;
100
+ mapping(address delegator => mapping(address neutral => Delegation)) delegations;
101
+ mapping(address neutral => EnumerableSet.AddressSet delegators) delegatorsPerNeutral;
102
+ mapping(address delegator => EnumerableSet.AddressSet delegatedNeutrals) neutralsPerDelegator;
103
+ }
104
+
105
+ modifier onlyDAOSLC() {
106
+ _requireDAOSLC();
107
+ _;
108
+ }
109
+
110
+ modifier onlyGovernance() {
111
+ _requireGovernance();
112
+ _;
113
+ }
114
+
115
+ modifier syncedMaxActiveNeutrals() {
116
+ _requiredSyncedMaxActiveNeutrals();
117
+ _;
118
+ }
119
+
120
+ function __NeutralsRegistry_init(address governance_) external initializer {
121
+ __UUPSUpgradeable_init();
122
+
123
+ if (governance_ == address(0)) revert Errors.InvalidGovernance();
124
+
125
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
126
+ $.governance = IGovernance(governance_);
127
+
128
+ address stakeToken_ = $.governance.getSystemToken();
129
+ address parametersRegistry_ = $.governance.getParametersRegistry();
130
+
131
+ if (stakeToken_ == address(0)) revert Errors.InvalidERC20Token();
132
+ if (parametersRegistry_ == address(0)) revert Errors.InvalidParametersRegistry();
133
+
134
+ $.stakeToken = IERC20(stakeToken_);
135
+ $.parametersRegistry = IQParameters(parametersRegistry_);
136
+ $.maxActiveNeutrals = _getMaxActiveNeutrals();
137
+ }
138
+
139
+ /**
140
+ * @notice Stake tokens to become a neutral
141
+ * @param amount_ Amount of tokens to stake
142
+ */
143
+ function addNeutralsStake(uint256 amount_) external syncedMaxActiveNeutrals {
144
+ if (amount_ == 0) revert Errors.InvalidStakeAmount(amount_);
145
+
146
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
147
+ NeutralsInfo storage neutralInfo_ = $.neutrals[msg.sender];
148
+
149
+ neutralInfo_.selfStake += amount_;
150
+
151
+ _manipulateNeutralState(msg.sender);
152
+
153
+ $.stakeToken.safeTransferFrom(msg.sender, address(this), amount_);
154
+
155
+ emit NeutralStaked(msg.sender, amount_, neutralInfo_.selfStake);
156
+ }
157
+
158
+ /**
159
+ * @notice Announce intention to withdraw a portion of self-stake
160
+ * @param amount_ Amount of tokens to withdraw
161
+ */
162
+ function announceNeutralsWithdrawal(uint256 amount_) public syncedMaxActiveNeutrals {
163
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
164
+
165
+ NeutralsInfo storage neutralInfo_ = $.neutrals[msg.sender];
166
+ WithdrawalAnnouncement storage announcement_ = $.neutralsWithdrawalAnnouncements[
167
+ msg.sender
168
+ ];
169
+
170
+ if (neutralInfo_.selfStake == 0) revert NeutralHasNoStake(msg.sender);
171
+ if (amount_ > neutralInfo_.selfStake)
172
+ revert Errors.InsufficientStakeAmount(msg.sender, amount_, neutralInfo_.selfStake);
173
+ if (announcement_.announcedAt != 0) revert WithdrawalAlreadyAnnounced(msg.sender);
174
+
175
+ uint256 withdrawalPeriod_ = _getNeutralsWithdrawalPeriod();
176
+ uint256 availableAt_ = block.timestamp + withdrawalPeriod_;
177
+
178
+ announcement_.amount = amount_;
179
+ announcement_.announcedAt = block.timestamp;
180
+ announcement_.availableAt = availableAt_;
181
+
182
+ neutralInfo_.selfStake -= amount_;
183
+
184
+ _manipulateNeutralState(msg.sender);
185
+
186
+ emit WithdrawalAnnounced(msg.sender, amount_, availableAt_);
187
+ }
188
+
189
+ /**
190
+ * @notice Cancel a previously announced withdrawal
191
+ */
192
+ function cancelNeutralsWithdrawal() public syncedMaxActiveNeutrals {
193
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
194
+ NeutralsInfo storage neutralInfo = $.neutrals[msg.sender];
195
+ WithdrawalAnnouncement storage announcement = $.neutralsWithdrawalAnnouncements[
196
+ msg.sender
197
+ ];
198
+
199
+ if (announcement.announcedAt == 0) revert NoWithdrawalAnnouncement(msg.sender);
200
+
201
+ uint256 amount_ = announcement.amount;
202
+
203
+ neutralInfo.selfStake += amount_;
204
+
205
+ delete $.neutralsWithdrawalAnnouncements[msg.sender];
206
+
207
+ _manipulateNeutralState(msg.sender);
208
+
209
+ emit WithdrawalCancelled(msg.sender, amount_);
210
+ }
211
+
212
+ /**
213
+ * @notice Update the amount for a previously announced withdrawal
214
+ * @param newAmount_ New withdrawal amount
215
+ */
216
+ function updateNeutralsWithdrawal(uint256 newAmount_) public {
217
+ cancelNeutralsWithdrawal();
218
+ announceNeutralsWithdrawal(newAmount_);
219
+ }
220
+
221
+ /**
222
+ * @notice Complete withdrawal after the announcement period has passed
223
+ */
224
+ function completeNeutralsWithdrawal() external {
225
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
226
+ NeutralsInfo storage neutralInfo = $.neutrals[msg.sender];
227
+ WithdrawalAnnouncement storage announcement = $.neutralsWithdrawalAnnouncements[
228
+ msg.sender
229
+ ];
230
+
231
+ if (announcement.announcedAt == 0) revert NoWithdrawalAnnouncement(msg.sender);
232
+
233
+ if (block.timestamp < announcement.availableAt) {
234
+ revert WithdrawalNotReady(msg.sender, announcement.availableAt);
235
+ }
236
+
237
+ uint256 amount_ = announcement.amount;
238
+
239
+ delete $.neutralsWithdrawalAnnouncements[msg.sender];
240
+
241
+ $.stakeToken.safeTransfer(msg.sender, amount_);
242
+
243
+ emit NeutralUnstaked(msg.sender, amount_, neutralInfo.selfStake);
244
+ }
245
+
246
+ /**
247
+ * @notice Delegate tokens to a neutral
248
+ * @param neutral_ Address of the neutral to delegate to
249
+ * @param amount_ Amount of tokens to delegate
250
+ */
251
+ function delegate(address neutral_, uint256 amount_) external {
252
+ if (amount_ == 0) revert InvalidDelegationAmount(amount_);
253
+ if (neutral_ == msg.sender) revert NeutralCannotDelegateToHimself(msg.sender);
254
+
255
+ _checkDelegation(neutral_, amount_);
256
+
257
+ _addDelegationStake(msg.sender, neutral_, amount_);
258
+
259
+ _getNeutralsRegistryStorage().stakeToken.safeTransferFrom(
260
+ msg.sender,
261
+ address(this),
262
+ amount_
263
+ );
264
+ }
265
+
266
+ /**
267
+ * @notice Complete withdrawal of delegated tokens after the announcement period
268
+ * @param neutral_ Address of the neutral
269
+ */
270
+ function completeDelegationWithdrawal(address neutral_) external {
271
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
272
+ WithdrawalAnnouncement storage announcement = $
273
+ .delegations[msg.sender][neutral_].withdrawal;
274
+
275
+ if (announcement.announcedAt == 0) revert NoWithdrawalAnnouncement(msg.sender);
276
+
277
+ if (block.timestamp < announcement.availableAt) {
278
+ revert WithdrawalNotReady(msg.sender, announcement.availableAt);
279
+ }
280
+
281
+ uint256 amount_ = announcement.amount;
282
+
283
+ delete $.delegations[neutral_][msg.sender].withdrawal;
284
+
285
+ $.stakeToken.safeTransfer(msg.sender, amount_);
286
+
287
+ emit DelegationWithdrawn(msg.sender, neutral_, amount_);
288
+ }
289
+
290
+ /**
291
+ * @notice Claim rewards accrued as a neutral
292
+ */
293
+ function claimNeutralRewards() external {
294
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
295
+ uint256 rewards_ = $.neutrals[msg.sender].rewards;
296
+
297
+ if (rewards_ == 0) revert Errors.NoRewardsAvailible(msg.sender);
298
+
299
+ delete $.neutrals[msg.sender].rewards;
300
+
301
+ _getNeutralsRegistryStorage().stakeToken.safeTransfer(msg.sender, rewards_);
302
+
303
+ emit RewardsClaimed(msg.sender, rewards_);
304
+ }
305
+
306
+ /**
307
+ * @notice Claim rewards accrued as a delegator for a specific neutral
308
+ * @param neutral_ Address of the neutral
309
+ */
310
+ function claimDelegatorRewards(address neutral_) external {
311
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
312
+ uint256 rewards_ = _distributeAllDelegatorRewards(msg.sender, neutral_);
313
+
314
+ $.stakeToken.safeTransfer(msg.sender, rewards_);
315
+
316
+ emit RewardsClaimed(msg.sender, rewards_);
317
+ }
318
+
319
+ /**
320
+ * @notice Set the share of rewards to be distributed to delegators
321
+ * @param delegationShare_ Percentage of rewards for delegators in basis points (for example, 250 basis points = 2.5 %)
322
+ */
323
+ function setDelegationShare(uint256 delegationShare_) external {
324
+ require(
325
+ delegationShare_ <= MAX_BASIS_POINTS,
326
+ Errors.ValueShouldBeInBasisPoints(delegationShare_)
327
+ );
328
+
329
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
330
+ NeutralsInfo storage neutralInfo = $.neutrals[msg.sender];
331
+
332
+ if (neutralInfo.selfStake == 0) revert NeutralHasNoStake(msg.sender);
333
+ neutralInfo.delegationShare = delegationShare_;
334
+ }
335
+
336
+ /**
337
+ * @notice Set an external link for the neutral (e.g., profile or info page)
338
+ * @param externalLink_ The external link string
339
+ */
340
+ function setExternalLink(string calldata externalLink_) external {
341
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
342
+ NeutralsInfo storage neutralInfo = $.neutrals[msg.sender];
343
+
344
+ if (neutralInfo.selfStake == 0) revert NeutralHasNoStake(msg.sender);
345
+
346
+ neutralInfo.externalLink = externalLink_;
347
+ }
348
+
349
+ /**
350
+ * @notice Slash a neutral's self-stake (only callable by DAOSLC)
351
+ * @param neutral_ Address of the neutral to slash
352
+ * @param amount_ Amount to slash
353
+ * @dev This function accounts for withdrawal announcements to prevent front-running.
354
+ * If an executor has announced withdrawal, the total slashable amount includes
355
+ * both their current stake and the withdrawal announcement amount.
356
+ */
357
+ function slashNeutral(
358
+ address neutral_,
359
+ uint256 amount_
360
+ ) external onlyDAOSLC syncedMaxActiveNeutrals {
361
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
362
+ NeutralsInfo storage neutralInfo = $.neutrals[neutral_];
363
+ WithdrawalAnnouncement storage announcement = $.neutralsWithdrawalAnnouncements[neutral_];
364
+
365
+ uint256 totalSlashableAmount_ = neutralInfo.selfStake;
366
+ if (announcement.announcedAt != 0) {
367
+ totalSlashableAmount_ += announcement.amount;
368
+ }
369
+
370
+ if (totalSlashableAmount_ == 0) revert NeutralHasNoStake(neutral_);
371
+ if (amount_ > totalSlashableAmount_)
372
+ revert Errors.InsufficientStakeAmount(neutral_, amount_, neutralInfo.selfStake);
373
+
374
+ address slashingRecipient_ = _getSlashingRecipient();
375
+
376
+ if (amount_ > neutralInfo.selfStake && announcement.announcedAt != 0) {
377
+ neutralInfo.selfStake += announcement.amount;
378
+ delete $.neutralsWithdrawalAnnouncements[neutral_];
379
+ }
380
+
381
+ neutralInfo.selfStake -= amount_;
382
+
383
+ _manipulateNeutralState(neutral_);
384
+
385
+ $.stakeToken.safeTransfer(slashingRecipient_, amount_);
386
+
387
+ emit NeutralSlashed(neutral_, amount_, slashingRecipient_);
388
+ }
389
+
390
+ /**
391
+ * @notice Synchronize local maxActiveNeutrals parameter with the one in the registry
392
+ */
393
+ function boundActiveNeutralsList() external {
394
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
395
+ uint256 remoteMaxActiveNeutrals_ = _getMaxActiveNeutrals();
396
+ if ($.maxActiveNeutrals == remoteMaxActiveNeutrals_) {
397
+ revert Errors.ParameterIsAlreadySynced(MAX_ACTIVE_NEUTRALS_PARAM_NAME);
398
+ }
399
+
400
+ $.maxActiveNeutrals = remoteMaxActiveNeutrals_;
401
+
402
+ _handleMaxActiveNeutralsResize();
403
+ }
404
+
405
+ function tryBecomeActive() external syncedMaxActiveNeutrals {
406
+ _tryActivateNeutral(msg.sender);
407
+ }
408
+
409
+ /**
410
+ * @notice Distribute rewards to selected neutrals (only callable by Governance)
411
+ * @param selectedNeutrals_ Array of selected neutral addresses
412
+ * @param amount_ Total amount to distribute
413
+ */
414
+ function distributeRewards(
415
+ address[] calldata selectedNeutrals_,
416
+ uint256 amount_
417
+ ) external onlyGovernance {
418
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
419
+
420
+ uint256 selectedNeutralsLen_ = selectedNeutrals_.length;
421
+ uint256 neutralPart_ = amount_ / selectedNeutralsLen_;
422
+ uint256 remainder_ = amount_ % selectedNeutralsLen_;
423
+
424
+ for (uint256 i = 0; i < selectedNeutralsLen_; ++i) {
425
+ _updateNeutralRewards(selectedNeutrals_[i], neutralPart_);
426
+ }
427
+
428
+ address treasuryAddress_ = _getTreasuryAddress();
429
+
430
+ $.stakeToken.safeTransferFrom(msg.sender, address(this), amount_);
431
+ $.stakeToken.safeTransfer(treasuryAddress_, remainder_);
432
+ emit RewardsDistributed(selectedNeutrals_, amount_);
433
+ }
434
+
435
+ /**
436
+ * @notice Get the current governance contract address
437
+ * @return Address of the governance contract
438
+ */
439
+ function getCurrentGovernance() external view returns (address) {
440
+ return address(_getNeutralsRegistryStorage().governance);
441
+ }
442
+
443
+ /**
444
+ * @notice Get the address of the staking token
445
+ * @return Address of the staking token
446
+ */
447
+ function getToken() external view returns (address) {
448
+ return address(_getNeutralsRegistryStorage().stakeToken);
449
+ }
450
+
451
+ function getCurrentMaxActiveNeutralsParameter() external view returns (uint256) {
452
+ return _getNeutralsRegistryStorage().maxActiveNeutrals;
453
+ }
454
+
455
+ /**
456
+ * @notice Get the address of the parameters registry
457
+ * @return Address of the parameters registry
458
+ */
459
+ function getParameterRegistry() external view returns (address) {
460
+ return address(_getNeutralsRegistryStorage().parametersRegistry);
461
+ }
462
+
463
+ /**
464
+ * @notice Get a paginated list of active neutrals
465
+ * @param offset_ Starting index
466
+ * @param limit_ Maximum number of neutrals to return
467
+ * @return Array of active neutral addresses
468
+ */
469
+ function getActiveNeutrals(
470
+ uint256 offset_,
471
+ uint256 limit_
472
+ ) external view returns (address[] memory) {
473
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
474
+
475
+ uint256 to_ = Paginator.getTo($.activeNeutrals.length(), offset_, limit_);
476
+
477
+ address[] memory result = new address[](to_ - offset_);
478
+
479
+ for (uint256 i = offset_; i < to_; ++i) {
480
+ result[i - offset_] = address(uint160(uint256(($.activeNeutrals._queue._values[i]))));
481
+ }
482
+
483
+ return result;
484
+ }
485
+
486
+ /**
487
+ * @notice Get a paginated list of standby neutrals
488
+ * @param offset_ Starting index
489
+ * @param limit_ Maximum number of neutrals to return
490
+ * @return Array of standby neutral addresses
491
+ */
492
+ function getStandbyNeutrals(
493
+ uint256 offset_,
494
+ uint256 limit_
495
+ ) external view returns (address[] memory) {
496
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
497
+
498
+ uint256 to_ = Paginator.getTo($.standbyNeutrals.length(), offset_, limit_);
499
+
500
+ address[] memory result = new address[](to_ - offset_);
501
+
502
+ for (uint256 i = offset_; i < to_; ++i) {
503
+ result[i - offset_] = address(uint160(uint256(($.standbyNeutrals._queue._values[i]))));
504
+ }
505
+
506
+ return result;
507
+ }
508
+
509
+ /**
510
+ * @notice Get withdrawal announcement information for a neutral
511
+ * @param neutral_ Address of the neutral
512
+ * @return Withdrawal announcement struct
513
+ */
514
+ function getWithdrawalAnnouncement(
515
+ address neutral_
516
+ ) external view returns (WithdrawalAnnouncement memory) {
517
+ return _getNeutralsRegistryStorage().neutralsWithdrawalAnnouncements[neutral_];
518
+ }
519
+
520
+ /**
521
+ * @notice Get delegation of delegator for neutral
522
+ * @param delegator_ Address of the delegator_
523
+ * @param neutral_ Address of the neutral
524
+ * @return Info about delegation
525
+ */
526
+ function getDelegation(
527
+ address delegator_,
528
+ address neutral_
529
+ ) external view returns (Delegation memory) {
530
+ return _getNeutralsRegistryStorage().delegations[delegator_][neutral_];
531
+ }
532
+
533
+ /**
534
+ * @notice Get all delegators for the neutral
535
+ */
536
+ function getDelegatorsPerNeutral(
537
+ address neutral_,
538
+ uint256 offset_,
539
+ uint256 limit_
540
+ ) external view returns (address[] memory) {
541
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
542
+
543
+ return Paginator.part($.delegatorsPerNeutral[neutral_], offset_, limit_);
544
+ }
545
+
546
+ /**
547
+ * @notice Get all neutrals that were delegated by the delegator.
548
+ */
549
+ function getNeutralsPerDelegator(
550
+ address delegator_,
551
+ uint256 offset_,
552
+ uint256 limit_
553
+ ) external view returns (address[] memory) {
554
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
555
+
556
+ return Paginator.part($.neutralsPerDelegator[delegator_], offset_, limit_);
557
+ }
558
+
559
+ /**
560
+ * @notice Check if a neutral's withdrawal is ready to be completed
561
+ * @param neutral_ Address of the neutral
562
+ * @return True if withdrawal can be completed
563
+ */
564
+ function isWithdrawalReady(address neutral_) external view returns (bool) {
565
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
566
+ WithdrawalAnnouncement storage announcement_ = $.neutralsWithdrawalAnnouncements[neutral_];
567
+
568
+ if (announcement_.announcedAt == 0) return false;
569
+
570
+ return block.timestamp >= announcement_.availableAt;
571
+ }
572
+
573
+ /**
574
+ * @notice Get a random slice of active neutrals for selection
575
+ * @param number_ Number of neutrals to select
576
+ * @return Array of selected neutral addresses
577
+ */
578
+ function getNeutralsSlice(uint256 number_) external view returns (address[] memory) {
579
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
580
+ uint256 activeNeutralsLength_ = $.activeNeutrals.length();
581
+
582
+ if (number_ > activeNeutralsLength_ || number_ == 0) {
583
+ revert SelectedNeutralsNumberOutOfRange(number_, activeNeutralsLength_);
584
+ }
585
+
586
+ address[] memory activeNeutrals_ = $.activeNeutrals.values();
587
+ uint256[] memory randomizedWeights_ = new uint256[](activeNeutralsLength_);
588
+ bytes32 seed_ = keccak256(abi.encode(blockhash(block.number - 1)));
589
+
590
+ for (uint256 i = 0; i < activeNeutralsLength_; ++i) {
591
+ address neutral_ = activeNeutrals_[i];
592
+ uint256 neutralWeight_ = $.neutrals[neutral_].weight;
593
+
594
+ uint256 randomNumber_ = NeutralsSelection.getRandomNumber(abi.encodePacked(seed_, i));
595
+ randomizedWeights_[i] = Math.mulDiv(randomNumber_, neutralWeight_, PRECISION);
596
+ }
597
+
598
+ NeutralsSelection.selectTopAddressesByWeight(activeNeutrals_, randomizedWeights_, number_);
599
+
600
+ // solhint-disable-next-line no-inline-assembly
601
+ assembly ("memory-safe") {
602
+ mstore(activeNeutrals_, number_)
603
+ }
604
+
605
+ return activeNeutrals_;
606
+ }
607
+
608
+ /**
609
+ * @notice Get the count of currently active neutrals
610
+ * @return Number of active neutrals
611
+ */
612
+ function getNeutralsCount() external view override returns (uint256) {
613
+ return _getNeutralsRegistryStorage().activeNeutrals.length();
614
+ }
615
+
616
+ /**
617
+ * @notice Get detailed information about a neutral
618
+ * @param neutral_ Address of the neutral
619
+ * @return NeutralsInfo struct with all neutral data
620
+ */
621
+ function getNeutralInfo(address neutral_) external view returns (NeutralsInfo memory) {
622
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
623
+ return $.neutrals[neutral_];
624
+ }
625
+
626
+ /**
627
+ * @notice Get the delegation share set by a neutral
628
+ * @param neutral_ Address of the neutral
629
+ * @return Delegation share (percentage)
630
+ */
631
+ function getNeutralDelegationShare(address neutral_) external view returns (uint256) {
632
+ return _getNeutralsRegistryStorage().neutrals[neutral_].delegationShare;
633
+ }
634
+
635
+ /**
636
+ * @notice Get the external link set by a neutral
637
+ * @param neutral_ Address of the neutral
638
+ * @return The external link string
639
+ */
640
+ function getExternalLink(address neutral_) external view returns (string memory) {
641
+ return _getNeutralsRegistryStorage().neutrals[neutral_].externalLink;
642
+ }
643
+
644
+ function implementation() external view returns (address) {
645
+ return ERC1967Utils.getImplementation();
646
+ }
647
+
648
+ /**
649
+ * @notice Announce intention to withdraw delegated tokens from a neutral
650
+ * @param neutral_ Address of the neutral
651
+ * @param amount_ Amount of tokens to withdraw
652
+ */
653
+ function announceDelegationWithdrawal(address neutral_, uint256 amount_) public {
654
+ if (amount_ == 0) revert InvalidDelegationAmount(amount_);
655
+
656
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
657
+ WithdrawalAnnouncement storage announcement = $
658
+ .delegations[msg.sender][neutral_].withdrawal;
659
+
660
+ uint256 delegationAmount_ = $.delegations[msg.sender][neutral_].delegationAmount;
661
+
662
+ if (amount_ > delegationAmount_)
663
+ revert InsufficientDelegationAmount(msg.sender, neutral_, amount_, delegationAmount_);
664
+ if (announcement.announcedAt != 0) revert WithdrawalAlreadyAnnounced(msg.sender);
665
+
666
+ uint256 withdrawalPeriod_ = _getDelegatorsWithdrawalPeriod();
667
+ uint256 availableAt_ = block.timestamp + withdrawalPeriod_;
668
+
669
+ announcement.amount = amount_;
670
+ announcement.announcedAt = block.timestamp;
671
+ announcement.availableAt = availableAt_;
672
+
673
+ _removeDelegationStake(msg.sender, neutral_, amount_);
674
+
675
+ emit WithdrawalAnnounced(msg.sender, amount_, availableAt_);
676
+ }
677
+
678
+ /**
679
+ * @notice Cancel a previously announced delegation withdrawal
680
+ * @param neutral_ Address of the neutral
681
+ */
682
+ function cancelDelegationWithdrawal(address neutral_) public {
683
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
684
+ WithdrawalAnnouncement storage announcement = $
685
+ .delegations[msg.sender][neutral_].withdrawal;
686
+
687
+ if (announcement.announcedAt == 0) revert NoWithdrawalAnnouncement(msg.sender);
688
+ uint256 amount_ = announcement.amount;
689
+
690
+ _addDelegationStake(msg.sender, neutral_, amount_);
691
+
692
+ delete $.delegations[msg.sender][neutral_].withdrawal;
693
+
694
+ emit WithdrawalCancelled(msg.sender, amount_);
695
+ }
696
+
697
+ /**
698
+ * @notice Update the amount for a previously announced delegation withdrawal
699
+ * @param neutral_ Address of the neutral
700
+ * @param newAmount_ New withdrawal amount
701
+ */
702
+ function updateDelegationWithdrawal(address neutral_, uint256 newAmount_) public {
703
+ cancelDelegationWithdrawal(neutral_);
704
+ announceDelegationWithdrawal(neutral_, newAmount_);
705
+ }
706
+
707
+ /**
708
+ * @notice Check if an address is an active neutral
709
+ * @param candidate_ Address to check
710
+ * @return True if the address is an active neutral
711
+ */
712
+ function isNeutral(address candidate_) public view returns (bool) {
713
+ return _getNeutralsRegistryStorage().neutrals[candidate_].isActive;
714
+ }
715
+
716
+ /**
717
+ * @notice Get the available rewards for a neutral
718
+ * @param neutral_ Address of the neutral
719
+ * @return Amount of available rewards
720
+ */
721
+ function getAvailibleRewards(address neutral_) public view returns (uint256) {
722
+ return _getNeutralsRegistryStorage().neutrals[neutral_].rewards;
723
+ }
724
+
725
+ /// @inheritdoc ERC165
726
+ function supportsInterface(
727
+ bytes4 interfaceId_
728
+ ) public view override(ERC165, IERC165) returns (bool) {
729
+ return
730
+ interfaceId_ == type(INeutralsRegistry).interfaceId ||
731
+ super.supportsInterface(interfaceId_);
732
+ }
733
+
734
+ function _tryActivateNeutral(address neutral_) internal {
735
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
736
+ NeutralsInfo storage neutralInfo = $.neutrals[neutral_];
737
+
738
+ if (neutralInfo.selfStake < _getMinimumStake()) return;
739
+
740
+ if ($.activeNeutrals.length() < $.maxActiveNeutrals) {
741
+ _activateNeutral(neutral_);
742
+ } else {
743
+ address lowestNeutral_ = $.activeNeutrals.topValue();
744
+ uint256 lowestStake_ = $.neutrals[lowestNeutral_].selfStake;
745
+
746
+ if (neutralInfo.selfStake > lowestStake_) {
747
+ _removeFromActiveQueue(lowestNeutral_);
748
+ _addToStandby(lowestNeutral_);
749
+
750
+ _activateNeutral(neutral_);
751
+ } else {
752
+ _addToStandby(neutral_);
753
+ }
754
+ }
755
+ }
756
+
757
+ /**
758
+ * @notice Handle changing of maxActiveNeutrals parameter in Registry.
759
+ * In case of maxActiveNeutrals increasing, do nothing, but if it is decreased,
760
+ * and size of the list of active neutrals is greater than
761
+ * the maxActiveNeutrals parameter, then the list will be reduced.
762
+ * Neutrals, which was removed from active queue, are moved to standby list.
763
+ */
764
+ function _handleMaxActiveNeutralsResize() internal {
765
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
766
+ uint256 maxActiveNeutrals_ = $.maxActiveNeutrals;
767
+ uint256 activeNeutralsLength_ = $.activeNeutrals.length();
768
+
769
+ if (maxActiveNeutrals_ >= activeNeutralsLength_) return;
770
+
771
+ // SAFE: Previously checked, that activeNeutralsLength_ > maxActiveNeutrals
772
+ uint256 delta_ = activeNeutralsLength_ - maxActiveNeutrals_;
773
+
774
+ while (delta_ > 0) {
775
+ address lowestNeutral_ = $.activeNeutrals.topValue();
776
+
777
+ _removeFromActiveQueue(lowestNeutral_);
778
+ _addToStandby(lowestNeutral_);
779
+
780
+ --delta_;
781
+ }
782
+ }
783
+
784
+ function _addToActiveQueue(address neutral_) internal {
785
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
786
+ NeutralsInfo storage neutralInfo = $.neutrals[neutral_];
787
+
788
+ $.activeNeutrals.add(neutral_, type(uint256).max - neutralInfo.selfStake);
789
+
790
+ neutralInfo.isActive = true;
791
+ }
792
+
793
+ function _addToStandby(address neutral_) internal {
794
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
795
+ NeutralsInfo storage neutralInfo = $.neutrals[neutral_];
796
+
797
+ if (neutralInfo.selfStake < _getMinimumStake()) return;
798
+
799
+ if (neutralInfo.isStandby) {
800
+ _removeFromStandby(neutral_);
801
+ }
802
+
803
+ $.standbyNeutrals.add(neutral_, neutralInfo.selfStake);
804
+
805
+ neutralInfo.isStandby = true;
806
+
807
+ emit NeutralMovedToStandby(neutral_);
808
+ }
809
+
810
+ function _removeFromStandby(address neutral_) internal {
811
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
812
+
813
+ $.standbyNeutrals.remove(neutral_);
814
+
815
+ $.neutrals[neutral_].isStandby = false;
816
+ }
817
+
818
+ function _removeFromActiveQueue(address neutral_) internal {
819
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
820
+
821
+ $.activeNeutrals.remove(neutral_);
822
+
823
+ $.neutrals[neutral_].isActive = false;
824
+ }
825
+
826
+ /**
827
+ * @inheritdoc UUPSUpgradeable
828
+ */
829
+ // solhint-disable-next-line no-empty-blocks
830
+ function _authorizeUpgrade(address newImplementation_) internal override onlyDAOSLC {}
831
+
832
+ function _activateNeutral(address neutral_) internal {
833
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
834
+
835
+ _removeFromStandby(neutral_);
836
+ _addToActiveQueue(neutral_);
837
+
838
+ emit NeutralActivated(neutral_, $.neutrals[neutral_].selfStake);
839
+ }
840
+
841
+ function _getMinimumStake() internal view returns (uint256) {
842
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
843
+ try $.parametersRegistry.getUint(MINIMUM_STAKE_PARAM_NAME) returns (
844
+ uint256 minimumStake_
845
+ ) {
846
+ return minimumStake_;
847
+ } catch {
848
+ revert Errors.ParameterIsNotSet(
849
+ address($.parametersRegistry),
850
+ MINIMUM_STAKE_PARAM_NAME
851
+ );
852
+ }
853
+ }
854
+
855
+ function _getMaxDelegationAmplification() private view returns (uint256) {
856
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
857
+
858
+ try $.parametersRegistry.getUint(MAX_DELEGATION_AMPLIFICATION_PARAM_NAME) returns (
859
+ uint256 maxDelegationAmplification_
860
+ ) {
861
+ return maxDelegationAmplification_;
862
+ } catch {
863
+ revert Errors.ParameterIsNotSet(
864
+ address($.parametersRegistry),
865
+ MAX_DELEGATION_AMPLIFICATION_PARAM_NAME
866
+ );
867
+ }
868
+ }
869
+
870
+ function _getMaxActiveNeutrals() internal view returns (uint256) {
871
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
872
+ try $.parametersRegistry.getUint(MAX_ACTIVE_NEUTRALS_PARAM_NAME) returns (
873
+ uint256 maxActiveNeutrals_
874
+ ) {
875
+ return maxActiveNeutrals_;
876
+ } catch {
877
+ revert Errors.ParameterIsNotSet(
878
+ address($.parametersRegistry),
879
+ MAX_ACTIVE_NEUTRALS_PARAM_NAME
880
+ );
881
+ }
882
+ }
883
+
884
+ function _getDAOSLC() internal view returns (address) {
885
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
886
+ try $.parametersRegistry.getAddr(DAOSLC_PARAM_NAME) returns (address daoslc_) {
887
+ return daoslc_;
888
+ } catch {
889
+ revert Errors.ParameterIsNotSet(address($.parametersRegistry), DAOSLC_PARAM_NAME);
890
+ }
891
+ }
892
+
893
+ function _getNeutralsWithdrawalPeriod() internal view returns (uint256) {
894
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
895
+ try $.parametersRegistry.getUint(NEUTRALS_WITHDRAWAL_PERIOD_PARAM_NAME) returns (
896
+ uint256 neutralsWithdrawalPeriod_
897
+ ) {
898
+ return neutralsWithdrawalPeriod_;
899
+ } catch {
900
+ revert Errors.ParameterIsNotSet(
901
+ address($.parametersRegistry),
902
+ NEUTRALS_WITHDRAWAL_PERIOD_PARAM_NAME
903
+ );
904
+ }
905
+ }
906
+
907
+ function _getDelegatorsWithdrawalPeriod() internal view returns (uint256) {
908
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
909
+ try $.parametersRegistry.getUint(NEUTRALS_WITHDRAWAL_PERIOD_PARAM_NAME) returns (
910
+ uint256 delegatorsWithdrawalPeriod_
911
+ ) {
912
+ return delegatorsWithdrawalPeriod_;
913
+ } catch {
914
+ revert Errors.ParameterIsNotSet(
915
+ address($.parametersRegistry),
916
+ DELEGATORS_WITHDRAWAL_PERIOD_PARAM_NAME
917
+ );
918
+ }
919
+ }
920
+
921
+ function _getTreasuryAddress() internal view returns (address) {
922
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
923
+ try $.parametersRegistry.getAddr(TREASURY_ADDRESS_PARAM_NAME) returns (
924
+ address treasuryAddress_
925
+ ) {
926
+ return treasuryAddress_;
927
+ } catch {
928
+ revert Errors.ParameterIsNotSet(
929
+ address($.parametersRegistry),
930
+ TREASURY_ADDRESS_PARAM_NAME
931
+ );
932
+ }
933
+ }
934
+
935
+ function _getSlashingRecipient() internal view returns (address) {
936
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
937
+ try $.parametersRegistry.getAddr(NEUTRALS_SLASHING_RECIPIENT_PARAM_NAME) returns (
938
+ address slashingRecipient_
939
+ ) {
940
+ return slashingRecipient_;
941
+ } catch {
942
+ revert Errors.ParameterIsNotSet(
943
+ address($.parametersRegistry),
944
+ NEUTRALS_SLASHING_RECIPIENT_PARAM_NAME
945
+ );
946
+ }
947
+ }
948
+
949
+ function _recalculateNeutralWeight(address neutral_) private {
950
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
951
+ uint256 selfStake = $.neutrals[neutral_].selfStake;
952
+ uint256 delegatedStake = $.neutrals[neutral_].delegatedStake;
953
+
954
+ $.neutrals[neutral_].weight = Math.sqrt(selfStake + delegatedStake);
955
+ }
956
+
957
+ function _addDelegationStake(address delegator_, address neutral_, uint256 amount_) private {
958
+ _updateDelegatorOwedValue(delegator_, neutral_);
959
+
960
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
961
+ NeutralsInfo storage neutralsInfo = $.neutrals[neutral_];
962
+ Delegation storage delegation = $.delegations[delegator_][neutral_];
963
+
964
+ if (delegation.delegationAmount == 0) {
965
+ $.neutralsPerDelegator[delegator_].add(neutral_);
966
+ $.delegatorsPerNeutral[neutral_].add(delegator_);
967
+ }
968
+
969
+ delegation.delegationAmount += amount_;
970
+ neutralsInfo.delegatedStake += amount_;
971
+
972
+ _recalculateNeutralWeight(neutral_);
973
+ }
974
+
975
+ function _removeDelegationStake(
976
+ address delegator_,
977
+ address neutral_,
978
+ uint256 amount_
979
+ ) private {
980
+ _updateDelegatorOwedValue(delegator_, neutral_);
981
+
982
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
983
+ NeutralsInfo storage neutralsInfo = $.neutrals[neutral_];
984
+ Delegation storage delegation = $.delegations[delegator_][neutral_];
985
+
986
+ delegation.delegationAmount -= amount_;
987
+ neutralsInfo.delegatedStake -= amount_;
988
+
989
+ if (delegation.delegationAmount == 0) {
990
+ $.neutralsPerDelegator[delegator_].remove(neutral_);
991
+ $.delegatorsPerNeutral[neutral_].remove(delegator_);
992
+ }
993
+
994
+ _recalculateNeutralWeight(neutral_);
995
+ }
996
+
997
+ function _updateNeutralRewards(address neutral_, uint256 amount_) private {
998
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
999
+ uint256 delegatedStake_ = $.neutrals[neutral_].delegatedStake;
1000
+
1001
+ if (delegatedStake_ == 0) {
1002
+ $.neutrals[neutral_].rewards += amount_;
1003
+ return;
1004
+ }
1005
+
1006
+ uint256 delegationShare_ = $.neutrals[neutral_].delegationShare;
1007
+ uint256 delegatorsPart_ = Math.mulDiv(delegationShare_, amount_, MAX_BASIS_POINTS);
1008
+ uint256 neutralPart_ = amount_ - delegatorsPart_;
1009
+
1010
+ $.neutrals[neutral_].rewards += neutralPart_;
1011
+ _updateDelegatorsRewardPool(neutral_, delegatorsPart_);
1012
+ }
1013
+
1014
+ function _updateDelegatorsRewardPool(address neutral_, uint256 amount_) private {
1015
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
1016
+ uint256 delegatedStake_ = $.neutrals[neutral_].delegatedStake;
1017
+
1018
+ $.delegatorsRewardPools[neutral_].cumulativeSum += Math.mulDiv(
1019
+ amount_,
1020
+ PRECISION,
1021
+ delegatedStake_
1022
+ );
1023
+ }
1024
+
1025
+ function _updateDelegatorOwedValue(address delegator_, address neutral_) private {
1026
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
1027
+ DelegatorRewardPool storage rewardPool = $.delegatorsRewardPools[neutral_];
1028
+ Delegation storage delegation = $.delegations[delegator_][neutral_];
1029
+
1030
+ uint256 delegatedAmount_ = delegation.delegationAmount;
1031
+ uint256 poolCummulativeSum_ = rewardPool.cumulativeSum;
1032
+ uint256 delegatorCummulativeSum_ = delegation.cumulativeSum;
1033
+
1034
+ delegation.owedValue += Math.mulDiv(
1035
+ delegatedAmount_,
1036
+ poolCummulativeSum_ - delegatorCummulativeSum_,
1037
+ PRECISION
1038
+ );
1039
+
1040
+ delegation.cumulativeSum = rewardPool.cumulativeSum;
1041
+ }
1042
+
1043
+ function _distributeAllDelegatorRewards(
1044
+ address delegator_,
1045
+ address neutral_
1046
+ ) private returns (uint256) {
1047
+ _updateDelegatorOwedValue(delegator_, neutral_);
1048
+
1049
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
1050
+ Delegation storage delegation = $.delegations[delegator_][neutral_];
1051
+
1052
+ uint256 amount_ = delegation.owedValue;
1053
+
1054
+ if (amount_ == 0) revert Errors.NoRewardsAvailible(delegator_);
1055
+
1056
+ delete delegation.owedValue;
1057
+
1058
+ return amount_;
1059
+ }
1060
+
1061
+ function _manipulateNeutralState(address neutral_) private {
1062
+ if (isNeutral(neutral_)) {
1063
+ _removeFromActiveQueue(neutral_);
1064
+ } else {
1065
+ _removeFromStandby(neutral_);
1066
+ }
1067
+
1068
+ _addToStandby(neutral_);
1069
+ _tryActivateFromStandby();
1070
+
1071
+ _recalculateNeutralWeight(neutral_);
1072
+ }
1073
+
1074
+ function _tryActivateFromStandby() private {
1075
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
1076
+
1077
+ if ($.standbyNeutrals.length() == 0) return;
1078
+ _tryActivateNeutral($.standbyNeutrals.topValue());
1079
+ }
1080
+
1081
+ function _requireDAOSLC() private view {
1082
+ if (msg.sender != _getDAOSLC()) {
1083
+ revert Errors.SenderExpectedToBeDAOSLC(msg.sender);
1084
+ }
1085
+ }
1086
+
1087
+ function _checkDelegation(address neutral_, uint256 amount_) private view {
1088
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
1089
+ NeutralsInfo storage neutralInfo_ = $.neutrals[neutral_];
1090
+
1091
+ uint256 neutralsStake_ = neutralInfo_.selfStake;
1092
+ uint256 maxDelegationAmplification_ = _getMaxDelegationAmplification();
1093
+ uint256 expectedDelegationAmount_ = neutralInfo_.delegatedStake + amount_;
1094
+ uint256 allowedDelegationAmount_ = maxDelegationAmplification_ * neutralsStake_;
1095
+
1096
+ if (neutralInfo_.selfStake == 0) revert NeutralHasNoStake(neutral_);
1097
+
1098
+ if (expectedDelegationAmount_ >= allowedDelegationAmount_)
1099
+ revert MaxDelegationAmplificationConstraintViolated(
1100
+ amount_,
1101
+ expectedDelegationAmount_,
1102
+ allowedDelegationAmount_
1103
+ );
1104
+ }
1105
+
1106
+ function _requireGovernance() private view {
1107
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
1108
+ if (msg.sender != address($.governance)) {
1109
+ revert Errors.SenderExpectedToBeGovernance(msg.sender);
1110
+ }
1111
+ }
1112
+
1113
+ function _requiredSyncedMaxActiveNeutrals() private view {
1114
+ NeutralsRegistryStorage storage $ = _getNeutralsRegistryStorage();
1115
+ if ($.maxActiveNeutrals != _getMaxActiveNeutrals()) {
1116
+ revert Errors.ParameterIsNotSynced(MAX_ACTIVE_NEUTRALS_PARAM_NAME);
1117
+ }
1118
+ }
1119
+
1120
+ function _getNeutralsRegistryStorage()
1121
+ private
1122
+ pure
1123
+ returns (NeutralsRegistryStorage storage $)
1124
+ {
1125
+ /* solhint-disable-next-line no-inline-assembly */
1126
+ assembly ("memory-safe") {
1127
+ $.slot := NEUTRALS_REGISTRY_STORAGE_LOCATION
1128
+ }
1129
+ }
1130
+ }