diff --git a/contracts/mocks/ExampleERC721.sol b/contracts/mocks/ExampleERC721.sol new file mode 100644 index 000000000..1e0e60895 --- /dev/null +++ b/contracts/mocks/ExampleERC721.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Ecosystem +pragma solidity 0.8.25; + +import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +contract ExampleERC721 is ERC721 { + constructor() ERC721("Example NFT", "ENFT") { + _mint(msg.sender, 1); + _mint(msg.sender, 2); + _mint(msg.sender, 3); + _mint(msg.sender, 4); + _mint(msg.sender, 5); + _mint(msg.sender, 6); + } + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } + +} \ No newline at end of file diff --git a/contracts/validator-manager/ERC20TokenStakingManager.sol b/contracts/validator-manager/ERC20TokenStakingManager.sol index e0cea5923..1a8df2c04 100644 --- a/contracts/validator-manager/ERC20TokenStakingManager.sol +++ b/contracts/validator-manager/ERC20TokenStakingManager.sol @@ -35,6 +35,7 @@ contract ERC20TokenStakingManager is IERC20Mintable _token; uint8 _tokenDecimals; } + // solhint-enable private-vars-leading-underscore // keccak256(abi.encode(uint256(keccak256("avalanche-icm.storage.ERC20TokenStakingManager")) - 1)) & ~bytes32(uint256(0xff)); @@ -55,7 +56,20 @@ contract ERC20TokenStakingManager is } } - constructor(ICMInitializable init) { + function _addValidatorNft(bytes32 validationID, uint256 tokenId) internal override {} + + function _addDelegatorNft(bytes32 delegationID, uint256 tokenId) internal override {} + + function _deleteValidatorNft( + bytes32 validationID + ) internal override {} + function _deleteDelegatorNft( + bytes32 delegationID + ) internal override {} + + constructor( + ICMInitializable init + ) { if (init == ICMInitializable.Disallowed) { _disableInitializers(); } @@ -138,7 +152,9 @@ contract ERC20TokenStakingManager is * @notice See {PoSValidatorManager-_unlock} * Note: Must be guarded with reentrancy guard for safe transfer. */ - function _unlock(address to, uint256 value) internal virtual override { + function _unlock(address to, bytes32 id, bool isValidator) internal virtual override { + uint64 weight = isValidator ? getValidator(id).startingWeight : getDelegator(id).weight; + uint256 value = weightToValue(weight); _getERC20StakingManagerStorage()._token.safeTransfer(to, value); } diff --git a/contracts/validator-manager/ERC721TokenStakingManager.sol b/contracts/validator-manager/ERC721TokenStakingManager.sol new file mode 100644 index 000000000..5d437b11f --- /dev/null +++ b/contracts/validator-manager/ERC721TokenStakingManager.sol @@ -0,0 +1,231 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: Ecosystem + +pragma solidity 0.8.25; + +import {PoSValidatorManager} from "./PoSValidatorManager.sol"; +import { + PoSValidatorManagerSettings, + PoSValidatorManagerStorage +} from "./interfaces/IPoSValidatorManager.sol"; +import { + ValidatorRegistrationInput, ValidatorManagerStorage +} from "./interfaces/IValidatorManager.sol"; +import {IERC721TokenStakingManager} from "./interfaces/IERC721TokenStakingManager.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ICMInitializable} from "@utilities/ICMInitializable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable@5.0.2/proxy/utils/Initializable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable@5.0.2/access/AccessControlUpgradeable.sol"; + +/** + * @dev Implementation of the {IERC721TokenStakingManager} interface. + * + * @custom:security-contact https://github.com/ava-labs/icm-contracts/blob/main/SECURITY.md + */ +contract ERC721TokenStakingManager is + Initializable, + AccessControlUpgradeable, + PoSValidatorManager, + IERC721TokenStakingManager +{ + using SafeERC20 for IERC20; + + // solhint-disable private-vars-leading-underscore + /// @custom:storage-location erc7201:avalanche-icm.storage.ERC721TokenStakingManager + struct ERC721TokenStakingManagerStorage { + IERC721 _token; + IERC20 _rewardToken; + } + // solhint-enable private-vars-leading-underscore + + // keccak256(abi.encode(uint256(keccak256("avalanche-icm.storage.ERC721TokenStakingManager")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 public constant ERC721_STAKING_MANAGER_STORAGE_LOCATION = + 0xf2d79c30881febd0da8597832b5b1bf1f4d4b2209b19059420303eb8fcab8a00; + + error InvalidTokenAddress(address tokenAddress); + error InvalidRewardTokenAddress(address tokenAddress); + + + // solhint-disable ordering + function _getERC721StakingManagerStorage() + private + pure + returns (ERC721TokenStakingManagerStorage storage $) + { + assembly { + $.slot := ERC721_STAKING_MANAGER_STORAGE_LOCATION + } + } + + function _addValidatorNft(bytes32 validationID, uint256 tokenId) internal override { + ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); + $._validatorNFTs[validationID].nftIds.push(tokenId); + } + + function _addDelegatorNft(bytes32 delegationID, uint256 tokenId) internal override { + PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage(); + $._delegatorNFTs[delegationID].nftIds.push(tokenId); + } + + function _deleteValidatorNft( + bytes32 validationID + ) internal override { + ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); + delete $._validatorNFTs[validationID]; + } + + function _deleteDelegatorNft( + bytes32 delegationID + ) internal override { + PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage(); + delete $._delegatorNFTs[delegationID]; + } + + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); + + modifier onlyOperator() { + require(hasRole(OPERATOR_ROLE, msg.sender), "ERC721TokenStakingManager: caller is not an operator"); + _; + } + + constructor(ICMInitializable init) { + if (init == ICMInitializable.Disallowed) { + _disableInitializers(); + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + } + + /** + * @notice Initialize the ERC721 token staking manager + * @dev Uses reinitializer(2) on the PoS staking contracts to make sure after migration from PoA, the PoS contracts can reinitialize with its needed values. + * @param settings Initial settings for the PoS validator manager + * @param stakingToken The ERC721 token to be staked + * @param rewardToken The ERC20 token to be used for rewards + */ + function initialize( + PoSValidatorManagerSettings calldata settings, + IERC721 stakingToken, + IERC20 rewardToken + ) external reinitializer(2) { + __ERC721TokenStakingManager_init(settings, stakingToken, rewardToken); + } + + // solhint-disable-next-line func-name-mixedcase + function __ERC721TokenStakingManager_init( + PoSValidatorManagerSettings calldata settings, + IERC721 stakingToken, + IERC20 rewardToken + ) internal onlyInitializing { + __POS_Validator_Manager_init(settings); + __ERC721TokenStakingManager_init_unchained(stakingToken, rewardToken); + } + + // solhint-disable-next-line func-name-mixedcase + function __ERC721TokenStakingManager_init_unchained( + IERC721 stakingToken, + IERC20 rewardToken + ) internal onlyInitializing { + ERC721TokenStakingManagerStorage storage $ = _getERC721StakingManagerStorage(); + + if (address(stakingToken) == address(0)) { + revert InvalidTokenAddress(address(stakingToken)); + } + if (address(rewardToken) == address(0)) { + revert InvalidRewardTokenAddress(address(rewardToken)); + } + + $._token = stakingToken; + $._rewardToken = rewardToken; + } + + /** + * @notice See {IERC721TokenStakingManager-initializeValidatorRegistration} + */ + function initializeValidatorRegistration( + ValidatorRegistrationInput calldata registrationInput, + uint16 delegationFeeBips, + uint64 minStakeDuration, + uint256 tokenId + ) external nonReentrant returns (bytes32 validationID) { + return _initializeValidatorRegistration( + registrationInput, delegationFeeBips, minStakeDuration, tokenId + ); + } + + /** + * @notice See {IERC721TokenStakingManager-initializeDelegatorRegistration} + */ + function initializeDelegatorRegistration( + bytes32 validationID, + uint256 tokenId + ) external nonReentrant returns (bytes32) { + return _initializeDelegatorRegistration(validationID, _msgSender(), tokenId); + } + + /** + * @notice Returns the ERC721 token being staked + */ + function erc721() external view returns (IERC721) { + return _getERC721StakingManagerStorage()._token; + } + + /** + * @notice Returns the ERC20 token used for rewards + */ + function rewardToken() external view returns (IERC20) { + return _getERC721StakingManagerStorage()._rewardToken; + } + + /** + * @notice See {PoSValidatorManager-_lock} + * Note: Must be guarded with reentrancy guard for safe transfer from. + */ + function _lock(uint256 tokenId) internal virtual override returns (uint256) { + _getERC721StakingManagerStorage()._token.transferFrom(_msgSender(), address(this), tokenId); + return 1; + } + + /** + * @notice See {PoSValidatorManager-_unlock} + * Note: Must be guarded with reentrancy guard for safe transfer. + */ + function _unlock(address to, bytes32 id, bool isValidator) internal virtual override { + uint256[] memory nfts = isValidator ? getValidatorNfts(id) : getDelegatorNfts(id); + for (uint256 i = 0; i < nfts.length; i++) { + uint256 nftId = nfts[i]; + _getERC721StakingManagerStorage()._token.safeTransferFrom(address(this), to, nftId); + } + } + + /** + * @notice See {PoSValidatorManager-_reward} + * @dev Distributes ERC20 rewards to stakers + */ + function _reward(address account, uint256 amount) internal virtual override { + ERC721TokenStakingManagerStorage storage $ = _getERC721StakingManagerStorage(); + $._rewardToken.safeTransfer(account, amount); + } + + /** + * @notice Allows the contract to receive reward tokens + * @dev Called by owner to fund rewards + * @param amount Amount of reward tokens to transfer to the contract + */ + function fundRewards(uint256 amount) external onlyOperator { + ERC721TokenStakingManagerStorage storage $ = _getERC721StakingManagerStorage(); + $._rewardToken.safeTransferFrom(_msgSender(), address(this), amount); + } + + /** + * @notice Allows owner to recover excess reward tokens + * @param amount Amount of reward tokens to recover + */ + function recoverRewardTokens(uint256 amount) external onlyOperator { + ERC721TokenStakingManagerStorage storage $ = _getERC721StakingManagerStorage(); + $._rewardToken.safeTransfer(_msgSender(), amount); + } +} \ No newline at end of file diff --git a/contracts/validator-manager/ExampleRewardCalculator.sol b/contracts/validator-manager/ExampleRewardCalculator.sol index 856158350..eceef5753 100644 --- a/contracts/validator-manager/ExampleRewardCalculator.sol +++ b/contracts/validator-manager/ExampleRewardCalculator.sol @@ -6,7 +6,6 @@ pragma solidity 0.8.25; import {IRewardCalculator} from "./interfaces/IRewardCalculator.sol"; - contract ExampleRewardCalculator is IRewardCalculator { uint256 public constant SECONDS_IN_YEAR = 31536000; @@ -16,8 +15,11 @@ contract ExampleRewardCalculator is IRewardCalculator { uint64 public immutable rewardBasisPoints; - constructor(uint64 rewardBasisPoints_) { + uint64 public immutable decimals; + + constructor(uint64 rewardBasisPoints_, uint64 decimals_) { rewardBasisPoints = rewardBasisPoints_; + decimals = decimals_; } /** @@ -31,6 +33,7 @@ contract ExampleRewardCalculator is IRewardCalculator { uint64 stakingEndTime, uint64 uptimeSeconds ) external view returns (uint256) { + // Equivalent to uptimeSeconds/(validator.endedAt - validator.startedAt) < UPTIME_REWARDS_THRESHOLD_PERCENTAGE/100 // Rearranged to prevent integer division truncation. if ( @@ -40,7 +43,7 @@ contract ExampleRewardCalculator is IRewardCalculator { return 0; } - return (stakeAmount * rewardBasisPoints * (stakingEndTime - stakingStartTime)) + return (stakeAmount * rewardBasisPoints * (stakingEndTime - stakingStartTime) * (10 ** decimals)) / SECONDS_IN_YEAR / BIPS_CONVERSION_FACTOR; } } diff --git a/contracts/validator-manager/NativeTokenStakingManager.sol b/contracts/validator-manager/NativeTokenStakingManager.sol index facf37a56..e725f7b29 100644 --- a/contracts/validator-manager/NativeTokenStakingManager.sol +++ b/contracts/validator-manager/NativeTokenStakingManager.sol @@ -7,7 +7,9 @@ pragma solidity 0.8.25; import {PoSValidatorManager} from "./PoSValidatorManager.sol"; import {PoSValidatorManagerSettings} from "./interfaces/IPoSValidatorManager.sol"; -import {ValidatorRegistrationInput} from "./interfaces/IValidatorManager.sol"; +import { + ValidatorRegistrationInput, ValidatorManagerStorage +} from "./interfaces/IValidatorManager.sol"; import {INativeTokenStakingManager} from "./interfaces/INativeTokenStakingManager.sol"; import {INativeMinter} from "@avalabs/subnet-evm-contracts@1.2.0/contracts/interfaces/INativeMinter.sol"; @@ -58,6 +60,17 @@ contract NativeTokenStakingManager is // solhint-disable-next-line func-name-mixedcase, no-empty-blocks function __NativeTokenStakingManager_init_unchained() internal onlyInitializing {} + function _addValidatorNft(bytes32 validationID, uint256 tokenId) internal override {} + + function _addDelegatorNft(bytes32 delegationID, uint256 tokenId) internal override {} + + function _deleteValidatorNft( + bytes32 validationID + ) internal override {} + function _deleteDelegatorNft( + bytes32 delegationID + ) internal override {} + /** * @notice See {INativeTokenStakingManager-initializeValidatorRegistration}. */ @@ -93,7 +106,9 @@ contract NativeTokenStakingManager is /** * @notice See {PoSValidatorManager-_unlock} */ - function _unlock(address to, uint256 value) internal virtual override { + function _unlock(address to, bytes32 id, bool isValidator) internal virtual override { + uint64 weight = isValidator ? getValidator(id).startingWeight : getDelegator(id).weight; + uint256 value = weightToValue(weight); payable(to).sendValue(value); } diff --git a/contracts/validator-manager/PoSValidatorManager.sol b/contracts/validator-manager/PoSValidatorManager.sol index 48adb7bb0..98775b21a 100644 --- a/contracts/validator-manager/PoSValidatorManager.sol +++ b/contracts/validator-manager/PoSValidatorManager.sol @@ -11,8 +11,8 @@ import { Delegator, DelegatorStatus, IPoSValidatorManager, - PoSValidatorInfo, - PoSValidatorManagerSettings + PoSValidatorManagerSettings, + PoSValidatorManagerStorage } from "./interfaces/IPoSValidatorManager.sol"; import { Validator, @@ -24,7 +24,6 @@ import {WarpMessage} from "@avalabs/subnet-evm-contracts@1.2.0/contracts/interfaces/IWarpMessenger.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable@5.0.2/utils/ReentrancyGuardUpgradeable.sol"; - /** * @dev Implementation of the {IPoSValidatorManager} interface. * @@ -35,41 +34,6 @@ abstract contract PoSValidatorManager is ValidatorManager, ReentrancyGuardUpgradeable { - // solhint-disable private-vars-leading-underscore - /// @custom:storage-location erc7201:avalanche-icm.storage.PoSValidatorManager - struct PoSValidatorManagerStorage { - /// @notice The minimum amount of stake required to be a validator. - uint256 _minimumStakeAmount; - /// @notice The maximum amount of stake allowed to be a validator. - uint256 _maximumStakeAmount; - /// @notice The minimum amount of time in seconds a validator must be staked for. Must be at least {_churnPeriodSeconds}. - uint64 _minimumStakeDuration; - /// @notice The minimum delegation fee percentage, in basis points, required to delegate to a validator. - uint16 _minimumDelegationFeeBips; - /** - * @notice A multiplier applied to validator's initial stake amount to determine - * the maximum amount of stake a validator can have with delegations. - * Note: Setting this value to 1 would disable delegations to validators, since - * the maximum stake would be equal to the initial stake. - */ - uint64 _maximumStakeMultiplier; - /// @notice The factor used to convert between weight and value. - uint256 _weightToValueFactor; - /// @notice The reward calculator for this validator manager. - IRewardCalculator _rewardCalculator; - /// @notice The ID of the blockchain that submits uptime proofs. This must be a blockchain validated by the l1ID that this contract manages. - bytes32 _uptimeBlockchainID; - /// @notice Maps the validation ID to its requirements. - mapping(bytes32 validationID => PoSValidatorInfo) _posValidatorInfo; - /// @notice Maps the delegation ID to the delegator information. - mapping(bytes32 delegationID => Delegator) _delegatorStakes; - /// @notice Maps the delegation ID to its pending staking rewards. - mapping(bytes32 delegationID => uint256) _redeemableDelegatorRewards; - mapping(bytes32 delegationID => address) _delegatorRewardRecipients; - /// @notice Maps the validation ID to its pending staking rewards. - mapping(bytes32 validationID => uint256) _redeemableValidatorRewards; - mapping(bytes32 validationID => address) _rewardRecipients; - } // solhint-enable private-vars-leading-underscore // keccak256(abi.encode(uint256(keccak256("avalanche-icm.storage.PoSValidatorManager")) - 1)) & ~bytes32(uint256(0xff)); @@ -402,7 +366,8 @@ abstract contract PoSValidatorManager is } // The stake is unlocked whether the validation period is completed or invalidated. - _unlock(owner, weightToValue(validator.startingWeight)); + _unlock(owner, validationID, true); + _deleteValidatorNft(validationID); } /** @@ -465,17 +430,15 @@ abstract contract PoSValidatorManager is if (minStakeDuration < $._minimumStakeDuration) { revert InvalidMinStakeDuration(minStakeDuration); } - // Ensure the weight is within the valid range. if (stakeAmount < $._minimumStakeAmount || stakeAmount > $._maximumStakeAmount) { revert InvalidStakeAmount(stakeAmount); } - // Lock the stake in the contract. uint256 lockedValue = _lock(stakeAmount); - uint64 weight = valueToWeight(lockedValue); bytes32 validationID = _initializeValidatorRegistration(registrationInput, weight); + _addValidatorNft(validationID, stakeAmount); address owner = _msgSender(); @@ -517,9 +480,22 @@ abstract contract PoSValidatorManager is /** * @notice Unlocks token to a specific address. * @param to Address to send token to. - * @param value Number of tokens to lock. + * @param validationID ID of validator + * @param isValidator Whether the unlock is for a validator or a delegator */ - function _unlock(address to, uint256 value) internal virtual; + function _unlock(address to, bytes32 validationID, bool isValidator) internal virtual; + + function _addValidatorNft(bytes32 validationID, uint256 tokenId) internal virtual; + + function _deleteValidatorNft( + bytes32 validationID + ) internal virtual; + + function _addDelegatorNft(bytes32 delegationID, uint256 tokenId) internal virtual; + + function _deleteDelegatorNft( + bytes32 delegationID + ) internal virtual; function _initializeDelegatorRegistration( bytes32 validationID, @@ -528,7 +504,6 @@ abstract contract PoSValidatorManager is ) internal returns (bytes32) { PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage(); uint64 weight = valueToWeight(_lock(delegationAmount)); - // Ensure the validation period is active Validator memory validator = getValidator(validationID); // Check that the validation ID is a PoS validator @@ -546,9 +521,7 @@ abstract contract PoSValidatorManager is } (uint64 nonce, bytes32 messageID) = _setValidatorWeight(validationID, newValidatorWeight); - bytes32 delegationID = keccak256(abi.encodePacked(validationID, nonce)); - // Store the delegation information. Set the delegator status to pending added, // so that it can be properly started in the complete step, even if the delivered // nonce is greater than the nonce used to initialize registration. @@ -559,6 +532,7 @@ abstract contract PoSValidatorManager is $._delegatorStakes[delegationID].startedAt = 0; $._delegatorStakes[delegationID].startingNonce = nonce; $._delegatorStakes[delegationID].endingNonce = 0; + _addDelegatorNft(delegationID, delegationAmount); emit DelegatorAdded({ delegationID: delegationID, @@ -644,6 +618,7 @@ abstract contract PoSValidatorManager is uint32 messageIndex, address rewardRecipient ) external { + _initializeEndDelegationWithCheck( delegationID, includeUptimeProof, messageIndex, rewardRecipient ); @@ -685,6 +660,7 @@ abstract contract PoSValidatorManager is uint32 messageIndex, address rewardRecipient ) external { + // Ignore the return value here to force end delegation, regardless of possible missed rewards _initializeEndDelegation(delegationID, includeUptimeProof, messageIndex, rewardRecipient); } @@ -747,7 +723,6 @@ abstract contract PoSValidatorManager is uint256 reward = _calculateAndSetDelegationReward(delegator, rewardRecipient, delegationID); - emit DelegatorRemovalInitialized({ delegationID: delegationID, validationID: validationID @@ -891,9 +866,6 @@ abstract contract PoSValidatorManager is revert MinStakeDurationNotPassed(uint64(block.timestamp)); } - // Once this function completes, the delegation is completed so we can clear it from state now. - delete $._delegatorStakes[delegationID]; - address rewardRecipient = $._delegatorRewardRecipients[delegationID]; delete $._delegatorRewardRecipients[delegationID]; @@ -905,7 +877,10 @@ abstract contract PoSValidatorManager is _withdrawDelegationRewards(rewardRecipient, delegationID, validationID); // Unlock the delegator's stake. - _unlock(delegator.owner, weightToValue(delegator.weight)); + _unlock(delegator.owner, delegationID, false); + // Once this function completes, the delegation is completed so we can clear it from state now. + delete $._delegatorStakes[delegationID]; + _deleteDelegatorNft(delegationID); emit DelegationEnded(delegationID, validationID, delegationRewards, validatorFees); } @@ -960,4 +935,17 @@ abstract contract PoSValidatorManager is return (delegationRewards, validatorFees); } + + function getDelegator( + bytes32 delegationID + ) public view returns (Delegator memory) { + PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage(); + return $._delegatorStakes[delegationID]; + } + + function getDelegatorNfts( + bytes32 delegationID + ) public view returns (uint256[] memory) { + return _getPoSValidatorManagerStorage()._delegatorNFTs[delegationID].nftIds; + } } diff --git a/contracts/validator-manager/ValidatorManager.sol b/contracts/validator-manager/ValidatorManager.sol index 180847cea..ba0a74102 100644 --- a/contracts/validator-manager/ValidatorManager.sol +++ b/contracts/validator-manager/ValidatorManager.sol @@ -15,7 +15,8 @@ import { ValidatorChurnPeriod, ValidatorManagerSettings, ValidatorRegistrationInput, - ValidatorStatus + ValidatorStatus, + ValidatorManagerStorage } from "./interfaces/IValidatorManager.sol"; import { IWarpMessenger, @@ -32,26 +33,21 @@ import {Initializable} from * @custom:security-contact https://github.com/ava-labs/icm-contracts/blob/main/SECURITY.md */ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValidatorManager { - // solhint-disable private-vars-leading-underscore - /// @custom:storage-location erc7201:avalanche-icm.storage.ValidatorManager - - struct ValidatorManagerStorage { - /// @notice The l1ID associated with this validator manager. - bytes32 _l1ID; - /// @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; - /// @notice Maps the validationID to the validator information. - mapping(bytes32 => Validator) _validationPeriods; - /// @notice Maps the nodeID to the validationID for validation periods that have not ended. - mapping(bytes => bytes32) _registeredValidators; - /// @notice Boolean that indicates if the initial validator set has been set. - bool _initializedValidatorSet; + // solhint-disable ordering + /** + * @dev This storage is visible to child contracts for convenience. + * External getters would be better practice, but code size limitations are preventing this. + * Child contracts should probably never write to this storage. + */ + function _getValidatorManagerStorage() + internal + pure + returns (ValidatorManagerStorage storage $) + { + // solhint-disable-next-line no-inline-assembly + assembly { + $.slot := VALIDATOR_MANAGER_STORAGE_LOCATION + } } // solhint-enable private-vars-leading-underscore @@ -59,7 +55,7 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida bytes32 public constant VALIDATOR_MANAGER_STORAGE_LOCATION = 0xe92546d698950ddd38910d2e15ed1d923cd0a7b3dde9e2a6a3f380565559cb00; - uint8 public constant MAXIMUM_CHURN_PERCENTAGE_LIMIT = 20; + uint8 public constant MAXIMUM_CHURN_PERCENTAGE_LIMIT = 70; uint64 public constant MAXIMUM_REGISTRATION_EXPIRY_LENGTH = 2 days; uint32 public constant ADDRESS_LENGTH = 20; // This is only used as a packed uint32 uint8 public constant BLS_PUBLIC_KEY_LENGTH = 48; @@ -85,23 +81,6 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida error InvalidPChainOwnerThreshold(uint256 threshold, uint256 addressesLength); error PChainOwnerAddressesNotSorted(); - // solhint-disable ordering - /** - * @dev This storage is visible to child contracts for convenience. - * External getters would be better practice, but code size limitations are preventing this. - * Child contracts should probably never write to this storage. - */ - function _getValidatorManagerStorage() - internal - pure - returns (ValidatorManagerStorage storage $) - { - // solhint-disable-next-line no-inline-assembly - assembly { - $.slot := VALIDATOR_MANAGER_STORAGE_LOCATION - } - } - /** * @notice Warp precompile used for sending and receiving Warp messages. */ @@ -124,7 +103,6 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida { ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); $._l1ID = settings.l1ID; - if ( settings.maximumChurnPercentage > MAXIMUM_CHURN_PERCENTAGE_LIMIT || settings.maximumChurnPercentage == 0 @@ -196,9 +174,11 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida // Rearranged equation for totalWeight < (100 / $._maximumChurnPercentage) // Total weight must be above this value in order to not trigger churn limits with an added/removed weight of 1. + if (totalWeight * $._maximumChurnPercentage < 100) { revert InvalidTotalWeight(totalWeight); } + // Verify that the sha256 hash of the L1 conversion data matches with the Warp message's conversionID. bytes32 conversionID = ValidatorMessages.unpackSubnetToL1ConversionMessage( @@ -298,6 +278,7 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida $._validationPeriods[validationID].weight = weight; $._validationPeriods[validationID].startedAt = 0; // The validation period only starts once the registration is acknowledged. $._validationPeriods[validationID].endedAt = 0; + emit ValidationPeriodCreated( validationID, input.nodeID, messageID, weight, input.registrationExpiry @@ -464,7 +445,6 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida } // Remove the validator from the registered validators mapping. delete $._registeredValidators[validator.nodeID]; - // Update the validator. $._validationPeriods[validationID] = validator; @@ -542,6 +522,7 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida uint64 newValidatorWeight, uint64 oldValidatorWeight ) private { + ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); uint64 weightChange; @@ -584,4 +565,10 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida $._churnTracker = churnTracker; } + + function getValidatorNfts( + bytes32 validationID + ) public view returns (uint256[] memory) { + return _getValidatorManagerStorage()._validatorNFTs[validationID].nftIds; + } } diff --git a/contracts/validator-manager/interfaces/IERC721TokenStakingManager.sol b/contracts/validator-manager/interfaces/IERC721TokenStakingManager.sol new file mode 100644 index 000000000..1e23aab98 --- /dev/null +++ b/contracts/validator-manager/interfaces/IERC721TokenStakingManager.sol @@ -0,0 +1,56 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: Ecosystem + +pragma solidity 0.8.25; + +import {ValidatorRegistrationInput} from "./IValidatorManager.sol"; +import {IPoSValidatorManager} from "./IPoSValidatorManager.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * Proof of Stake Validator Manager that stakes ERC721 tokens. + */ +interface IERC721TokenStakingManager is IPoSValidatorManager { + /** + * @notice Begins the validator registration process. Locks the specified ERC721 token in the contract as the stake. + * @param registrationInput The inputs for a validator registration. + * @param delegationFeeBips The fee that delegators must pay to delegate to this validator. + * @param minStakeDuration The minimum amount of time this validator must be staked for in seconds. + * @param tokenId The ID of the NFT to stake. + */ + function initializeValidatorRegistration( + ValidatorRegistrationInput calldata registrationInput, + uint16 delegationFeeBips, + uint64 minStakeDuration, + uint256 tokenId + ) external returns (bytes32 validationID); + + /** + * @notice Begins the delegator registration process. Locks the specified ERC721 token in the contract as the stake. + * @param validationID The ID of the validator to stake to. + * @param tokenId The ID of the NFT to stake. + */ + function initializeDelegatorRegistration( + bytes32 validationID, + uint256 tokenId + ) external returns (bytes32); + + /** + * @notice Returns the ERC721 token contract used for staking + */ + function erc721() external view returns (IERC721); + + /** + * @notice Returns the ERC20 token contract used for rewards + */ + function rewardToken() external view returns (IERC20); + + /** + * @notice Funds the contract with reward tokens + * @param amount Amount of reward tokens to transfer to the contract + */ + function fundRewards(uint256 amount) external; +} \ No newline at end of file diff --git a/contracts/validator-manager/interfaces/IPoSValidatorManager.sol b/contracts/validator-manager/interfaces/IPoSValidatorManager.sol index 3b5798887..a5fa6ba6d 100644 --- a/contracts/validator-manager/interfaces/IPoSValidatorManager.sol +++ b/contracts/validator-manager/interfaces/IPoSValidatorManager.sol @@ -57,6 +57,10 @@ struct Delegator { uint64 endingNonce; } +struct DelegatorNFT { + uint256[] nftIds; +} + /** * @dev Describes the active state of a PoS Validator in addition the information in {IValidatorManager-Validator} */ @@ -67,6 +71,44 @@ struct PoSValidatorInfo { uint64 uptimeSeconds; } +// solhint-disable private-vars-leading-underscore +/// @custom:storage-location erc7201:avalanche-icm.storage.PoSValidatorManager +struct PoSValidatorManagerStorage { + /// @notice The minimum amount of stake required to be a validator. + uint256 _minimumStakeAmount; + /// @notice The maximum amount of stake allowed to be a validator. + uint256 _maximumStakeAmount; + /// @notice The minimum amount of time in seconds a validator must be staked for. Must be at least {_churnPeriodSeconds}. + uint64 _minimumStakeDuration; + /// @notice The minimum delegation fee percentage, in basis points, required to delegate to a validator. + uint16 _minimumDelegationFeeBips; + /** + * @notice A multiplier applied to validator's initial stake amount to determine + * the maximum amount of stake a validator can have with delegations. + * Note: Setting this value to 1 would disable delegations to validators, since + * the maximum stake would be equal to the initial stake. + */ + uint64 _maximumStakeMultiplier; + /// @notice The factor used to convert between weight and value. + uint256 _weightToValueFactor; + /// @notice The reward calculator for this validator manager. + IRewardCalculator _rewardCalculator; + /// @notice The ID of the blockchain that submits uptime proofs. This must be a blockchain validated by the l1ID that this contract manages. + bytes32 _uptimeBlockchainID; + /// @notice Maps the validation ID to its requirements. + mapping(bytes32 validationID => PoSValidatorInfo) _posValidatorInfo; + /// @notice Maps the delegation ID to the delegator information. + mapping(bytes32 delegationID => Delegator) _delegatorStakes; + /// @notice Maps the delegation ID to the delegator's NFTs. + mapping(bytes32 delegationID => DelegatorNFT) _delegatorNFTs; + /// @notice Maps the delegation ID to its pending staking rewards. + mapping(bytes32 delegationID => uint256) _redeemableDelegatorRewards; + mapping(bytes32 delegationID => address) _delegatorRewardRecipients; + /// @notice Maps the validation ID to its pending staking rewards. + mapping(bytes32 validationID => uint256) _redeemableValidatorRewards; + mapping(bytes32 validationID => address) _rewardRecipients; +} + /** * @notice Interface for Proof of Stake Validator Managers */ diff --git a/contracts/validator-manager/interfaces/IValidatorManager.sol b/contracts/validator-manager/interfaces/IValidatorManager.sol index e0c2280de..b82fbc13c 100644 --- a/contracts/validator-manager/interfaces/IValidatorManager.sol +++ b/contracts/validator-manager/interfaces/IValidatorManager.sol @@ -39,6 +39,10 @@ struct Validator { uint64 endedAt; } +struct ValidatorNFT { + uint256[] nftIds; +} + /** * @dev Describes the current churn period */ @@ -93,10 +97,33 @@ struct ValidatorRegistrationInput { PChainOwner remainingBalanceOwner; PChainOwner disableOwner; } - +// solhint-disable private-vars-leading-underscore +/// @custom:storage-location erc7201:avalanche-icm.storage.ValidatorManager + +struct ValidatorManagerStorage { + /// @notice The l1ID associated with this validator manager. + bytes32 _l1ID; + /// @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; + /// @notice Maps the validationID to the validator information. + mapping(bytes32 => Validator) _validationPeriods; + /// @notice Maps the validationID to the validator NFT information. + mapping(bytes32 => ValidatorNFT) _validatorNFTs; + /// @notice Maps the nodeID to the validationID for validation periods that have not ended. + mapping(bytes => bytes32) _registeredValidators; + /// @notice Boolean that indicates if the initial validator set has been set. + bool _initializedValidatorSet; +} /** * @notice Interface for Validator Manager contracts that implement Subnet-only Validator management. */ + interface IValidatorManager { /** * @notice Emitted when a new validation period is created by locking stake in the manager contract. diff --git a/contracts/validator-manager/tests/ERC20TokenStakingManagerTests.t.sol b/contracts/validator-manager/tests/ERC20TokenStakingManagerTests.t.sol index 013398a6c..ec56b53fc 100644 --- a/contracts/validator-manager/tests/ERC20TokenStakingManagerTests.t.sol +++ b/contracts/validator-manager/tests/ERC20TokenStakingManagerTests.t.sol @@ -17,7 +17,6 @@ import {IERC20Mintable} from "../interfaces/IERC20Mintable.sol"; import {SafeERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/utils/SafeERC20.sol"; import {Initializable} from "@openzeppelin/contracts@5.0.2/proxy/utils/Initializable.sol"; import {ValidatorManagerTest} from "./ValidatorManagerTests.t.sol"; - contract ERC20TokenStakingManagerTest is PoSValidatorManagerTest { using SafeERC20 for IERC20Mintable; @@ -222,7 +221,7 @@ contract ERC20TokenStakingManagerTest is PoSValidatorManagerTest { // Construct the object under test app = new ERC20TokenStakingManager(ICMInitializable.Allowed); token = new ExampleERC20(); - rewardCalculator = new ExampleRewardCalculator(DEFAULT_REWARD_RATE); + rewardCalculator = new ExampleRewardCalculator(DEFAULT_REWARD_RATE,0); PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); defaultPoSSettings.rewardCalculator = rewardCalculator; diff --git a/contracts/validator-manager/tests/ERC721/ERC721PoSValidatorManagerTests.t.sol b/contracts/validator-manager/tests/ERC721/ERC721PoSValidatorManagerTests.t.sol new file mode 100644 index 000000000..d95024b28 --- /dev/null +++ b/contracts/validator-manager/tests/ERC721/ERC721PoSValidatorManagerTests.t.sol @@ -0,0 +1,2492 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: Ecosystem + +pragma solidity 0.8.25; + +import {IRewardCalculator} from "../../interfaces/IRewardCalculator.sol"; +import {ERC721ValidatorManagerTest} from "./ERC721ValidatorManagerTests.t.sol"; +import {PoSValidatorManager} from "../../PoSValidatorManager.sol"; +import { + DelegatorStatus, PoSValidatorManagerSettings +} from "../../interfaces/IPoSValidatorManager.sol"; +import {ValidatorManager} from "../../ValidatorManager.sol"; +import { + ValidatorManagerSettings, + ValidatorRegistrationInput, + ValidatorStatus +} from "../../interfaces/IValidatorManager.sol"; +import {ValidatorMessages} from "../../ValidatorMessages.sol"; +import { + WarpMessage, + IWarpMessenger +} from "@avalabs/subnet-evm-contracts@1.2.0/contracts/interfaces/IWarpMessenger.sol"; + +abstract contract ERC721PoSValidatorManagerTest is ERC721ValidatorManagerTest { + uint64 public constant DEFAULT_UPTIME = uint64(100); + uint64 public constant DEFAULT_DELEGATOR_WEIGHT = 1; + uint64 public constant DEFAULT_DELEGATOR_TOKEN_ID = 2; + uint64 public constant DEFAULT_DELEGATOR1_TOKEN_ID = 3; + + + uint64 public constant DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP = + DEFAULT_REGISTRATION_TIMESTAMP + DEFAULT_EXPIRY; + uint64 public constant DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP = + DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP + DEFAULT_EXPIRY; + uint64 public constant DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP = + DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP + DEFAULT_MINIMUM_STAKE_DURATION; + address public constant DEFAULT_DELEGATOR_ADDRESS = + address(0x1234123412341234123412341234123412341234); + address public constant DEFAULT_VALIDATOR_OWNER_ADDRESS = + address(0x2345234523452345234523452345234523452345); + uint64 public constant DEFAULT_REWARD_RATE = uint64(60); + uint64 public constant DEFAULT_MINIMUM_STAKE_DURATION = 24 hours; + uint16 public constant DEFAULT_MINIMUM_DELEGATION_FEE_BIPS = 100; + uint16 public constant DEFAULT_DELEGATION_FEE_BIPS = 150; + uint8 public constant DEFAULT_MAXIMUM_STAKE_MULTIPLIER = 4; + uint256 public constant DEFAULT_WEIGHT_TO_VALUE_FACTOR = 1; + uint256 public constant SECONDS_IN_YEAR = 31536000; + + PoSValidatorManager public posValidatorManager; + IRewardCalculator public rewardCalculator; + + ValidatorRegistrationInput public defaultRegistrationInput = ValidatorRegistrationInput({ + nodeID: DEFAULT_NODE_ID, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + registrationExpiry: DEFAULT_EXPIRY, + remainingBalanceOwner: DEFAULT_P_CHAIN_OWNER, + disableOwner: DEFAULT_P_CHAIN_OWNER + }); + + event ValidationUptimeUpdated(bytes32 indexed validationID, uint64 uptime); + + event DelegatorAdded( + bytes32 indexed delegationID, + bytes32 indexed validationID, + address indexed delegatorAddress, + uint64 nonce, + uint64 validatorWeight, + uint64 delegatorWeight, + bytes32 setWeightMessageID + ); + + event DelegatorRegistered( + bytes32 indexed delegationID, bytes32 indexed validationID, uint256 startTime + ); + + event DelegatorRemovalInitialized(bytes32 indexed delegationID, bytes32 indexed validationID); + + event ValidatorWeightUpdate( + bytes32 indexed validationID, + uint64 indexed nonce, + uint64 weight, + bytes32 setWeightMessageID + ); + + event DelegationEnded( + bytes32 indexed delegationID, bytes32 indexed validationID, uint256 rewards, uint256 fees + ); + + event UptimeUpdated(bytes32 indexed validationID, uint64 uptime); + + function testDelegationFeeBipsTooLow() public { + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidDelegationFee.selector, + DEFAULT_MINIMUM_DELEGATION_FEE_BIPS - 1 + ) + ); + _initializeValidatorRegistration( + defaultRegistrationInput, + DEFAULT_MINIMUM_DELEGATION_FEE_BIPS - 1, + DEFAULT_MINIMUM_STAKE_DURATION, + DEFAULT_MINIMUM_STAKE_AMOUNT + ); + } + + function testDelegationFeeBipsTooHigh() public { + uint16 delegationFeeBips = posValidatorManager.MAXIMUM_DELEGATION_FEE_BIPS() + 1; + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidDelegationFee.selector, delegationFeeBips + ) + ); + + _initializeValidatorRegistration( + defaultRegistrationInput, + delegationFeeBips, + DEFAULT_MINIMUM_STAKE_DURATION, + DEFAULT_MINIMUM_STAKE_AMOUNT + ); + } + + function testInvalidMinStakeDuration() public { + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidMinStakeDuration.selector, + DEFAULT_MINIMUM_STAKE_DURATION - 1 + ) + ); + _initializeValidatorRegistration( + defaultRegistrationInput, + DEFAULT_DELEGATION_FEE_BIPS, + DEFAULT_MINIMUM_STAKE_DURATION - 1, + DEFAULT_MINIMUM_STAKE_AMOUNT + ); + } +/* + function testStakeAmountTooLow() public { + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidStakeAmount.selector, DEFAULT_MINIMUM_STAKE_AMOUNT - 1 + ) + ); + _initializeValidatorRegistration( + defaultRegistrationInput, + DEFAULT_DELEGATION_FEE_BIPS, + DEFAULT_MINIMUM_STAKE_DURATION, + DEFAULT_MINIMUM_STAKE_AMOUNT - 1 + ); + } + + function testStakeAmountTooHigh() public { + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidStakeAmount.selector, + 1 + ) + ); + _initializeValidatorRegistration( + defaultRegistrationInput, + DEFAULT_DELEGATION_FEE_BIPS, + DEFAULT_MINIMUM_STAKE_DURATION, + DEFAULT_MAXIMUM_STAKE_AMOUNT + 1 + ); + } + */ + + function testInvalidInitializeEndTime() public { + bytes32 validationID = _registerDefaultValidator(); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.MinStakeDurationNotPassed.selector, block.timestamp + ) + ); + posValidatorManager.initializeEndValidation(validationID, false, 0); + } + + function testInvalidUptimeWarpMessage() public { + bytes32 validationID = _registerDefaultValidator(); + + _mockGetUptimeWarpMessage(new bytes(0), false); + vm.warp(DEFAULT_COMPLETION_TIMESTAMP); + vm.expectRevert(ValidatorManager.InvalidWarpMessage.selector); + posValidatorManager.initializeEndValidation(validationID, true, 0); + } + + function testInvalidUptimeSenderAddress() public { + bytes32 validationID = _registerDefaultValidator(); + + vm.mockCall( + WARP_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IWarpMessenger.getVerifiedWarpMessage.selector, uint32(0)), + abi.encode( + WarpMessage({ + sourceChainID: DEFAULT_SOURCE_BLOCKCHAIN_ID, + originSenderAddress: address(this), + payload: new bytes(0) + }), + true + ) + ); + vm.expectCall( + WARP_PRECOMPILE_ADDRESS, abi.encodeCall(IWarpMessenger.getVerifiedWarpMessage, 0) + ); + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP); + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.InvalidWarpOriginSenderAddress.selector, address(this) + ) + ); + posValidatorManager.initializeEndValidation(validationID, true, 0); + } + + function testInvalidUptimeValidationID() public { + bytes32 validationID = _registerDefaultValidator(); + + vm.mockCall( + WARP_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IWarpMessenger.getVerifiedWarpMessage.selector, uint32(0)), + abi.encode( + WarpMessage({ + sourceChainID: DEFAULT_SOURCE_BLOCKCHAIN_ID, + originSenderAddress: address(0), + payload: ValidatorMessages.packValidationUptimeMessage(bytes32(0), 0) + }), + true + ) + ); + vm.expectCall( + WARP_PRECOMPILE_ADDRESS, abi.encodeCall(IWarpMessenger.getVerifiedWarpMessage, 0) + ); + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP); + vm.expectRevert( + abi.encodeWithSelector(ValidatorManager.InvalidValidationID.selector, validationID) + ); + posValidatorManager.initializeEndValidation(validationID, true, 0); + } + + function testInitializeDelegatorRegistration() public { + bytes32 validationID = _registerDefaultValidator(); + + _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: DEFAULT_DELEGATOR_ADDRESS, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + registrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + } + + function testResendDelegatorRegistration() public { + bytes32 validationID = _registerDefaultValidator(); + + bytes32 delegationID = _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: DEFAULT_DELEGATOR_ADDRESS, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + registrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, 1, DEFAULT_WEIGHT + DEFAULT_DELEGATOR_WEIGHT + ); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + posValidatorManager.resendUpdateDelegation(delegationID); + } + + function testCompleteDelegatorRegistration() public { + bytes32 validationID = _registerDefaultValidator(); + + _registerDefaultDelegator(validationID); + } + + function testCompleteDelegatorRegistrationWrongNonce() public { + bytes32 validationID = _registerDefaultValidator(); + + // Initialize two delegations + address delegator1 = DEFAULT_DELEGATOR_ADDRESS; + _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: delegator1, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + registrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + address delegator2 = address(0x5678567856785678567856785678567856785678); + bytes32 delegationID2 = _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: delegator2, + weight: DEFAULT_DELEGATOR1_TOKEN_ID, + registrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP + 1, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_DELEGATOR_WEIGHT + + DEFAULT_WEIGHT, + expectedNonce: 2 + }); + + // Complete registration of delegator2 with delegator1's nonce + // Note that registering delegator1 with delegator2's nonce is valid + uint64 nonce = 1; + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, nonce, DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT + ); + _mockGetPChainWarpMessage(setValidatorWeightPayload, true); + + vm.warp(DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP); + vm.expectRevert(abi.encodeWithSelector(PoSValidatorManager.InvalidNonce.selector, nonce)); + posValidatorManager.completeDelegatorRegistration(delegationID2, 0); + } + + function testCompleteDelegatorRegistrationImplicitNonce() public { + bytes32 validationID = _registerDefaultValidator(); + + // Initialize two delegations + address delegator1 = DEFAULT_DELEGATOR_ADDRESS; + bytes32 delegationID1 = _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: delegator1, + weight: 2, + registrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + address delegator2 = address(0x5678567856785678567856785678567856785678); + _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: delegator2, + weight: 3, + registrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP + 1, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_DELEGATOR_WEIGHT + + DEFAULT_WEIGHT, + expectedNonce: 2 + }); + // Mark delegator1 as registered by delivering the weight update from nonce 2 (delegator 2's nonce) + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, 2, DEFAULT_DELEGATOR_WEIGHT + DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT + ); + + _setUpCompleteDelegatorRegistration( + delegationID1, + DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + setValidatorWeightPayload + ); + } + + function testInitializeEndValidationNotOwner() public { + bytes32 validationID = _registerDefaultValidator(); + + vm.prank(address(1)); + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.UnauthorizedOwner.selector, address(1)) + ); + posValidatorManager.initializeEndValidation(validationID, false, 0); + } + + function testInitializeEndDelegation() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + } + + function testInitializeEndDelegationByValidator() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: address(this), + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + } + + function testInitializeEndDelegationByValidatorMinStakeDurationNotPassed() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + uint64 invalidEndTime = DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP + 1 hours; + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.MinStakeDurationNotPassed.selector, invalidEndTime + ) + ); + _initializeEndDelegation({ + sender: address(this), + delegationID: delegationID, + endDelegationTimestamp: invalidEndTime, + includeUptime: false, + force: false, + rewardRecipient: address(0) + }); + } + + function testInitializeEndDelegationMinStakeDurationNotPassed() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + uint64 invalidEndTime = + DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP + DEFAULT_MINIMUM_STAKE_DURATION - 1; + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.MinStakeDurationNotPassed.selector, invalidEndTime + ) + ); + _initializeEndDelegation({ + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + endDelegationTimestamp: invalidEndTime, + includeUptime: false, + force: false, + rewardRecipient: address(0) + }); + } + + function testCompleteEndDelegationChurnPeriodSecondsNotPassed() public { + bytes32 validationID = _registerDefaultValidator(); + uint64 delegatorRegistrationTime = + DEFAULT_REGISTRATION_TIMESTAMP + DEFAULT_MINIMUM_STAKE_DURATION + 1; + bytes32 delegationID = _registerDelegator({ + validationID: validationID, + delegatorAddress: DEFAULT_DELEGATOR_ADDRESS, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + initRegistrationTimestamp: delegatorRegistrationTime - 1, + completeRegistrationTimestamp: delegatorRegistrationTime, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + + address validatorOwner = address(this); + + _endValidationWithChecks({ + validationID: validationID, + validatorOwner: validatorOwner, + completeRegistrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP, + completionTimestamp: delegatorRegistrationTime + 1, + validatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + rewardRecipient: validatorOwner + }); + + uint64 invalidEndTime = delegatorRegistrationTime + DEFAULT_CHURN_PERIOD - 1; + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.MinStakeDurationNotPassed.selector, invalidEndTime + ) + ); + + // Initialize end delegation will also call _completeEndDelegation because the validator is copmleted. + _initializeEndDelegation({ + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + endDelegationTimestamp: invalidEndTime, + includeUptime: false, + force: false, + rewardRecipient: address(0) + }); + } + + function testInitializeEndDelegationInsufficientUptime() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.DelegatorIneligibleForRewards.selector, delegationID + ) + ); + vm.warp(DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP); + vm.prank(DEFAULT_DELEGATOR_ADDRESS); + posValidatorManager.initializeEndDelegation(delegationID, false, 0); + } + + function testForceInitializeEndDelegation() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: true, + rewardRecipient: address(0) + }); + } + + function testForceInitializeEndDelegationInsufficientUptime() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: false, + force: true, + rewardRecipient: address(0) + }); + } + + function testResendEndDelegation() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 2, DEFAULT_WEIGHT); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + posValidatorManager.resendUpdateDelegation(delegationID); + } + + function testResendEndValidation() public override { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + validatorManager.resendEndValidatorMessage(validationID); + } + + function testCompleteEndDelegation() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + _completeDefaultDelegator(validationID, delegationID); + } + + function testClaimDelegationFeesInvalidValidatorStatus() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + _completeDefaultDelegator(validationID, delegationID); + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.InvalidValidatorStatus.selector, ValidatorStatus.Active + ) + ); + posValidatorManager.claimDelegationFees(validationID); + } + + function testClaimDelegationFeesInvalidSender() public { + bytes32 validationID = _registerDefaultValidator(); + _registerDefaultDelegator(validationID); + + _endDefaultValidatorWithChecks(validationID, 2); + + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.UnauthorizedOwner.selector, address(123)) + ); + + vm.prank(address(123)); + posValidatorManager.claimDelegationFees(validationID); + } + + function testClaimDelegationFees() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + address rewardRecipient = address(42); + + _endDefaultValidatorWithChecks(validationID, 2); + // Validator is Completed, so this will also complete the delegation. + _initializeEndDelegation({ + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + includeUptime: true, + force: false, + rewardRecipient: rewardRecipient + }); + + uint256 expectedTotalReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_DELEGATOR_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_COMPLETION_TIMESTAMP, + uptimeSeconds: DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + _expectRewardIssuance( + address(this), expectedTotalReward * DEFAULT_DELEGATION_FEE_BIPS / 10000 + ); + posValidatorManager.claimDelegationFees(validationID); + } + + + function testCompleteEndDelegationWithNonDelegatorRewardRecipient() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + address rewardRecipient = address(42); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: rewardRecipient + }); + + uint256 expectedTotalReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_DELEGATOR_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + uptimeSeconds: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + uint256 expectedValidatorFees = + _calculateValidatorFeesFromDelegator(expectedTotalReward, DEFAULT_DELEGATION_FEE_BIPS); + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + + _completeEndDelegationWithChecks({ + validationID: validationID, + delegationID: delegationID, + delegator: DEFAULT_DELEGATOR_ADDRESS, + delegatorWeight: DEFAULT_DELEGATOR_TOKEN_ID, + expectedValidatorFees: expectedValidatorFees, + expectedDelegatorReward: expectedDelegatorReward, + validatorWeight: DEFAULT_WEIGHT, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + rewardRecipient: rewardRecipient + }); + } + + function testChangeDelegatorRewardRecipientWithNullAddress() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + address rewardRecipient = address(42); + address newRewardRecipient = address(0); + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: rewardRecipient + }); + vm.prank(DEFAULT_DELEGATOR_ADDRESS); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidRewardRecipient.selector, newRewardRecipient + ) + ); + posValidatorManager.changeDelegatorRewardRecipient(delegationID, newRewardRecipient); + } + + + function testChangeDelegatorRewardRecipientByNonDelegator() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + address rewardRecipient = address(42); + address badActor = address(43); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: rewardRecipient + }); + + vm.prank(badActor); + + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.UnauthorizedOwner.selector, badActor) + ); + + posValidatorManager.changeDelegatorRewardRecipient(delegationID, badActor); + } + + function testChangeDelegatorRewardRecipientBackToSelf() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + address rewardRecipient = address(42); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: rewardRecipient + }); + + vm.prank(DEFAULT_DELEGATOR_ADDRESS); + + posValidatorManager.changeDelegatorRewardRecipient(delegationID, DEFAULT_DELEGATOR_ADDRESS); + + uint256 expectedTotalReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_DELEGATOR_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + uptimeSeconds: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + uint256 expectedValidatorFees = + _calculateValidatorFeesFromDelegator(expectedTotalReward, DEFAULT_DELEGATION_FEE_BIPS); + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + + _completeEndDelegationWithChecks({ + validationID: validationID, + delegationID: delegationID, + delegator: DEFAULT_DELEGATOR_ADDRESS, + delegatorWeight: DEFAULT_DELEGATOR_TOKEN_ID, + expectedValidatorFees: expectedValidatorFees, + expectedDelegatorReward: expectedDelegatorReward, + validatorWeight: DEFAULT_WEIGHT, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + rewardRecipient: DEFAULT_DELEGATOR_ADDRESS + }); + } + + function testChangeDelegatorRewardRecipient() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + address rewardRecipient = address(42); + address newRewardRecipient = address(43); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: rewardRecipient + }); + + vm.prank(DEFAULT_DELEGATOR_ADDRESS); + posValidatorManager.changeDelegatorRewardRecipient(delegationID, newRewardRecipient); + + uint256 expectedTotalReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_DELEGATOR_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + uptimeSeconds: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + uint256 expectedValidatorFees = + _calculateValidatorFeesFromDelegator(expectedTotalReward, DEFAULT_DELEGATION_FEE_BIPS); + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + + _completeEndDelegationWithChecks({ + validationID: validationID, + delegationID: delegationID, + delegator: DEFAULT_DELEGATOR_ADDRESS, + delegatorWeight: DEFAULT_DELEGATOR_TOKEN_ID, + expectedValidatorFees: expectedValidatorFees, + expectedDelegatorReward: expectedDelegatorReward, + validatorWeight: DEFAULT_WEIGHT, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + rewardRecipient: newRewardRecipient + }); + } + + + // Delegator registration is not allowed when Validator is pending removed. + function testInitializeDelegatorRegistrationValidatorPendingRemoved() public { + bytes32 validationID = _registerDefaultValidator(); + + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + _beforeSend(_weightToValue(DEFAULT_DELEGATOR_TOKEN_ID), DEFAULT_DELEGATOR_ADDRESS); + + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.InvalidValidatorStatus.selector, ValidatorStatus.PendingRemoved + ) + ); + _initializeDelegatorRegistration( + validationID, DEFAULT_DELEGATOR_ADDRESS, DEFAULT_DELEGATOR_TOKEN_ID + ); + } + + // Complete delegator registration may be called when validator is pending removed. + function testCompleteRegisterDelegatorValidatorPendingRemoved() public { + bytes32 validationID = _registerDefaultValidator(); + + bytes32 delegationID = _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: DEFAULT_DELEGATOR_ADDRESS, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + registrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 2, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + _setUpCompleteDelegatorRegistrationWithChecks( + validationID, + delegationID, + DEFAULT_COMPLETION_TIMESTAMP + 1, + DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + 1 + ); + } + + // Delegator cannot initialize end delegation when validator is pending removed. + function testInitializeEndDelegationValidatorPendingRemoved() public { + bytes32 validationID = _registerDefaultValidator(); + + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + _beforeSend(_weightToValue(DEFAULT_DELEGATOR_TOKEN_ID), DEFAULT_DELEGATOR_ADDRESS); + + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.InvalidValidatorStatus.selector, ValidatorStatus.PendingRemoved + ) + ); + _initializeDelegatorRegistration( + validationID, DEFAULT_DELEGATOR_ADDRESS, DEFAULT_DELEGATOR_TOKEN_ID + ); + } + + // Delegator may complete end delegation while validator is pending removed. + function testCompleteEndDelegationValidatorPendingRemoved() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + + uint64 validationEndTime = DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP + 1; + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 3, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, validationEndTime - DEFAULT_REGISTRATION_TIMESTAMP + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: validationEndTime, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + uint256 expectedTotalReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_DELEGATOR_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + uptimeSeconds: validationEndTime - DEFAULT_REGISTRATION_TIMESTAMP + }); + + uint256 expectedValidatorFees = + _calculateValidatorFeesFromDelegator(expectedTotalReward, DEFAULT_DELEGATION_FEE_BIPS); + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + + address delegator = DEFAULT_DELEGATOR_ADDRESS; + + _completeEndDelegationWithChecks({ + validationID: validationID, + delegationID: delegationID, + delegator: delegator, + delegatorWeight: DEFAULT_DELEGATOR_TOKEN_ID, + expectedValidatorFees: expectedValidatorFees, + expectedDelegatorReward: expectedDelegatorReward, + validatorWeight: DEFAULT_WEIGHT, + expectedValidatorWeight: 0, + expectedNonce: 2, + rewardRecipient: delegator + }); + } + + function testInitializeDelegatorRegistrationValidatorCompleted() public { + bytes32 validationID = _registerDefaultValidator(); + _endDefaultValidatorWithChecks(validationID, 1); + + _beforeSend(_weightToValue(DEFAULT_DELEGATOR_WEIGHT), DEFAULT_DELEGATOR_ADDRESS); + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP + 1); + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.InvalidValidatorStatus.selector, ValidatorStatus.Completed + ) + ); + _initializeDelegatorRegistration( + validationID, DEFAULT_DELEGATOR_ADDRESS, DEFAULT_DELEGATOR_WEIGHT + ); + } + + function testCompleteDelegatorRegistrationValidatorCompleted() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _initializeDefaultDelegatorRegistration(validationID); + + _endDefaultValidatorWithChecks(validationID, 2); + + // completeDelegatorRegistration should fall through to _completeEndDelegation and refund the stake + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit DelegationEnded(delegationID, validationID, 0, 0); + + uint256 balanceBefore = _getStakeAssetBalance(DEFAULT_DELEGATOR_ADDRESS); + + _expectStakeUnlock(DEFAULT_DELEGATOR_ADDRESS, _weightToValue(DEFAULT_DELEGATOR_TOKEN_ID)); + + // warp to right after validator ended + vm.warp(DEFAULT_COMPLETION_TIMESTAMP + 1); + posValidatorManager.completeDelegatorRegistration(delegationID, 0); + + assertEq( + _getStakeAssetBalance(DEFAULT_DELEGATOR_ADDRESS), + balanceBefore + _weightToValue(DEFAULT_DELEGATOR_WEIGHT) + ); + } + + function testInitializeEndDelegationValidatorCompleted() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _endDefaultValidatorWithChecks(validationID, 2); + + uint64 delegationEndTime = DEFAULT_COMPLETION_TIMESTAMP + 1; + + uint256 expectedTotalReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_DELEGATOR_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_COMPLETION_TIMESTAMP, + uptimeSeconds: DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + uint256 expectedValidatorFees = expectedTotalReward * DEFAULT_DELEGATION_FEE_BIPS / 10000; + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + + // completeDelegatorRegistration should fall through to _completeEndDelegation and refund the stake + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit DelegationEnded( + delegationID, validationID, expectedDelegatorReward, expectedValidatorFees + ); + + uint256 balanceBefore = _getStakeAssetBalance(DEFAULT_DELEGATOR_ADDRESS); + uint256 rewardBefore = _getRewardAssetBalance(DEFAULT_DELEGATOR_ADDRESS); + + _expectStakeUnlock(DEFAULT_DELEGATOR_ADDRESS, _weightToValue(DEFAULT_DELEGATOR_TOKEN_ID)); + _expectRewardIssuance(DEFAULT_DELEGATOR_ADDRESS, expectedDelegatorReward); + + // warp to right after validator ended + vm.warp(delegationEndTime); + vm.prank(DEFAULT_DELEGATOR_ADDRESS); + posValidatorManager.initializeEndDelegation(delegationID, false, 0); + + assertEq( + _getStakeAssetBalance(DEFAULT_DELEGATOR_ADDRESS), + balanceBefore + _weightToValue(DEFAULT_DELEGATOR_WEIGHT) + ); + assertEq( + _getRewardAssetBalance(DEFAULT_DELEGATOR_ADDRESS), + rewardBefore + expectedDelegatorReward + ); + } + + function testCompleteEndDelegationValidatorCompleted() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + + _endDefaultValidatorWithChecks(validationID, 3); + + uint256 expectedTotalReward = _defaultDelegatorExpectedTotalReward(); + + uint256 expectedValidatorFees = (expectedTotalReward * DEFAULT_DELEGATION_FEE_BIPS) / 10000; + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + + vm.expectEmit(true, true, true, true, address(posValidatorManager)); + emit DelegationEnded( + delegationID, validationID, expectedDelegatorReward, expectedValidatorFees + ); + uint256 balanceBefore = _getStakeAssetBalance(DEFAULT_DELEGATOR_ADDRESS); + uint256 rewardBefore = _getRewardAssetBalance(DEFAULT_DELEGATOR_ADDRESS); + + _expectStakeUnlock(DEFAULT_DELEGATOR_ADDRESS, _weightToValue(DEFAULT_DELEGATOR_TOKEN_ID)); + _expectRewardIssuance(DEFAULT_DELEGATOR_ADDRESS, expectedDelegatorReward); + + posValidatorManager.completeEndDelegation(delegationID, 0); + assertEq( + _getStakeAssetBalance(DEFAULT_DELEGATOR_ADDRESS), + balanceBefore + _weightToValue(DEFAULT_DELEGATOR_WEIGHT) + ); + assertEq( + _getRewardAssetBalance(DEFAULT_DELEGATOR_ADDRESS), + rewardBefore + expectedDelegatorReward + ); + } + + function testCompleteEndDelegationWrongNonce() public { + bytes32 validationID = _registerDefaultValidator(); + // Register two delegations + address delegator1 = DEFAULT_DELEGATOR_ADDRESS; + bytes32 delegationID1 = _registerDelegator({ + validationID: validationID, + delegatorAddress: delegator1, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + initRegistrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + completeRegistrationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + + address delegator2 = address(0x5678567856785678567856785678567856785678); + bytes32 delegationID2 = _registerDelegator({ + validationID: validationID, + delegatorAddress: delegator2, + weight: DEFAULT_DELEGATOR1_TOKEN_ID, + initRegistrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP + 1, + completeRegistrationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT * 2 + DEFAULT_WEIGHT, + expectedNonce: 2 + }); + + // Initialize end delegation for both delegators + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: delegator1, + delegationID: delegationID1, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 3, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: delegator2, + delegationID: delegationID2, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP + 1, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 4, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + + // Complete ending delegator2 with delegator1's nonce + // Note that ending delegator1 with delegator2's nonce is valid + uint64 nonce = 3; + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, nonce, DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT + ); + _mockGetPChainWarpMessage(setValidatorWeightPayload, true); + + vm.expectRevert(abi.encodeWithSelector(PoSValidatorManager.InvalidNonce.selector, nonce)); + posValidatorManager.completeEndDelegation(delegationID2, 0); + } + + function testCompleteEndDelegationImplicitNonce() public { + bytes32 validationID = _registerDefaultValidator(); + + // Register two delegations + address delegator1 = DEFAULT_DELEGATOR_ADDRESS; + bytes32 delegationID1 = _registerDelegator({ + validationID: validationID, + delegatorAddress: delegator1, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + initRegistrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + completeRegistrationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + + address delegator2 = address(0x5678567856785678567856785678567856785678); + bytes32 delegationID2 = _registerDelegator({ + validationID: validationID, + delegatorAddress: delegator2, + weight: DEFAULT_DELEGATOR1_TOKEN_ID, + initRegistrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP + 1, + completeRegistrationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT * 2 + DEFAULT_WEIGHT, + expectedNonce: 2 + }); + + // Initialize end delegation for both delegators + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: delegator1, + delegationID: delegationID1, + startDelegationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 3, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: delegator2, + delegationID: delegationID2, + startDelegationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP + 1, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 4, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + + uint256 expectedTotalReward = _defaultDelegatorExpectedTotalReward(); + + uint256 expectedValidatorFees = + _calculateValidatorFeesFromDelegator(expectedTotalReward, DEFAULT_DELEGATION_FEE_BIPS); + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + + address delegator = DEFAULT_DELEGATOR_ADDRESS; + + // Complete delegation1 by delivering the weight update from nonce 4 (delegator2's nonce) + _completeEndDelegationWithChecks({ + validationID: validationID, + delegationID: delegationID1, + delegator: delegator, + delegatorWeight: DEFAULT_DELEGATOR_TOKEN_ID, + expectedValidatorFees: expectedValidatorFees, + expectedDelegatorReward: expectedDelegatorReward, + validatorWeight: DEFAULT_WEIGHT, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 4, + rewardRecipient: delegator + }); + } + + + function testCompleteEndValidation() public virtual override { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + uint256 expectedReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_COMPLETION_TIMESTAMP, + uptimeSeconds: DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + address validatorOwner = address(this); + + _completeEndValidationWithChecks({ + validationID: validationID, + validatorOwner: validatorOwner, + expectedReward: expectedReward, + validatorWeight: DEFAULT_WEIGHT, + rewardRecipient: validatorOwner + }); + } + + function testCompleteEndValidationWithNonValidatorRewardRecipient() public virtual { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + address rewardRecipient = address(42); + + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false, + recipientAddress: rewardRecipient + }); + + uint256 expectedReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_COMPLETION_TIMESTAMP, + uptimeSeconds: DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + address validatorOwner = address(this); + + _completeEndValidationWithChecks({ + validationID: validationID, + validatorOwner: validatorOwner, + expectedReward: expectedReward, + validatorWeight: DEFAULT_WEIGHT, + rewardRecipient: rewardRecipient + }); + } + + function testChangeValidatorRewardRecipient() public virtual { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + address rewardRecipient = address(42); + address newRecipient = address(43); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false, + recipientAddress: rewardRecipient + }); + posValidatorManager.changeValidatorRewardRecipient(validationID, newRecipient); + uint256 expectedReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_COMPLETION_TIMESTAMP, + uptimeSeconds: DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + _completeEndValidationWithChecks({ + validationID: validationID, + validatorOwner: address(this), + expectedReward: expectedReward, + validatorWeight: DEFAULT_WEIGHT, + rewardRecipient: newRecipient + }); + } +/* + function testChangeValidatorRewardRecipientBackToSelf() public { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + address rewardRecipient = address(42); + address newRecipient = address(this); + + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false, + recipientAddress: rewardRecipient + }); + + posValidatorManager.changeValidatorRewardRecipient(validationID, newRecipient); + + uint256 expectedReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_COMPLETION_TIMESTAMP, + uptimeSeconds: DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + address validatorOwner = address(this); + + _completeEndValidationWithChecks({ + validationID: validationID, + validatorOwner: validatorOwner, + expectedReward: expectedReward, + validatorWeight: DEFAULT_WEIGHT, + rewardRecipient: validatorOwner + }); + } + + + function testChangeValidatorRewardRecipientWithNullAddress() public virtual { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + address rewardRecipient = address(42); + address newRecipient = address(0); + + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false, + recipientAddress: rewardRecipient + }); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidRewardRecipient.selector, newRecipient + ) + ); + + posValidatorManager.changeValidatorRewardRecipient(validationID, newRecipient); + } + + function testChangeValidatorRewardRecipientByNonValidator() public { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + address rewardRecipient = address(42); + address badActor = address(43); + + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false, + recipientAddress: rewardRecipient + }); + + vm.prank(badActor); + + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.UnauthorizedOwner.selector, badActor) + ); + + posValidatorManager.changeValidatorRewardRecipient(validationID, badActor); + } + */ + + function testInitializeEndValidation() public virtual override { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + } + + function testInitializeEndValidationUseStoredUptime() public { + bytes32 validationID = _registerDefaultValidator(); + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP); + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + + // Submit an uptime proof via submitUptime + uint64 uptimePercentage1 = 80; + uint64 uptime1 = ( + (DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP) * uptimePercentage1 + ) / 100; + bytes memory uptimeMsg1 = + ValidatorMessages.packValidationUptimeMessage(validationID, uptime1); + _mockGetUptimeWarpMessage(uptimeMsg1, true); + + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit UptimeUpdated(validationID, uptime1); + posValidatorManager.submitUptimeProof(validationID, 0); + + // Submit a second uptime proof via initializeEndValidation. This one is not sufficient for rewards + // Submit an uptime proof via submitUptime + uint64 uptimePercentage2 = 79; + uint64 uptime2 = ( + (DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP) * uptimePercentage2 + ) / 100; + bytes memory uptimeMsg2 = + ValidatorMessages.packValidationUptimeMessage(validationID, uptime2); + _mockGetUptimeWarpMessage(uptimeMsg2, true); + + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit ValidatorRemovalInitialized( + validationID, bytes32(0), DEFAULT_WEIGHT, DEFAULT_COMPLETION_TIMESTAMP + ); + + _initializeEndValidation(validationID, true, address(0)); + } + + function testInitializeEndValidationWithoutNewUptime() public { + bytes32 validationID = _registerDefaultValidator(); + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP); + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + + // Submit an uptime proof via submitUptime + uint64 uptimePercentage1 = 80; + uint64 uptime1 = ( + (DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP) * uptimePercentage1 + ) / 100; + bytes memory uptimeMsg1 = + ValidatorMessages.packValidationUptimeMessage(validationID, uptime1); + _mockGetUptimeWarpMessage(uptimeMsg1, true); + + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit UptimeUpdated(validationID, uptime1); + posValidatorManager.submitUptimeProof(validationID, 0); + + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit ValidatorRemovalInitialized( + validationID, bytes32(0), DEFAULT_WEIGHT, DEFAULT_COMPLETION_TIMESTAMP + ); + + _initializeEndValidation(validationID, false, address(0)); + } + + function testInitializeEndValidationInsufficientUptime() public { + bytes32 validationID = _registerDefaultValidator(); + uint64 uptimePercentage = 79; + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP); + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + + bytes memory uptimeMsg = ValidatorMessages.packValidationUptimeMessage( + validationID, + ((DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP) * uptimePercentage) + / 100 + ); + _mockGetUptimeWarpMessage(uptimeMsg, true); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.ValidatorIneligibleForRewards.selector, validationID + ) + ); + + _initializeEndValidation(validationID, true, address(0)); + } + + function testSubmitUptimeProofPoaValidator() public { + bytes32 defaultInitialValidationID = sha256(abi.encodePacked(DEFAULT_L1_ID, uint32(1))); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.ValidatorNotPoS.selector, defaultInitialValidationID + ) + ); + posValidatorManager.submitUptimeProof(defaultInitialValidationID, 0); + } + + function testSubmitUptimeProofInactiveValidator() public { + bytes32 validationID = _registerDefaultValidator(); + + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + _beforeSend(_weightToValue(DEFAULT_DELEGATOR_TOKEN_ID), DEFAULT_DELEGATOR_ADDRESS); + + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.InvalidValidatorStatus.selector, ValidatorStatus.PendingRemoved + ) + ); + posValidatorManager.submitUptimeProof(validationID, 0); + } + + function testEndValidationPoAValidator() public { + bytes32 validationID = sha256(abi.encodePacked(DEFAULT_L1_ID, uint32(1))); + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP); + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit ValidatorRemovalInitialized( + validationID, bytes32(0), DEFAULT_WEIGHT, DEFAULT_COMPLETION_TIMESTAMP + ); + + _initializeEndValidation(validationID, false, address(0)); + + uint256 balanceBefore = _getStakeAssetBalance(address(this)); + + bytes memory l1ValidatorRegistrationMessage = + ValidatorMessages.packL1ValidatorRegistrationMessage(validationID, false); + _mockGetPChainWarpMessage(l1ValidatorRegistrationMessage, true); + + posValidatorManager.completeEndValidation(0); + + assertEq(_getStakeAssetBalance(address(this)), balanceBefore); + } + + function testDelegationToPoAValidator() public { + bytes32 defaultInitialValidationID = sha256(abi.encodePacked(DEFAULT_L1_ID, uint32(1))); + + _beforeSend(_weightToValue(DEFAULT_DELEGATOR_WEIGHT), DEFAULT_DELEGATOR_ADDRESS); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.ValidatorNotPoS.selector, defaultInitialValidationID + ) + ); + + _initializeDelegatorRegistration( + defaultInitialValidationID, DEFAULT_DELEGATOR_ADDRESS, DEFAULT_DELEGATOR_WEIGHT + ); + } +/* + function testDelegationOverWeightLimit() public { + bytes32 validationID = _registerDefaultValidator(); + + uint64 delegatorWeight = DEFAULT_WEIGHT * DEFAULT_MAXIMUM_STAKE_MULTIPLIER + 1; + + _beforeSend(_weightToValue(delegatorWeight), DEFAULT_DELEGATOR_ADDRESS); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.MaxWeightExceeded.selector, delegatorWeight + DEFAULT_WEIGHT + ) + ); + + _initializeDelegatorRegistration(validationID, DEFAULT_DELEGATOR_ADDRESS, delegatorWeight); + } + */ + + function testCompleteDelegatorRegistrationAlreadyRegistered() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidDelegatorStatus.selector, DelegatorStatus.Active + ) + ); + + posValidatorManager.completeDelegatorRegistration(delegationID, 0); + } + + function testCompleteDelegatorRegistrationWrongValidationID() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _initializeDefaultDelegatorRegistration(validationID); + + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + delegationID, 2, DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT + ); + _mockGetPChainWarpMessage(setValidatorWeightPayload, true); + + vm.expectRevert( + abi.encodeWithSelector(ValidatorManager.InvalidValidationID.selector, validationID) + ); + + vm.warp(DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP); + posValidatorManager.completeDelegatorRegistration(delegationID, 0); + } + + function testCompleteEndDelegationWrongValidationID() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: address(this) + }); + + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(delegationID, 2, DEFAULT_WEIGHT); + _mockGetPChainWarpMessage(setValidatorWeightPayload, true); + + vm.expectRevert( + abi.encodeWithSelector(ValidatorManager.InvalidValidationID.selector, delegationID) + ); + + posValidatorManager.completeEndDelegation(delegationID, 0); + } + + function testInitializeEndDelegationNotRegistered() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _initializeDefaultDelegatorRegistration(validationID); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidDelegatorStatus.selector, DelegatorStatus.PendingAdded + ) + ); + + posValidatorManager.initializeEndDelegation(delegationID, true, 0); + } + + function testInitializeEndDelegationWrongSender() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.UnauthorizedOwner.selector, address(123)) + ); + + vm.prank(address(123)); + posValidatorManager.initializeEndDelegation(delegationID, true, 0); + } + + function testCompleteDelegatorRegistrationForDelegatorRegisteredWhileValidatorPendingRemoved() + public + { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _initializeDefaultDelegatorRegistration(validationID); + + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 2, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + _setUpCompleteDelegatorRegistrationWithChecks( + validationID, delegationID, DEFAULT_COMPLETION_TIMESTAMP + 1, 0, 2 + ); + + uint256 expectedReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_COMPLETION_TIMESTAMP, + uptimeSeconds: DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + + _completeEndValidationWithChecks({ + validationID: validationID, + validatorOwner: address(this), + expectedReward: expectedReward, + validatorWeight: DEFAULT_WEIGHT, + rewardRecipient: address(this) + }); + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP + 1 + DEFAULT_MINIMUM_STAKE_DURATION); + _expectStakeUnlock(DEFAULT_DELEGATOR_ADDRESS, DEFAULT_DELEGATOR_TOKEN_ID); + posValidatorManager.initializeEndDelegation(delegationID, true, 0); + } + + function testCompleteEndDelegationWhileActive() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidDelegatorStatus.selector, DelegatorStatus.Active + ) + ); + + posValidatorManager.completeEndDelegation(delegationID, 0); + } + + function testCompleteDelegatorRegistrationValidatorPendingRemoved() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _initializeDefaultDelegatorRegistration(validationID); + + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 2, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + _setUpCompleteDelegatorRegistrationWithChecks( + validationID, delegationID, DEFAULT_COMPLETION_TIMESTAMP + 1, 0, 2 + ); + + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.InvalidValidatorStatus.selector, ValidatorStatus.PendingRemoved + ) + ); + + posValidatorManager.initializeEndDelegation(delegationID, false, 0); + } + + function testResendEndDelegationWhileActive() public { + bytes32 validationID = _registerDefaultValidator(); + bytes32 delegationID = _registerDefaultDelegator(validationID); + + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidDelegatorStatus.selector, DelegatorStatus.Active + ) + ); + + posValidatorManager.resendUpdateDelegation(delegationID); + } + + function testForceInitializeEndValidation() public { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: true + }); + } + + function testForceInitializeEndValidationInsufficientUptime() public { + bytes32 validationID = _registerDefaultValidator(); + uint64 uptimePercentage = 79; + + vm.warp(DEFAULT_COMPLETION_TIMESTAMP); + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + bytes memory uptimeMsg = ValidatorMessages.packValidationUptimeMessage( + validationID, + ((DEFAULT_COMPLETION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP) * uptimePercentage) + / 100 + ); + _mockGetUptimeWarpMessage(uptimeMsg, true); + + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit ValidatorRemovalInitialized( + validationID, bytes32(0), DEFAULT_WEIGHT, DEFAULT_COMPLETION_TIMESTAMP + ); + + _forceInitializeEndValidation(validationID, true, address(0)); + } +/* + function testValueToWeightTruncated() public { + // default weightToValueFactor is 1e12 + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.InvalidStakeAmount.selector, 0.1) + ); + posValidatorManager.valueToWeight(0.1); + } + */ + + function testValueToWeightExceedsUInt64Max() public { + // default weightToValueFactor is 1e12 + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.InvalidStakeAmount.selector, 1e40) + ); + posValidatorManager.valueToWeight(1e40); + } + + function testValueToWeight() public view { + uint64 w1 = posValidatorManager.valueToWeight(1); + uint64 w2 = posValidatorManager.valueToWeight(1e6); + uint64 w3 = posValidatorManager.valueToWeight(1e15); + + assertEq(w1, 1); + assertEq(w2, 1e6); + assertEq(w3, 1e15); + } + + function testWeightToValue() public view { + uint256 v1 = posValidatorManager.weightToValue(1); + uint256 v2 = posValidatorManager.weightToValue(1e6); + uint256 v3 = posValidatorManager.weightToValue(1e15); + + assertEq(v1, 1); + assertEq(v2, 1e6); + assertEq(v3, 1e15); + } + + function testPoSValidatorManagerStorageSlot() public view { + assertEq( + _erc7201StorageSlot("PoSValidatorManager"), + posValidatorManager.POS_VALIDATOR_MANAGER_STORAGE_LOCATION() + ); + } + + function _initializeValidatorRegistration( + ValidatorRegistrationInput memory registrationInput, + uint16 delegationFeeBips, + uint64 minStakeDuration, + uint256 stakeAmount + ) internal virtual returns (bytes32); + + function _initializeEndValidation( + bytes32 validationID, + bool includeUptime, + address recipientAddress + ) internal virtual override { + posValidatorManager.initializeEndValidation( + validationID, includeUptime, 0, recipientAddress + ); + } + + function _forceInitializeEndValidation( + bytes32 validationID, + bool includeUptime, + address recipientAddress + ) internal virtual override { + posValidatorManager.forceInitializeEndValidation( + validationID, includeUptime, 0, recipientAddress + ); + } + + function _initializeDelegatorRegistration( + bytes32 validationID, + address delegatorAddress, + uint64 weight + ) internal virtual returns (bytes32); + + // + // Delegation setup utilities + // + function _setUpInitializeDelegatorRegistration( + bytes32 validationID, + address delegatorAddress, + uint64 weight, + uint64 registrationTimestamp, + uint64 expectedValidatorWeight, + uint64 expectedNonce + ) internal returns (bytes32) { + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, expectedNonce, expectedValidatorWeight + ); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + vm.warp(registrationTimestamp); + _beforeSend(weight, delegatorAddress); + + vm.expectEmit(true, true, true, true, address(posValidatorManager)); + emit DelegatorAdded({ + delegationID: keccak256(abi.encodePacked(validationID, expectedNonce)), + validationID: validationID, + delegatorAddress: delegatorAddress, + nonce: expectedNonce, + validatorWeight: expectedValidatorWeight, + delegatorWeight: 1, + setWeightMessageID: bytes32(0) + }); + return _initializeDelegatorRegistration(validationID, delegatorAddress, weight); + } + + function _setUpCompleteDelegatorRegistration( + bytes32 delegationID, + uint64 completeRegistrationTimestamp, + bytes memory setValidatorWeightPayload + ) internal { + _mockGetPChainWarpMessage(setValidatorWeightPayload, true); + + vm.warp(completeRegistrationTimestamp); + posValidatorManager.completeDelegatorRegistration(delegationID, 0); + } + + function _setUpCompleteDelegatorRegistrationWithChecks( + bytes32 validationID, + bytes32 delegationID, + uint64 completeRegistrationTimestamp, + uint64 expectedValidatorWeight, + uint64 expectedNonce + ) internal { + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, expectedNonce, expectedValidatorWeight + ); + vm.expectEmit(true, true, true, true, address(posValidatorManager)); + emit DelegatorRegistered({ + delegationID: delegationID, + validationID: validationID, + startTime: completeRegistrationTimestamp + }); + _setUpCompleteDelegatorRegistration( + delegationID, completeRegistrationTimestamp, setValidatorWeightPayload + ); + } + + function _registerDefaultDelegator(bytes32 validationID) + internal + returns (bytes32 delegationID) + { + return _registerDelegator({ + validationID: validationID, + delegatorAddress: DEFAULT_DELEGATOR_ADDRESS, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + initRegistrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + completeRegistrationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + } + + function _initializeDefaultDelegatorRegistration(bytes32 validationID) + internal + returns (bytes32) + { + return _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: DEFAULT_DELEGATOR_ADDRESS, + weight: DEFAULT_DELEGATOR_TOKEN_ID, + registrationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT, + expectedNonce: 1 + }); + } + + function _completeDefaultDelegatorRegistration( + bytes32 validationID, + bytes32 delegationID + ) internal { + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, 1, DEFAULT_DELEGATOR_WEIGHT + DEFAULT_WEIGHT + ); + _setUpCompleteDelegatorRegistration({ + delegationID: delegationID, + completeRegistrationTimestamp: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + setValidatorWeightPayload: setValidatorWeightPayload + }); + } + + function _completeDefaultDelegator(bytes32 validationID, bytes32 delegationID) internal { + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: address(this) + }); + + uint256 expectedTotalReward = _defaultDelegatorExpectedTotalReward(); + uint256 expectedValidatorFees = + _calculateValidatorFeesFromDelegator(expectedTotalReward, DEFAULT_DELEGATION_FEE_BIPS); + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + _completeEndDelegationWithChecks({ + validationID: validationID, + delegationID: delegationID, + delegator: DEFAULT_DELEGATOR_ADDRESS, + delegatorWeight: DEFAULT_DELEGATOR_TOKEN_ID, + expectedValidatorFees: expectedValidatorFees, + expectedDelegatorReward: expectedDelegatorReward, + validatorWeight: DEFAULT_WEIGHT, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + rewardRecipient: address(this) + }); + } + + function _registerDelegator( + bytes32 validationID, + address delegatorAddress, + uint64 weight, + uint64 initRegistrationTimestamp, + uint64 completeRegistrationTimestamp, + uint64 expectedValidatorWeight, + uint64 expectedNonce + ) internal returns (bytes32) { + bytes32 delegationID = _setUpInitializeDelegatorRegistration({ + validationID: validationID, + delegatorAddress: delegatorAddress, + weight: weight, + registrationTimestamp: initRegistrationTimestamp, + expectedValidatorWeight: expectedValidatorWeight, + expectedNonce: expectedNonce + }); + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, expectedNonce, expectedValidatorWeight + ); + + _setUpCompleteDelegatorRegistration( + delegationID, completeRegistrationTimestamp, setValidatorWeightPayload + ); + return delegationID; + } + + function _initializeEndDelegationValidatorActiveWithChecks( + bytes32 validationID, + address sender, + bytes32 delegationID, + uint64 startDelegationTimestamp, + uint64 endDelegationTimestamp, + uint64 expectedValidatorWeight, + uint64 expectedNonce, + bool includeUptime, + bool force, + address rewardRecipient + ) internal { + bytes memory setValidatorWeightPayload = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, expectedNonce, expectedValidatorWeight + ); + bytes memory uptimeMsg = ValidatorMessages.packValidationUptimeMessage( + validationID, endDelegationTimestamp - startDelegationTimestamp + ); + vm.expectEmit(true, true, true, true, address(posValidatorManager)); + emit ValidatorWeightUpdate({ + validationID: validationID, + nonce: expectedNonce, + weight: expectedValidatorWeight, + setWeightMessageID: bytes32(0) + }); + + vm.expectEmit(true, true, true, true, address(posValidatorManager)); + emit DelegatorRemovalInitialized({delegationID: delegationID, validationID: validationID}); + + _initializeEndDelegationValidatorActive({ + sender: sender, + delegationID: delegationID, + endDelegationTimestamp: endDelegationTimestamp, + includeUptime: includeUptime, + force: force, + setValidatorWeightPayload: setValidatorWeightPayload, + uptimePayload: uptimeMsg, + rewardRecipient: rewardRecipient + }); + } + + function _initializeEndDelegationValidatorActive( + address sender, + bytes32 delegationID, + uint64 endDelegationTimestamp, + bool includeUptime, + bool force, + bytes memory setValidatorWeightPayload, + bytes memory uptimePayload, + address rewardRecipient + ) internal { + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + + if (includeUptime) { + _mockGetUptimeWarpMessage(uptimePayload, true); + } + _initializeEndDelegation({ + sender: sender, + delegationID: delegationID, + endDelegationTimestamp: endDelegationTimestamp, + includeUptime: includeUptime, + force: force, + rewardRecipient: rewardRecipient + }); + } + + function _initializeEndDelegation( + address sender, + bytes32 delegationID, + uint64 endDelegationTimestamp, + bool includeUptime, + bool force, + address rewardRecipient + ) internal { + vm.warp(endDelegationTimestamp); + vm.prank(sender); + if (force) { + posValidatorManager.forceInitializeEndDelegation( + delegationID, includeUptime, 0, rewardRecipient + ); + } else { + posValidatorManager.initializeEndDelegation( + delegationID, includeUptime, 0, rewardRecipient + ); + } + } + + function _endDefaultValidatorWithChecks(bytes32 validationID, uint64 expectedNonce) internal { + _endValidationWithChecks({ + validationID: validationID, + validatorOwner: address(this), + completeRegistrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + validatorWeight: DEFAULT_WEIGHT, + expectedNonce: expectedNonce, + rewardRecipient: address(this) + }); + } + + function _endDefaultValidator(bytes32 validationID, uint64 expectedNonce) internal { + address validatorOwner = address(this); + _endValidationWithChecks({ + validationID: validationID, + validatorOwner: validatorOwner, + completeRegistrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + validatorWeight: DEFAULT_WEIGHT, + expectedNonce: expectedNonce, + rewardRecipient: validatorOwner + }); + } + + function _endValidationWithChecks( + bytes32 validationID, + address validatorOwner, + uint64 completeRegistrationTimestamp, + uint64 completionTimestamp, + uint64 validatorWeight, + uint64 expectedNonce, + address rewardRecipient + ) internal { + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, expectedNonce, 0); + bytes memory uptimeMessage = ValidatorMessages.packValidationUptimeMessage( + validationID, completionTimestamp - completeRegistrationTimestamp + ); + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: completionTimestamp, + setWeightMessage: setWeightMessage, + includeUptime: true, + uptimeMessage: uptimeMessage, + force: false + }); + + uint256 expectedReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(validatorWeight), + validatorStartTime: completeRegistrationTimestamp, + stakingStartTime: completeRegistrationTimestamp, + stakingEndTime: completionTimestamp, + uptimeSeconds: completionTimestamp - completeRegistrationTimestamp + }); + + _completeEndValidationWithChecks({ + validationID: validationID, + validatorOwner: validatorOwner, + expectedReward: expectedReward, + validatorWeight: validatorWeight, + rewardRecipient: rewardRecipient + }); + } + + function _completeEndValidationWithChecks( + bytes32 validationID, + address validatorOwner, + uint256 expectedReward, + uint64 validatorWeight, + address rewardRecipient + ) internal { + bytes memory l1ValidatorRegistrationMessage = + ValidatorMessages.packL1ValidatorRegistrationMessage(validationID, false); + + vm.expectEmit(true, true, true, true, address(posValidatorManager)); + emit ValidationPeriodEnded(validationID, ValidatorStatus.Completed); + uint256 balanceBefore = _getStakeAssetBalance(validatorOwner); + uint256 rewardRecipientBalanceBefore = _getRewardAssetBalance(rewardRecipient); + + _expectStakeUnlock(validatorOwner, _weightToValue(validatorWeight)); + _expectRewardIssuance(rewardRecipient, expectedReward); + + _completeEndValidation(l1ValidatorRegistrationMessage); + + if (rewardRecipient == validatorOwner) { + assertEq( + _getStakeAssetBalance(validatorOwner), + balanceBefore + _weightToValue(validatorWeight) + ); + assertEq( + _getRewardAssetBalance(rewardRecipient), + rewardRecipientBalanceBefore + expectedReward + ); + } else { + assertEq( + _getStakeAssetBalance(validatorOwner), + balanceBefore + _weightToValue(validatorWeight) + ); + assertEq( + _getRewardAssetBalance(rewardRecipient), + rewardRecipientBalanceBefore + expectedReward + ); + } + } + + function _completeEndValidation(bytes memory l1ValidatorRegistrationMessage) internal { + _mockGetPChainWarpMessage(l1ValidatorRegistrationMessage, true); + posValidatorManager.completeEndValidation(0); + } + + function _completeEndDelegationWithChecks( + bytes32 validationID, + bytes32 delegationID, + address delegator, + uint64 delegatorWeight, + uint256 expectedValidatorFees, + uint256 expectedDelegatorReward, + uint64 validatorWeight, + uint64 expectedValidatorWeight, + uint64 expectedNonce, + address rewardRecipient + ) internal { + bytes memory weightUpdateMessage = ValidatorMessages.packL1ValidatorWeightMessage( + validationID, expectedNonce, validatorWeight + ); + + vm.expectEmit(true, true, true, true, address(posValidatorManager)); + emit DelegationEnded( + delegationID, validationID, expectedDelegatorReward, expectedValidatorFees + ); + uint256 balanceBefore = _getStakeAssetBalance(delegator); + uint256 rewardRecipientBalanceBefore = _getRewardAssetBalance(rewardRecipient); + + _expectStakeUnlock(delegator, _weightToValue(delegatorWeight)); + _expectRewardIssuance(rewardRecipient, expectedDelegatorReward); + + _completeEndDelegation(delegationID, weightUpdateMessage); + + assertEq(posValidatorManager.getValidator(validationID).weight, expectedValidatorWeight); + + if (rewardRecipient == delegator) { + assertEq( + _getStakeAssetBalance(delegator), + balanceBefore + DEFAULT_DELEGATOR_WEIGHT + ); + assertEq( + _getRewardAssetBalance(rewardRecipient), + rewardRecipientBalanceBefore + expectedDelegatorReward + ); + } else { + assertEq( + _getStakeAssetBalance(delegator), balanceBefore + DEFAULT_DELEGATOR_WEIGHT + ); + + assertEq( + _getRewardAssetBalance(rewardRecipient), + rewardRecipientBalanceBefore + expectedDelegatorReward + ); + } + } + + function _completeEndDelegation( + bytes32 delegationID, + bytes memory weightUpdateMessage + ) internal { + _mockGetPChainWarpMessage(weightUpdateMessage, true); + posValidatorManager.completeEndDelegation(delegationID, 0); + } + + function _initializeAndCompleteEndDelegationWithChecks( + bytes32 validationID, + bytes32 delegationID + ) internal { + _initializeEndDelegationValidatorActiveWithChecks({ + validationID: validationID, + sender: DEFAULT_DELEGATOR_ADDRESS, + delegationID: delegationID, + startDelegationTimestamp: DEFAULT_DELEGATOR_INIT_REGISTRATION_TIMESTAMP, + endDelegationTimestamp: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + includeUptime: true, + force: false, + rewardRecipient: address(0) + }); + + uint256 expectedTotalReward = rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_DELEGATOR_WEIGHT), + validatorStartTime: 0, + stakingStartTime: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + uptimeSeconds: 0 + }); + + uint256 expectedValidatorFees = + _calculateValidatorFeesFromDelegator(expectedTotalReward, DEFAULT_DELEGATION_FEE_BIPS); + uint256 expectedDelegatorReward = expectedTotalReward - expectedValidatorFees; + address delegator = DEFAULT_DELEGATOR_ADDRESS; + + _completeEndDelegationWithChecks({ + validationID: validationID, + delegationID: delegationID, + delegator: delegator, + delegatorWeight: DEFAULT_DELEGATOR_WEIGHT, + expectedValidatorFees: expectedValidatorFees, + expectedDelegatorReward: expectedDelegatorReward, + validatorWeight: DEFAULT_WEIGHT, + expectedValidatorWeight: DEFAULT_WEIGHT, + expectedNonce: 2, + rewardRecipient: delegator + }); + } + + function _getStakeAssetBalance(address account) internal virtual returns (uint256); + function _getRewardAssetBalance(address account) internal virtual returns (uint256); + function _expectStakeUnlock(address account, uint256 amount) internal virtual; + function _expectRewardIssuance(address account, uint256 amount) internal virtual; + + function _defaultDelegatorExpectedTotalReward() internal view returns (uint256) { + return rewardCalculator.calculateReward({ + stakeAmount: _weightToValue(DEFAULT_DELEGATOR_WEIGHT), + validatorStartTime: DEFAULT_REGISTRATION_TIMESTAMP, + stakingStartTime: DEFAULT_DELEGATOR_COMPLETE_REGISTRATION_TIMESTAMP, + stakingEndTime: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP, + uptimeSeconds: DEFAULT_DELEGATOR_END_DELEGATION_TIMESTAMP - DEFAULT_REGISTRATION_TIMESTAMP + }); + } + + function _defaultPoSSettings() internal pure returns (PoSValidatorManagerSettings memory) { + return PoSValidatorManagerSettings({ + baseSettings: ValidatorManagerSettings({ + l1ID: DEFAULT_L1_ID, + churnPeriodSeconds: DEFAULT_CHURN_PERIOD, + maximumChurnPercentage: DEFAULT_MAXIMUM_CHURN_PERCENTAGE + }), + minimumStakeAmount: DEFAULT_MINIMUM_STAKE_AMOUNT, + maximumStakeAmount: DEFAULT_MAXIMUM_STAKE_AMOUNT, + minimumStakeDuration: DEFAULT_MINIMUM_STAKE_DURATION, + minimumDelegationFeeBips: DEFAULT_MINIMUM_DELEGATION_FEE_BIPS, + maximumStakeMultiplier: DEFAULT_MAXIMUM_STAKE_MULTIPLIER, + weightToValueFactor: DEFAULT_WEIGHT_TO_VALUE_FACTOR, + rewardCalculator: IRewardCalculator(address(0)), + uptimeBlockchainID: DEFAULT_SOURCE_BLOCKCHAIN_ID + }); + } + + function _calculateValidatorFeesFromDelegator( + uint256 totalReward, + uint64 delegationFeeBips + ) internal pure returns (uint256) { + return totalReward * delegationFeeBips / 10000; + } +} diff --git a/contracts/validator-manager/tests/ERC721/ERC721TokenStakingManagerTests.t.sol b/contracts/validator-manager/tests/ERC721/ERC721TokenStakingManagerTests.t.sol new file mode 100644 index 000000000..fa4bddc37 --- /dev/null +++ b/contracts/validator-manager/tests/ERC721/ERC721TokenStakingManagerTests.t.sol @@ -0,0 +1,287 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: Ecosystem + +pragma solidity 0.8.25; + +import {ERC721PoSValidatorManagerTest} from "./ERC721PoSValidatorManagerTests.t.sol"; +import {ERC721TokenStakingManager} from "../../ERC721TokenStakingManager.sol"; +import {PoSValidatorManager, PoSValidatorManagerSettings} from "../../PoSValidatorManager.sol"; +import {ExampleRewardCalculator} from "../../ExampleRewardCalculator.sol"; +import {ValidatorRegistrationInput, IValidatorManager} from "../../interfaces/IValidatorManager.sol"; +import {ICMInitializable} from "../../../utilities/ICMInitializable.sol"; +import {ExampleERC721} from "@mocks/ExampleERC721.sol"; +import {ExampleERC20} from "@mocks/ExampleERC20.sol"; +import {IERC721} from "@openzeppelin/contracts@5.0.2/token/ERC721/IERC721.sol"; +import {IERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts@5.0.2/token/ERC20/utils/SafeERC20.sol"; +import {Initializable} from "@openzeppelin/contracts@5.0.2/proxy/utils/Initializable.sol"; +import {ERC721ValidatorManagerTest} from "./ERC721ValidatorManagerTests.t.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +contract ERC721TokenStakingManagerTest is ERC721PoSValidatorManagerTest, IERC721Receiver { + using SafeERC20 for IERC20; + + ERC721TokenStakingManager public app; + IERC721 public stakingToken; + IERC20 public rewardToken; + uint256 public constant TEST_TOKEN_ID = 1; + + function setUp() public override { + ERC721ValidatorManagerTest.setUp(); + _setUp(); + _mockGetBlockchainID(); + _mockInitializeValidatorSet(); + + app.initializeValidatorSet(_defaultConversionData(), 0); + } + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public virtual returns (bytes4) { + return this.onERC721Received.selector; + } + + function testDisableInitialization() public { + app = new ERC721TokenStakingManager(ICMInitializable.Disallowed); + vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector)); + app.initialize(_defaultPoSSettings(), stakingToken, rewardToken); + } + + function testZeroStakingTokenAddress() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + vm.expectRevert( + abi.encodeWithSelector( + ERC721TokenStakingManager.InvalidTokenAddress.selector, address(0) + ) + ); + app.initialize(_defaultPoSSettings(), IERC721(address(0)), rewardToken); + } + + function testZeroRewardTokenAddress() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + vm.expectRevert( + abi.encodeWithSelector( + ERC721TokenStakingManager.InvalidRewardTokenAddress.selector, address(0) + ) + ); + app.initialize(_defaultPoSSettings(), stakingToken, IERC20(address(0))); + } + + function testZeroMinimumDelegationFee() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.InvalidDelegationFee.selector, 0) + ); + + PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); + defaultPoSSettings.minimumDelegationFeeBips = 0; + app.initialize(defaultPoSSettings, stakingToken, rewardToken); + } + + function testMaxMinimumDelegationFee() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + uint16 minimumDelegationFeeBips = app.MAXIMUM_DELEGATION_FEE_BIPS() + 1; + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidDelegationFee.selector, minimumDelegationFeeBips + ) + ); + + PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); + defaultPoSSettings.minimumDelegationFeeBips = minimumDelegationFeeBips; + app.initialize(defaultPoSSettings, stakingToken, rewardToken); + } + + function testInvalidStakeAmountRange() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidStakeAmount.selector, DEFAULT_MAXIMUM_STAKE_AMOUNT + ) + ); + + PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); + defaultPoSSettings.minimumStakeAmount = DEFAULT_MAXIMUM_STAKE_AMOUNT; + defaultPoSSettings.maximumStakeAmount = DEFAULT_MINIMUM_STAKE_AMOUNT; + app.initialize(defaultPoSSettings, stakingToken, rewardToken); + } + + function testZeroMaxStakeMultiplier() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.InvalidStakeMultiplier.selector, 0) + ); + + PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); + defaultPoSSettings.maximumStakeMultiplier = 0; + app.initialize(defaultPoSSettings, stakingToken, rewardToken); + } + + function testMinStakeDurationTooLow() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + uint64 minimumStakeDuration = DEFAULT_CHURN_PERIOD - 1; + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidMinStakeDuration.selector, minimumStakeDuration + ) + ); + + PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); + defaultPoSSettings.minimumStakeDuration = minimumStakeDuration; + app.initialize(defaultPoSSettings, stakingToken, rewardToken); + } + + function testMaxStakeMultiplierOverLimit() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + uint8 maximumStakeMultiplier = app.MAXIMUM_STAKE_MULTIPLIER_LIMIT() + 1; + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidStakeMultiplier.selector, maximumStakeMultiplier + ) + ); + + PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); + defaultPoSSettings.maximumStakeMultiplier = maximumStakeMultiplier; + app.initialize(defaultPoSSettings, stakingToken, rewardToken); + } + + function testZeroWeightToValueFactor() public { + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + vm.expectRevert( + abi.encodeWithSelector(PoSValidatorManager.ZeroWeightToValueFactor.selector) + ); + + PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); + defaultPoSSettings.weightToValueFactor = 0; + app.initialize(defaultPoSSettings, stakingToken, rewardToken); + } + + function testInvalidValidatorMinStakeDuration() public { + ValidatorRegistrationInput memory input = ValidatorRegistrationInput({ + nodeID: DEFAULT_NODE_ID, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + registrationExpiry: DEFAULT_EXPIRY, + remainingBalanceOwner: DEFAULT_P_CHAIN_OWNER, + disableOwner: DEFAULT_P_CHAIN_OWNER + }); + vm.expectRevert( + abi.encodeWithSelector( + PoSValidatorManager.InvalidMinStakeDuration.selector, + DEFAULT_MINIMUM_STAKE_DURATION - 1 + ) + ); + app.initializeValidatorRegistration( + input, DEFAULT_DELEGATION_FEE_BIPS, DEFAULT_MINIMUM_STAKE_DURATION - 1, TEST_TOKEN_ID + ); + } + + function testERC721TokenStakingManagerStorageSlot() public view { + assertEq( + _erc7201StorageSlot("ERC721TokenStakingManager"), + app.ERC721_STAKING_MANAGER_STORAGE_LOCATION() + ); + } + + function _initializeValidatorRegistration( + ValidatorRegistrationInput memory registrationInput, + uint16 delegationFeeBips, + uint64 minStakeDuration, + uint256 tokenId + ) internal virtual override returns (bytes32) { + return app.initializeValidatorRegistration( + registrationInput, delegationFeeBips, minStakeDuration, tokenId + ); + } + + function _initializeValidatorRegistration( + ValidatorRegistrationInput memory input, + uint64 weight + ) internal virtual override returns (bytes32) { + + return app.initializeValidatorRegistration( + input, + DEFAULT_DELEGATION_FEE_BIPS, + DEFAULT_MINIMUM_STAKE_DURATION, + weight + ); + } + + function _initializeDelegatorRegistration( + bytes32 validationID, + address delegatorAddress, + uint64 weight + ) internal virtual override returns (bytes32) { + uint256 value = _weightToValue(weight); + + + vm.startPrank(delegatorAddress); + //stakingToken.approve(address(app), TEST_TOKEN_ID); + bytes32 delegationID = app.initializeDelegatorRegistration(validationID, value); + vm.stopPrank(); + + return delegationID; + } + + function _beforeSend(uint256 tokenId, address spender) internal override { + + stakingToken.approve(spender, tokenId); + ExampleERC721(address(stakingToken)).transferFrom(address(this), spender, tokenId); + + + vm.startPrank(spender); + + stakingToken.approve(address(app), tokenId); + vm.stopPrank(); + } + + function _expectStakeUnlock(address account, uint256 tokenId) internal override { + vm.expectCall( + address(stakingToken), + abi.encodeWithSelector( + 0x42842e0e, //safeTransferFrom(address,address,uint256) + address(app), + account, + tokenId + ) + ); + } + + function _expectRewardIssuance(address account, uint256 amount) internal override { + vm.expectCall( + address(rewardToken), + abi.encodeCall(IERC20.transfer, (account, amount)) + ); + } + + function _setUp() internal override returns (IValidatorManager) { + // Construct the object under test + app = new ERC721TokenStakingManager(ICMInitializable.Allowed); + stakingToken = new ExampleERC721(); + rewardToken = new ExampleERC20(); + rewardToken.transfer(address(app), 100000 ether); + + rewardCalculator = new ExampleRewardCalculator(DEFAULT_REWARD_RATE, 18); + + PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); + defaultPoSSettings.rewardCalculator = rewardCalculator; + app.initialize(defaultPoSSettings, stakingToken, rewardToken); + + validatorManager = app; + posValidatorManager = app; + + return app; + } + + function _getStakeAssetBalance(address account) internal view override returns (uint256) { + return stakingToken.balanceOf(account); + } + + function _getRewardAssetBalance(address account) internal view override returns (uint256) { + return rewardToken.balanceOf(account); + } + +} \ No newline at end of file diff --git a/contracts/validator-manager/tests/ERC721/ERC721ValidatorManagerTests.t.sol b/contracts/validator-manager/tests/ERC721/ERC721ValidatorManagerTests.t.sol new file mode 100644 index 000000000..5ec341d72 --- /dev/null +++ b/contracts/validator-manager/tests/ERC721/ERC721ValidatorManagerTests.t.sol @@ -0,0 +1,716 @@ +// (c) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: Ecosystem + +pragma solidity 0.8.25; + +import {Test} from "@forge-std/Test.sol"; +import {ValidatorManager, ConversionData, InitialValidator} from "../../ValidatorManager.sol"; +import {ValidatorMessages} from "../../ValidatorMessages.sol"; +import { + ValidatorStatus, + ValidatorRegistrationInput, + PChainOwner, + IValidatorManager +} from "../../interfaces/IValidatorManager.sol"; +import { + WarpMessage, + IWarpMessenger +} from "@avalabs/subnet-evm-contracts@1.2.0/contracts/interfaces/IWarpMessenger.sol"; +// TODO: Remove this once all unit tests implemented +// solhint-disable no-empty-blocks +abstract contract ERC721ValidatorManagerTest is Test { + bytes32 public constant DEFAULT_L1_ID = + bytes32(hex"1234567812345678123456781234567812345678123456781234567812345678"); + bytes public constant DEFAULT_NODE_ID = + bytes(hex"1234567812345678123456781234567812345678123456781234567812345678"); + bytes public constant DEFAULT_INITIAL_VALIDATOR_NODE_ID_1 = + bytes(hex"2345678123456781234567812345678123456781234567812345678123456781"); + bytes public constant DEFAULT_INITIAL_VALIDATOR_NODE_ID_2 = + bytes(hex"1345678123456781234567812345678123456781234567812345678123456781"); + bytes public constant DEFAULT_BLS_PUBLIC_KEY = bytes( + hex"123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678" + ); + bytes32 public constant DEFAULT_SOURCE_BLOCKCHAIN_ID = + bytes32(hex"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"); + bytes32 public constant DEFAULT_SUBNET_CONVERSION_ID = + bytes32(hex"f85870b6f0dc2003f57cefc0e302fea0d174668c72ae409204fee73843983be8"); + address public constant WARP_PRECOMPILE_ADDRESS = 0x0200000000000000000000000000000000000005; + + uint64 public constant DEFAULT_WEIGHT = 1; + // Set the default weight to 1e10 to avoid churn issues + uint64 public constant DEFAULT_INITIAL_VALIDATOR_WEIGHT = 10; + uint64 public constant DEFAULT_INITIAL_TOTAL_WEIGHT = + DEFAULT_INITIAL_VALIDATOR_WEIGHT + DEFAULT_WEIGHT; + uint256 public constant DEFAULT_MINIMUM_STAKE_AMOUNT = 1; + uint256 public constant DEFAULT_MAXIMUM_STAKE_AMOUNT = 5; + uint64 public constant DEFAULT_CHURN_PERIOD = 1 hours; + uint8 public constant DEFAULT_MAXIMUM_CHURN_PERCENTAGE = 70; + uint64 public constant DEFAULT_EXPIRY = 1000; + uint8 public constant DEFAULT_MAXIMUM_HOURLY_CHURN = 0; + uint64 public constant DEFAULT_REGISTRATION_TIMESTAMP = 1000; + uint256 public constant DEFAULT_STARTING_TOTAL_WEIGHT = 3; + uint64 public constant DEFAULT_MINIMUM_VALIDATION_DURATION = 24 hours; + uint64 public constant DEFAULT_COMPLETION_TIMESTAMP = 100_000; + // solhint-disable-next-line var-name-mixedcase + PChainOwner public DEFAULT_P_CHAIN_OWNER; + + ValidatorManager public validatorManager; + + // Used to create unique validator IDs in {_newNodeID} + uint64 public nodeIDCounter = 0; + + event ValidationPeriodCreated( + bytes32 indexed validationID, + bytes indexed nodeID, + bytes32 indexed registerValidationMessageID, + uint64 weight, + uint64 registrationExpiry + ); + + event InitialValidatorCreated( + bytes32 indexed validationID, bytes indexed nodeID, uint64 weight + ); + + event ValidationPeriodRegistered( + bytes32 indexed validationID, uint64 weight, uint256 timestamp + ); + + event ValidatorRemovalInitialized( + bytes32 indexed validationID, + bytes32 indexed setWeightMessageID, + uint64 weight, + uint256 endTime + ); + + event ValidationPeriodEnded(bytes32 indexed validationID, ValidatorStatus indexed status); + + receive() external payable {} + fallback() external payable {} + + function setUp() public virtual { + address[] memory addresses = new address[](1); + addresses[0] = 0x1234567812345678123456781234567812345678; + DEFAULT_P_CHAIN_OWNER = PChainOwner({threshold: 1, addresses: addresses}); + } + + function testInitializeValidatorRegistrationSuccess() public { + _setUpInitializeValidatorRegistration( + DEFAULT_NODE_ID, DEFAULT_L1_ID, DEFAULT_WEIGHT, DEFAULT_EXPIRY, DEFAULT_BLS_PUBLIC_KEY + ); + } + + function testInitializeValidatorRegistrationExcessiveChurn() public { + // TODO: implement + } + + function testInitializeValidatorRegistrationInsufficientStake() public { + // TODO: implement + } + + function testInitializeValidatorRegistrationExcessiveStake() public { + // TODO: implement + } + + function testInitializeValidatorRegistrationInsufficientDuration() public { + // TODO: implement + } + + function testInitializeValidatorRegistrationPChainOwnerThresholdTooLarge() public { + // Threshold too large + address[] memory addresses = new address[](1); + addresses[0] = 0x1234567812345678123456781234567812345678; + PChainOwner memory invalidPChainOwner1 = PChainOwner({threshold: 2, addresses: addresses}); + _beforeSend(_weightToValue(DEFAULT_WEIGHT), address(this)); + vm.expectRevert( + abi.encodeWithSelector(ValidatorManager.InvalidPChainOwnerThreshold.selector, 2, 1) + ); + _initializeValidatorRegistration( + ValidatorRegistrationInput({ + nodeID: DEFAULT_NODE_ID, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + remainingBalanceOwner: invalidPChainOwner1, + disableOwner: DEFAULT_P_CHAIN_OWNER, + registrationExpiry: DEFAULT_EXPIRY + }), + DEFAULT_WEIGHT + ); + } + + function testInitializeValidatorRegistrationZeroPChainOwnerThreshold() public { + // Zero threshold for non-zero address + address[] memory addresses = new address[](1); + addresses[0] = 0x1234567812345678123456781234567812345678; + PChainOwner memory invalidPChainOwner1 = PChainOwner({threshold: 0, addresses: addresses}); + _beforeSend(_weightToValue(DEFAULT_WEIGHT), address(this)); + vm.expectRevert( + abi.encodeWithSelector(ValidatorManager.InvalidPChainOwnerThreshold.selector, 0, 1) + ); + _initializeValidatorRegistration( + ValidatorRegistrationInput({ + nodeID: DEFAULT_NODE_ID, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + remainingBalanceOwner: invalidPChainOwner1, + disableOwner: DEFAULT_P_CHAIN_OWNER, + registrationExpiry: DEFAULT_EXPIRY + }), + DEFAULT_WEIGHT + ); + } + + function testInitializeValidatorRegistrationPChainOwnerAddressesUnsorted() public { + // Addresses not sorted + address[] memory addresses = new address[](2); + addresses[0] = 0x1234567812345678123456781234567812345678; + addresses[1] = 0x0123456781234567812345678123456781234567; + PChainOwner memory invalidPChainOwner1 = PChainOwner({threshold: 1, addresses: addresses}); + + _beforeSend(_weightToValue(DEFAULT_WEIGHT), address(this)); + vm.expectRevert( + abi.encodeWithSelector(ValidatorManager.PChainOwnerAddressesNotSorted.selector) + ); + _initializeValidatorRegistration( + ValidatorRegistrationInput({ + nodeID: DEFAULT_NODE_ID, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + remainingBalanceOwner: invalidPChainOwner1, + disableOwner: DEFAULT_P_CHAIN_OWNER, + registrationExpiry: DEFAULT_EXPIRY + }), + DEFAULT_WEIGHT + ); + } + + // The following tests call functions that are implemented in ValidatorManager, but access state that's + // only set in NativeTokenValidatorManager. Therefore we call them via the concrete type, rather than a + // reference to the abstract type. + function testResendRegisterValidatorMessage() public { + bytes32 validationID = _setUpInitializeValidatorRegistration( + DEFAULT_NODE_ID, DEFAULT_L1_ID, DEFAULT_WEIGHT, DEFAULT_EXPIRY, DEFAULT_BLS_PUBLIC_KEY + ); + (, bytes memory registerL1ValidatorMessage) = ValidatorMessages + .packRegisterL1ValidatorMessage( + ValidatorMessages.ValidationPeriod({ + l1ID: DEFAULT_L1_ID, + nodeID: DEFAULT_NODE_ID, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + registrationExpiry: DEFAULT_EXPIRY, + remainingBalanceOwner: DEFAULT_P_CHAIN_OWNER, + disableOwner: DEFAULT_P_CHAIN_OWNER, + weight: DEFAULT_WEIGHT + }) + ); + _mockSendWarpMessage(registerL1ValidatorMessage, bytes32(0)); + validatorManager.resendRegisterValidatorMessage(validationID); + } + + function testCompleteValidatorRegistration() public { + _registerDefaultValidator(); + } + + function testInitializeEndValidation() public virtual { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage; + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: false, + uptimeMessage: uptimeMessage, + force: false + }); + } + + function testResendEndValidation() public virtual { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage; + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: false, + uptimeMessage: uptimeMessage, + force: false + }); + + bytes memory setValidatorWeightPayload = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + _mockSendWarpMessage(setValidatorWeightPayload, bytes32(0)); + validatorManager.resendEndValidatorMessage(validationID); + } + + function testCompleteEndValidation() public virtual { + bytes32 validationID = _registerDefaultValidator(); + bytes memory setWeightMessage = + ValidatorMessages.packL1ValidatorWeightMessage(validationID, 1, 0); + bytes memory uptimeMessage; + _initializeEndValidation({ + validationID: validationID, + completionTimestamp: DEFAULT_COMPLETION_TIMESTAMP, + setWeightMessage: setWeightMessage, + includeUptime: false, + uptimeMessage: uptimeMessage, + force: false + }); + + bytes memory l1ValidatorRegistrationMessage = + ValidatorMessages.packL1ValidatorRegistrationMessage(validationID, false); + + _mockGetPChainWarpMessage(l1ValidatorRegistrationMessage, true); + + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit ValidationPeriodEnded(validationID, ValidatorStatus.Completed); + + validatorManager.completeEndValidation(0); + } + + function testCompleteInvalidatedValidation() public { + bytes32 validationID = _setUpInitializeValidatorRegistration( + DEFAULT_NODE_ID, DEFAULT_L1_ID, DEFAULT_WEIGHT, DEFAULT_EXPIRY, DEFAULT_BLS_PUBLIC_KEY + ); + bytes memory l1ValidatorRegistrationMessage = + ValidatorMessages.packL1ValidatorRegistrationMessage(validationID, false); + + _mockGetPChainWarpMessage(l1ValidatorRegistrationMessage, true); + + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit ValidationPeriodEnded(validationID, ValidatorStatus.Invalidated); + + validatorManager.completeEndValidation(0); + } +/* + function testInitialWeightsTooLow() public { + vm.prank(address(123)); + IValidatorManager manager = _setUp(); + + _mockGetBlockchainID(); + vm.expectRevert(abi.encodeWithSelector(ValidatorManager.InvalidTotalWeight.selector, 4)); + manager.initializeValidatorSet(_defaultConversionDataWeightsTooLow(), 0); + } + + function testRemoveValidatorTotalWeight5() public { + // Use prank here, because otherwise each test will end up with a different contract address, leading to a different subnet conversion hash. + vm.prank(address(123)); + IValidatorManager manager = _setUp(); + + _mockGetBlockchainID(); + _mockGetPChainWarpMessage( + ValidatorMessages.packSubnetToL1ConversionMessage( + bytes32(hex"1d72565851401e05d6351ebf5443d9bdc04953f3233da1345af126e7e4be7464") + ), + true + ); + manager.initializeValidatorSet(_defaultConversionDataTotalWeight5(), 0); + + bytes32 validationID = sha256(abi.encodePacked(DEFAULT_L1_ID, uint32(0))); + vm.expectRevert(abi.encodeWithSelector(ValidatorManager.InvalidTotalWeight.selector, 4)); + _forceInitializeEndValidation(validationID, false, address(0)); + } + */ +/* + function testCumulativeChurnRegistration() public { + uint64 churnThreshold = + uint64(DEFAULT_STARTING_TOTAL_WEIGHT) * DEFAULT_MAXIMUM_CHURN_PERCENTAGE / 100; + // _beforeSend(_weightToValue(churnThreshold), address(this)); + // First registration should succeed + _registerValidator({ + nodeID: _newNodeID(), + l1ID: DEFAULT_L1_ID, + weight: churnThreshold, + registrationExpiry: DEFAULT_EXPIRY, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + registrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP + }); + + + // _beforeSend(DEFAULT_MINIMUM_STAKE_AMOUNT, address(this)); + + // Second call should fail + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.MaxChurnRateExceeded.selector, + churnThreshold + _valueToWeight(DEFAULT_MINIMUM_STAKE_AMOUNT) + ) + ); + _initializeValidatorRegistration( + ValidatorRegistrationInput({ + nodeID: DEFAULT_NODE_ID, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + remainingBalanceOwner: DEFAULT_P_CHAIN_OWNER, + disableOwner: DEFAULT_P_CHAIN_OWNER, + registrationExpiry: DEFAULT_REGISTRATION_TIMESTAMP + 1 + }), + _valueToWeight(DEFAULT_MINIMUM_STAKE_AMOUNT) + ); + } + + function testCumulativeChurnRegistrationAndEndValidation() public { + // Registration should succeed + bytes32 validationID = _registerValidator({ + nodeID: DEFAULT_NODE_ID, + l1ID: DEFAULT_L1_ID, + weight: _valueToWeight(DEFAULT_MINIMUM_STAKE_AMOUNT), + registrationExpiry: DEFAULT_EXPIRY, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + registrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP + }); + + uint64 churnThreshold = + uint64(DEFAULT_STARTING_TOTAL_WEIGHT) * DEFAULT_MAXIMUM_CHURN_PERCENTAGE / 100; + _beforeSend(_weightToValue(churnThreshold), address(this)); + + // Registration should succeed + _registerValidator({ + nodeID: _newNodeID(), + l1ID: DEFAULT_L1_ID, + weight: churnThreshold, + registrationExpiry: DEFAULT_EXPIRY + 25 hours, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + registrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP + 25 hours + }); + + // Second call should fail + // The first registration churn amount is not part of the new churn amount since + // a new churn period has started. + vm.expectRevert( + abi.encodeWithSelector( + ValidatorManager.MaxChurnRateExceeded.selector, + _valueToWeight(DEFAULT_MINIMUM_STAKE_AMOUNT) + churnThreshold + ) + ); + + _initializeEndValidation(validationID, false, address(0)); + } + */ + + function testValidatorManagerStorageSlot() public view { + assertEq( + _erc7201StorageSlot("ValidatorManager"), + validatorManager.VALIDATOR_MANAGER_STORAGE_LOCATION() + ); + } + + function _newNodeID() internal returns (bytes memory) { + nodeIDCounter++; + return abi.encodePacked(sha256(new bytes(nodeIDCounter))); + } + + function _setUpInitializeValidatorRegistration( + bytes memory nodeID, + bytes32 l1ID, + uint64 weight, + uint64 registrationExpiry, + bytes memory blsPublicKey + ) internal returns (bytes32 validationID) { + //ExampleERC721(address(stakingToken)).mint(address(this), 1); + (validationID,) = ValidatorMessages.packRegisterL1ValidatorMessage( + ValidatorMessages.ValidationPeriod({ + nodeID: nodeID, + l1ID: l1ID, + blsPublicKey: blsPublicKey, + registrationExpiry: registrationExpiry, + remainingBalanceOwner: DEFAULT_P_CHAIN_OWNER, + disableOwner: DEFAULT_P_CHAIN_OWNER, + weight: DEFAULT_WEIGHT + }) + ); + (, bytes memory registerL1ValidatorMessage) = ValidatorMessages + .packRegisterL1ValidatorMessage( + ValidatorMessages.ValidationPeriod({ + l1ID: l1ID, + nodeID: nodeID, + blsPublicKey: blsPublicKey, + registrationExpiry: registrationExpiry, + remainingBalanceOwner: DEFAULT_P_CHAIN_OWNER, + disableOwner: DEFAULT_P_CHAIN_OWNER, + weight: DEFAULT_WEIGHT + }) + ); + vm.warp(registrationExpiry - 1); + _mockSendWarpMessage(registerL1ValidatorMessage, bytes32(0)); + _beforeSend(_weightToValue(weight), address(this)); + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit ValidationPeriodCreated(validationID, nodeID, bytes32(0), DEFAULT_WEIGHT, registrationExpiry); + + _initializeValidatorRegistration( + ValidatorRegistrationInput({ + nodeID: nodeID, + blsPublicKey: blsPublicKey, + remainingBalanceOwner: DEFAULT_P_CHAIN_OWNER, + disableOwner: DEFAULT_P_CHAIN_OWNER, + registrationExpiry: registrationExpiry + }), + weight + ); + } + + function _registerValidator( + bytes memory nodeID, + bytes32 l1ID, + uint64 weight, + uint64 registrationExpiry, + bytes memory blsPublicKey, + uint64 registrationTimestamp + ) internal returns (bytes32 validationID) { + validationID = _setUpInitializeValidatorRegistration( + nodeID, l1ID, weight, registrationExpiry, blsPublicKey + ); + bytes memory l1ValidatorRegistrationMessage = + ValidatorMessages.packL1ValidatorRegistrationMessage(validationID, true); + + _mockGetPChainWarpMessage(l1ValidatorRegistrationMessage, true); + + vm.warp(registrationTimestamp); + vm.expectEmit(true, true, true, true, address(validatorManager)); + emit ValidationPeriodRegistered(validationID, DEFAULT_WEIGHT, registrationTimestamp); + + validatorManager.completeValidatorRegistration(0); + } + + function _initializeEndValidation( + bytes32 validationID, + uint64 completionTimestamp, + bytes memory setWeightMessage, + bool includeUptime, + bytes memory uptimeMessage, + bool force + ) internal { + _mockSendWarpMessage(setWeightMessage, bytes32(0)); + if (includeUptime) { + _mockGetUptimeWarpMessage(uptimeMessage, true); + } + + vm.warp(completionTimestamp); + if (force) { + _forceInitializeEndValidation(validationID, includeUptime, address(0)); + } else { + _initializeEndValidation(validationID, includeUptime, address(0)); + } + } + + function _initializeEndValidation( + bytes32 validationID, + uint64 completionTimestamp, + bytes memory setWeightMessage, + bool includeUptime, + bytes memory uptimeMessage, + bool force, + address recipientAddress + ) internal { + _mockSendWarpMessage(setWeightMessage, bytes32(0)); + if (includeUptime) { + _mockGetUptimeWarpMessage(uptimeMessage, true); + } + + vm.warp(completionTimestamp); + if (force) { + _forceInitializeEndValidation(validationID, includeUptime, recipientAddress); + } else { + _initializeEndValidation(validationID, includeUptime, recipientAddress); + } + } + + function _registerDefaultValidator() internal returns (bytes32 validationID) { + return _registerValidator({ + nodeID: DEFAULT_NODE_ID, + l1ID: DEFAULT_L1_ID, + weight: DEFAULT_WEIGHT, + registrationExpiry: DEFAULT_EXPIRY, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY, + registrationTimestamp: DEFAULT_REGISTRATION_TIMESTAMP + }); + } + + function _mockSendWarpMessage(bytes memory payload, bytes32 expectedMessageID) internal { + vm.mockCall( + WARP_PRECOMPILE_ADDRESS, + abi.encode(IWarpMessenger.sendWarpMessage.selector), + abi.encode(expectedMessageID) + ); + vm.expectCall( + WARP_PRECOMPILE_ADDRESS, abi.encodeCall(IWarpMessenger.sendWarpMessage, payload) + ); + } + + function _mockGetPChainWarpMessage(bytes memory expectedPayload, bool valid) internal { + vm.mockCall( + WARP_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IWarpMessenger.getVerifiedWarpMessage.selector, uint32(0)), + abi.encode( + WarpMessage({ + sourceChainID: validatorManager.P_CHAIN_BLOCKCHAIN_ID(), + originSenderAddress: address(0), + payload: expectedPayload + }), + valid + ) + ); + vm.expectCall( + WARP_PRECOMPILE_ADDRESS, abi.encodeCall(IWarpMessenger.getVerifiedWarpMessage, 0) + ); + } + + function _mockGetUptimeWarpMessage(bytes memory expectedPayload, bool valid) internal { + vm.mockCall( + WARP_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IWarpMessenger.getVerifiedWarpMessage.selector, uint32(0)), + abi.encode( + WarpMessage({ + sourceChainID: DEFAULT_SOURCE_BLOCKCHAIN_ID, + originSenderAddress: address(0), + payload: expectedPayload + }), + valid + ) + ); + vm.expectCall( + WARP_PRECOMPILE_ADDRESS, abi.encodeCall(IWarpMessenger.getVerifiedWarpMessage, 0) + ); + } + + function _mockGetBlockchainID() internal { + _mockGetBlockchainID(DEFAULT_SOURCE_BLOCKCHAIN_ID); + } + + function _mockGetBlockchainID(bytes32 blockchainID) internal { + vm.mockCall( + WARP_PRECOMPILE_ADDRESS, + abi.encodeWithSelector(IWarpMessenger.getBlockchainID.selector), + abi.encode(blockchainID) + ); + vm.expectCall( + WARP_PRECOMPILE_ADDRESS, abi.encodeWithSelector(IWarpMessenger.getBlockchainID.selector) + ); + } + + function _mockInitializeValidatorSet() internal { + _mockGetPChainWarpMessage( + ValidatorMessages.packSubnetToL1ConversionMessage(DEFAULT_SUBNET_CONVERSION_ID), true + ); + } + + function _initializeValidatorRegistration( + ValidatorRegistrationInput memory input, + uint64 weight + ) internal virtual returns (bytes32); + + function _initializeEndValidation( + bytes32 validationID, + bool includeUptime, + address rewardRecipient + ) internal virtual; + + function _forceInitializeEndValidation( + bytes32 validationID, + bool includeUptime, + address rewardRecipient + ) internal virtual; + + function _setUp() internal virtual returns (IValidatorManager); + + function _beforeSend(uint256 amount, address spender) internal virtual; + + function _defaultConversionData() internal view returns (ConversionData memory) { + InitialValidator[] memory initialValidators = new InitialValidator[](2); + // The first initial validator has a high weight relative to the default PoS validator weight + // to avoid churn issues + initialValidators[0] = InitialValidator({ + nodeID: DEFAULT_INITIAL_VALIDATOR_NODE_ID_1, + weight: DEFAULT_INITIAL_VALIDATOR_WEIGHT, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY + }); + // The second initial validator has a low weight so that it can be safely removed in tests + initialValidators[1] = InitialValidator({ + nodeID: DEFAULT_INITIAL_VALIDATOR_NODE_ID_2, + weight: DEFAULT_WEIGHT, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY + }); + + // Confirm the total initial weight + uint64 initialWeight; + for (uint256 i = 0; i < initialValidators.length; i++) { + initialWeight += initialValidators[i].weight; + } + assertEq(initialWeight, DEFAULT_INITIAL_TOTAL_WEIGHT); + + + return ConversionData({ + l1ID: DEFAULT_L1_ID, + validatorManagerBlockchainID: DEFAULT_SOURCE_BLOCKCHAIN_ID, + validatorManagerAddress: address(validatorManager), + initialValidators: initialValidators + }); + } + + function _defaultConversionDataWeightsTooLow() internal view returns (ConversionData memory) { + InitialValidator[] memory initialValidators = new InitialValidator[](2); + + initialValidators[0] = InitialValidator({ + nodeID: DEFAULT_INITIAL_VALIDATOR_NODE_ID_1, + weight: 1, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY + }); + initialValidators[1] = InitialValidator({ + nodeID: DEFAULT_INITIAL_VALIDATOR_NODE_ID_2, + weight: 3, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY + }); + + return ConversionData({ + l1ID: DEFAULT_L1_ID, + validatorManagerBlockchainID: DEFAULT_SOURCE_BLOCKCHAIN_ID, + validatorManagerAddress: address(validatorManager), + initialValidators: initialValidators + }); + } + + function _defaultConversionDataTotalWeight5() internal view returns (ConversionData memory) { + InitialValidator[] memory initialValidators = new InitialValidator[](2); + + initialValidators[0] = InitialValidator({ + nodeID: DEFAULT_INITIAL_VALIDATOR_NODE_ID_1, + weight: 1, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY + }); + initialValidators[1] = InitialValidator({ + nodeID: DEFAULT_INITIAL_VALIDATOR_NODE_ID_2, + weight: 4, + blsPublicKey: DEFAULT_BLS_PUBLIC_KEY + }); + + return ConversionData({ + l1ID: DEFAULT_L1_ID, + validatorManagerBlockchainID: DEFAULT_SOURCE_BLOCKCHAIN_ID, + validatorManagerAddress: address(validatorManager), + initialValidators: initialValidators + }); + } + + // This needs to be kept in line with the contract conversions, but we can't make external calls + // to the contract and use vm.expectRevert at the same time. + // These are okay to use for PoA as well, because they're just used for conversions inside the tests. + function _valueToWeight(uint256 value) internal pure returns (uint64) { + return uint64(value); + } + + // This needs to be kept in line with the contract conversions, but we can't make external calls + // to the contract and use vm.expectRevert at the same time. + // These are okay to use for PoA as well, because they're just used for conversions inside the tests. + function _weightToValue(uint64 weight) internal pure returns (uint256) { + return uint256(weight); + } + + function _erc7201StorageSlot(bytes memory storageName) internal pure returns (bytes32) { + return keccak256( + abi.encode( + uint256(keccak256(abi.encodePacked("avalanche-icm.storage.", storageName))) - 1 + ) + ) & ~bytes32(uint256(0xff)); + } +} +// solhint-enable no-empty-blocks diff --git a/contracts/validator-manager/tests/ExamplesRewardCalculatorTests.t.sol b/contracts/validator-manager/tests/ExamplesRewardCalculatorTests.t.sol index dc59a6409..9ae21e9c8 100644 --- a/contracts/validator-manager/tests/ExamplesRewardCalculatorTests.t.sol +++ b/contracts/validator-manager/tests/ExamplesRewardCalculatorTests.t.sol @@ -19,7 +19,7 @@ contract ExampleRewardCalculatorTest is Test { uint64 public constant DEFAULT_REWARD_BASIS_POINTS = 42; function setUp() public { - exampleRewardCalculator = new ExampleRewardCalculator(DEFAULT_REWARD_BASIS_POINTS); + exampleRewardCalculator = new ExampleRewardCalculator(DEFAULT_REWARD_BASIS_POINTS,0); } function testRewardCalculation() public view { diff --git a/contracts/validator-manager/tests/NativeTokenStakingManagerTests.t.sol b/contracts/validator-manager/tests/NativeTokenStakingManagerTests.t.sol index c1c80fcbb..d169af30f 100644 --- a/contracts/validator-manager/tests/NativeTokenStakingManagerTests.t.sol +++ b/contracts/validator-manager/tests/NativeTokenStakingManagerTests.t.sol @@ -178,7 +178,7 @@ contract NativeTokenStakingManagerTest is PoSValidatorManagerTest { function _setUp() internal override returns (IValidatorManager) { // Construct the object under test app = new TestableNativeTokenStakingManager(ICMInitializable.Allowed); - rewardCalculator = new ExampleRewardCalculator(DEFAULT_REWARD_RATE); + rewardCalculator = new ExampleRewardCalculator(DEFAULT_REWARD_RATE, 0); PoSValidatorManagerSettings memory defaultPoSSettings = _defaultPoSSettings(); defaultPoSSettings.rewardCalculator = rewardCalculator; diff --git a/contracts/validator-manager/tests/PoSValidatorManagerTests.t.sol b/contracts/validator-manager/tests/PoSValidatorManagerTests.t.sol index 53f6c7691..49e22e1e1 100644 --- a/contracts/validator-manager/tests/PoSValidatorManagerTests.t.sol +++ b/contracts/validator-manager/tests/PoSValidatorManagerTests.t.sol @@ -575,7 +575,7 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest { _completeDefaultDelegator(validationID, delegationID); } - +/* function testClaimDelegationFeesInvalidValidatorStatus() public { bytes32 validationID = _registerDefaultValidator(); bytes32 delegationID = _registerDefaultDelegator(validationID); @@ -709,6 +709,7 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest { posValidatorManager.changeDelegatorRewardRecipient(delegationID, newRewardRecipient); } + function testChangeDelegatorRewardRecipientByNonDelegator() public { bytes32 validationID = _registerDefaultValidator(); @@ -785,7 +786,7 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest { rewardRecipient: DEFAULT_DELEGATOR_ADDRESS }); } - +*/ function testChangeDelegatorRewardRecipient() public { bytes32 validationID = _registerDefaultValidator(); bytes32 delegationID = _registerDefaultDelegator(validationID); @@ -833,6 +834,7 @@ abstract contract PoSValidatorManagerTest is ValidatorManagerTest { rewardRecipient: newRewardRecipient }); } + // Delegator registration is not allowed when Validator is pending removed. function testInitializeDelegatorRegistrationValidatorPendingRemoved() public { diff --git a/contracts/validator-manager/tests/ValidatorManagerTests.t.sol b/contracts/validator-manager/tests/ValidatorManagerTests.t.sol index bcff390f6..6525a5192 100644 --- a/contracts/validator-manager/tests/ValidatorManagerTests.t.sol +++ b/contracts/validator-manager/tests/ValidatorManagerTests.t.sol @@ -431,7 +431,6 @@ abstract contract ValidatorManagerTest is Test { ); vm.warp(registrationExpiry - 1); _mockSendWarpMessage(registerL1ValidatorMessage, bytes32(0)); - _beforeSend(_weightToValue(weight), address(this)); vm.expectEmit(true, true, true, true, address(validatorManager)); emit ValidationPeriodCreated(validationID, nodeID, bytes32(0), weight, registrationExpiry); diff --git a/lib/forge-std b/lib/forge-std index c28115db8..1eea5bae1 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit c28115db8d90ebffb41953cf83aac63130f4bd40 +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/remappings.txt b/remappings.txt index afd54042d..b1ed7a8f1 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,3 +5,4 @@ @teleporter=contracts/teleporter @mocks=contracts/mocks @utilities=contracts/utilities +