Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/initial-validators-ending' into …
Browse files Browse the repository at this point in the history
…initial-validators-ending-e2e
  • Loading branch information
iansuvak committed Sep 17, 2024
2 parents dac8d50 + 5551db9 commit ffb7151
Show file tree
Hide file tree
Showing 18 changed files with 568 additions and 180 deletions.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

85 changes: 74 additions & 11 deletions abi-bindings/go/staking/PoAValidatorManager/PoAValidatorManager.go

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions contracts/staking/ERC20TokenStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ import {SafeERC20TransferFrom} from "@utilities/SafeERC20TransferFrom.sol";
import {SafeERC20} from "@openzeppelin/[email protected]/token/ERC20/utils/SafeERC20.sol";
import {ICMInitializable} from "../utilities/ICMInitializable.sol";
import {PoSValidatorManager} from "./PoSValidatorManager.sol";
import {
PoSValidatorManagerSettings,
PoSValidatorRequirements
} from "./interfaces/IPoSValidatorManager.sol";
import {PoSValidatorManagerSettings} from "./interfaces/IPoSValidatorManager.sol";
import {ValidatorRegistrationInput} from "./interfaces/IValidatorManager.sol";

contract ERC20TokenStakingManager is
Expand Down
5 changes: 1 addition & 4 deletions contracts/staking/NativeTokenStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import {Initializable} from
"@openzeppelin/[email protected]/proxy/utils/Initializable.sol";
import {ICMInitializable} from "../utilities/ICMInitializable.sol";
import {PoSValidatorManager} from "./PoSValidatorManager.sol";
import {
PoSValidatorManagerSettings,
PoSValidatorRequirements
} from "./interfaces/IPoSValidatorManager.sol";
import {PoSValidatorManagerSettings} from "./interfaces/IPoSValidatorManager.sol";
import {ValidatorRegistrationInput} from "./interfaces/IValidatorManager.sol";

contract NativeTokenStakingManager is
Expand Down
22 changes: 16 additions & 6 deletions contracts/staking/PoSValidatorManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,14 @@ abstract contract PoSValidatorManager is
minStakeDuration >= $._minimumStakeDuration,
"PoSValidatorManager: invalid min stake duration"
);
require(
stakeAmount >= $._minimumStakeAmount && stakeAmount <= $._maximumStakeAmount,
"PoSValidatorManager: invalid stake amount"
);

// Ensure the weight is within the valid range.
require(stakeAmount >= $._minimumStakeAmount, "PoSValidatorManager: stake amount too low");
require(stakeAmount <= $._maximumStakeAmount, "PoSValidatorManager: stake amount too high");

// Lock the stake in the contract.
uint256 lockedValue = _lock(stakeAmount);

uint64 weight = valueToWeight(lockedValue);
bytes32 validationID = _initializeValidatorRegistration(registrationInput, weight);

Expand Down Expand Up @@ -243,6 +244,8 @@ abstract contract PoSValidatorManager is

// Check that the validation ID is a PoS validator
require(_isPoSValidator(validationID), "PoSValidatorManager: not a PoS validator");
// Check that adding this delegator would not exceed the maximum churn rate.
_checkAndUpdateChurnTrackerAddition(weight);

// Update the validator weight
uint64 newValidatorWeight = validator.weight + weight;
Expand All @@ -257,8 +260,6 @@ abstract contract PoSValidatorManager is
uint64 nonce = _incrementAndGetNonce(validationID);
bytes32 delegationID = keccak256(abi.encodePacked(validationID, nonce));

_checkAndUpdateChurnTracker(weight);

// Submit the message to the Warp precompile.
bytes32 messageID = WARP_MESSENGER.sendWarpMessage(
ValidatorMessages.packSetSubnetValidatorWeightMessage(
Expand Down Expand Up @@ -371,6 +372,15 @@ abstract contract PoSValidatorManager is

Validator memory validator = getValidator(validationID);
require(validator.weight > delegator.weight, "PoSValidatorManager: Invalid weight");

// Check that removing this delegator would not exceed the maximum churn rate.
// We only need to check this is the validator is still active. If the validator ends its validation
// period, the weight of all its delegators will be added to the churn tracker at that time. Ending
// a delegation whose validator has ended validating has no impact on the stake weight of the chain.
if (validator.status == ValidatorStatus.Active) {
_checkAndUpdateChurnTrackerRemoval(delegator.weight);
}

uint64 newValidatorWeight = validator.weight - delegator.weight;
_setValidatorWeight(validationID, newValidatorWeight);

Expand Down
121 changes: 81 additions & 40 deletions contracts/staking/ValidatorManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pragma solidity 0.8.25;
import {
IValidatorManager,
ValidatorManagerSettings,
ValidatorChurnPeriod,
ValidatorStatus,
Validator,
ValidatorChurnPeriod,
Expand All @@ -34,9 +35,11 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida
bytes32 _pChainBlockchainID;
/// @notice The subnetID associated with this validator manager.
bytes32 _subnetID;
/// @notice The maximum churn rate allowed per hour.
uint8 _maximumHourlyChurn;
/// @notice The churn tracker used to track the amount of stake added or removed in the past hour.
/// @notice The number of seconds after which to reset the churn tracker.
uint64 _churnPeriodSeconds;
/// @notice The maximum churn rate allowed per churn period.
uint8 _maximumChurnPercentage;
/// @notice The churn tracker used to track the amount of stake added or removed in the churn period.
ValidatorChurnPeriod _churnTracker;
/// @notice Maps the validationID to the registration message such that the message can be re-sent if needed.
mapping(bytes32 => bytes) _pendingRegisterValidationMessages;
Expand All @@ -54,6 +57,9 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida
bytes32 private constant _VALIDATOR_MANAGER_STORAGE_LOCATION =
0xe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb00;

uint8 public constant MAXIMUM_CHURN_PERCENTAGE_LIMIT = 20;
uint64 public constant MAXIMUM_REGISTRATION_EXPIRY_LENGTH = 2 days;

// solhint-disable ordering
function _getValidatorManagerStorage()
private
Expand Down Expand Up @@ -89,7 +95,16 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();
$._pChainBlockchainID = settings.pChainBlockchainID;
$._subnetID = settings.subnetID;
$._maximumHourlyChurn = settings.maximumHourlyChurn;

require(
settings.maximumChurnPercentage <= MAXIMUM_CHURN_PERCENTAGE_LIMIT,
"ValidatorManager: maximum churn percentage too high"
);
require(
settings.maximumChurnPercentage > 0, "ValidatorManager: zero maximum churn percentage"
);
$._maximumChurnPercentage = settings.maximumChurnPercentage;
$._churnPeriodSeconds = settings.churnPeriodSeconds;
$._initializedValidatorSet = false;
}

Expand Down Expand Up @@ -202,11 +217,13 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida
) internal virtual initializedValidatorSet returns (bytes32) {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();

// Ensure the registration expiry is in a valid range.
require(
input.registrationExpiry > block.timestamp
&& block.timestamp + 2 days > input.registrationExpiry,
"ValidatorManager: invalid registration expiry"
input.registrationExpiry > block.timestamp,
"ValidatorManager: registration expiry not in future"
);
require(
block.timestamp + MAXIMUM_REGISTRATION_EXPIRY_LENGTH > input.registrationExpiry,
"ValidatorManager: registration expiry too far in future"
);

// Ensure the nodeID is not the zero address, and is not already an active validator.
Expand All @@ -217,7 +234,8 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida
);
require(input.blsPublicKey.length == 48, "ValidatorManager: invalid blsPublicKey length");

_checkAndUpdateChurnTracker(weight);
// Check that adding this validator would not exceed the maximum churn rate.
_checkAndUpdateChurnTrackerAddition(weight);

(bytes32 validationID, bytes memory registerSubnetValidatorMessage) = ValidatorMessages
.packRegisterSubnetValidatorMessage(
Expand Down Expand Up @@ -321,8 +339,8 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida
validator.status == ValidatorStatus.Active, "ValidatorManager: validator not active"
);

// Check that removing this validator would not exceed the maximum churn rate.
_checkAndUpdateChurnTracker(validator.weight);
// Check that removing this delegator would not exceed the maximum churn rate.
_checkAndUpdateChurnTrackerRemoval(validator.weight);

// Update the validator status to pending removal.
// They are not removed from the active validators mapping until the P-Chain acknowledges the removal.
Expand Down Expand Up @@ -427,35 +445,6 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida
return getValidator(validationID).weight;
}

/**
* @notice Helper function to check if the stake amount to be added or removed would exceed the maximum stake churn
* rate for the past hour. If the churn rate is exceeded, the function will revert. If the churn rate is not exceeded,
* the function will update the churn tracker with the new amount.
*/
// TODO: right now implementation has reference to stake, evaluate for PoA.
function _checkAndUpdateChurnTracker(uint64 amount) internal {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();
if ($._maximumHourlyChurn == 0) {
return;
}

ValidatorChurnPeriod memory churnTracker = $._churnTracker;
uint256 currentTime = block.timestamp;
if (currentTime - churnTracker.startedAt >= 1 hours) {
churnTracker.churnAmount = amount;
churnTracker.startedAt = currentTime;
} else {
churnTracker.churnAmount += amount;
}

uint8 churnPercentage = uint8((churnTracker.churnAmount * 100) / churnTracker.initialStake);
require(
churnPercentage <= $._maximumHourlyChurn,
"ValidatorManager: maximum hourly churn rate exceeded"
);
$._churnTracker = churnTracker;
}

function _incrementAndGetNonce(bytes32 validationID) internal returns (uint64) {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();
$._validationPeriods[validationID].messageNonce++;
Expand Down Expand Up @@ -486,4 +475,56 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();
$._validationPeriods[validationID].weight = weight;
}

/**
* @dev Helper function to check if the stake amount to be added exceeds churn thresholds.
*/
function _checkAndUpdateChurnTrackerAddition(uint64 weight) internal {
_checkAndUpdateChurnTracker(weight, true);
}

/**
* @dev Helper function to check if the stake amount to be removed exceeds churn thresholds.
*/
function _checkAndUpdateChurnTrackerRemoval(uint64 weight) internal {
_checkAndUpdateChurnTracker(weight, false);
}

/**
* @dev Helper function to check if the stake weight to be added or removed would exceed the maximum stake churn
* rate for the past churn period. If the churn rate is exceeded, the function will revert. If the churn rate is
* not exceeded, the function will update the churn tracker with the new weight.
*/
function _checkAndUpdateChurnTracker(uint64 weight, bool addition) private {
ValidatorManagerStorage storage $ = _getValidatorManagerStorage();

uint256 currentTime = block.timestamp;
ValidatorChurnPeriod memory churnTracker = $._churnTracker;

if (
churnTracker.startedAt == 0
|| currentTime >= churnTracker.startedAt + $._churnPeriodSeconds
) {
churnTracker.churnAmount = weight;
churnTracker.startedAt = currentTime;
churnTracker.initialWeight = churnTracker.totalWeight;
} else {
// Churn is always additive whether the weight is being added or removed.
churnTracker.churnAmount += weight;
}

require(
// Rearranged equation of maximumChurnPercentage >= currentChurnPercentage to avoid integer division truncation.
$._maximumChurnPercentage * churnTracker.initialWeight >= churnTracker.churnAmount * 100,
"ValidatorManager: maximum churn rate exceeded"
);

if (addition) {
churnTracker.totalWeight += weight;
} else {
churnTracker.totalWeight -= weight;
}

$._churnTracker = churnTracker;
}
}
22 changes: 22 additions & 0 deletions contracts/staking/ValidatorMessages.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ library ValidatorMessages {
// TODO: The implementation of these packing and unpacking functions is neither tested nor optimized at all.
// Full test coverage should be provided, and the implementation should be optimized for gas efficiency.

function packSubnetConversionMessage(bytes32 subnetConversionID)
internal
pure
returns (bytes memory)
{
bytes memory res = new bytes(38);
// Pack the codec ID
for (uint256 i; i < 2; ++i) {
res[i] = bytes1(uint8(CODEC_ID >> uint8((8 * (1 - i)))));
}
// Pack the type ID
for (uint256 i; i < 4; ++i) {
res[i + 2] = bytes1(uint8(SUBNET_CONVERSION_MESSAGE_TYPE_ID >> uint8((8 * (3 - i)))));
}
// Pack the subnetConversionID
for (uint256 i; i < 32; ++i) {
res[i + 6] = bytes1(uint8(uint256(subnetConversionID >> (8 * (31 - i)))));
}

return res;
}

function unpackSubnetConversionMessage(bytes memory input) internal pure returns (bytes32) {
require(input.length == 38, "ValidatorMessages: invalid message length");

Expand Down
2 changes: 1 addition & 1 deletion contracts/staking/interfaces/IERC20TokenStakingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
pragma solidity 0.8.25;

import {ValidatorRegistrationInput} from "./IValidatorManager.sol";
import {IPoSValidatorManager, PoSValidatorRequirements} from "./IPoSValidatorManager.sol";
import {IPoSValidatorManager} from "./IPoSValidatorManager.sol";

interface IERC20TokenStakingManager is IPoSValidatorManager {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
pragma solidity 0.8.25;

import {ValidatorRegistrationInput} from "./IValidatorManager.sol";
import {IPoSValidatorManager, PoSValidatorRequirements} from "./IPoSValidatorManager.sol";
import {IPoSValidatorManager} from "./IPoSValidatorManager.sol";

interface INativeTokenStakingManager is IPoSValidatorManager {
/**
Expand Down
8 changes: 4 additions & 4 deletions contracts/staking/interfaces/IValidatorManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

// SPDX-License-Identifier: Ecosystem

import {EnumerableSet} from "@openzeppelin/[email protected]/utils/structs/EnumerableSet.sol";

pragma solidity 0.8.25;

enum ValidatorStatus {
Expand All @@ -28,14 +26,16 @@ struct Validator {

struct ValidatorChurnPeriod {
uint256 startedAt;
uint64 initialStake;
uint256 initialWeight;
uint256 totalWeight; // TODO add initial validator set to total weight.
uint64 churnAmount;
}

struct ValidatorManagerSettings {
bytes32 pChainBlockchainID;
bytes32 subnetID;
uint8 maximumHourlyChurn;
uint64 churnPeriodSeconds;
uint8 maximumChurnPercentage;
}

struct SubnetConversionData {
Expand Down
Loading

0 comments on commit ffb7151

Please sign in to comment.