From 2a4e501ffcae56368199df060b36083ed9ebb426 Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 13 May 2025 19:58:57 +0200 Subject: [PATCH 01/82] Implemented flow to trigger validators exit --- mainnet-contracts/src/PufferModule.sol | 28 ++- mainnet-contracts/src/PufferModuleManager.sol | 26 ++ .../Eigenlayer-Slashing/IEigenPod.sol | 227 +++++++++++++++--- .../Eigenlayer-Slashing/ISemVerMixin.sol | 11 + .../src/interface/IPufferModuleManager.sol | 7 + 5 files changed, 271 insertions(+), 28 deletions(-) create mode 100644 mainnet-contracts/src/interface/Eigenlayer-Slashing/ISemVerMixin.sol diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index 7c2e57cb..4e77333e 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -8,7 +8,7 @@ import { IEigenPodManager } from "../src/interface/Eigenlayer-Slashing/IEigenPod import { ISignatureUtils } from "../src/interface/Eigenlayer-Slashing/ISignatureUtils.sol"; import { IStrategy } from "../src/interface/Eigenlayer-Slashing/IStrategy.sol"; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; -import { IEigenPod } from "../src/interface/Eigenlayer-Slashing/IEigenPod.sol"; +import { IEigenPod, IEigenPodTypes } from "../src/interface/Eigenlayer-Slashing/IEigenPod.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; import { Unauthorized } from "./Errors.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -195,6 +195,32 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { return EIGEN_DELEGATION_MANAGER.undelegate(address(this)); } + /** + * @notice Triggers the validators exit for the given pubkeys + * @param pubkeys The pubkeys of the validators to exit + * @dev Only callable by the PufferModuleManager + * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * to the caller from the EigenPod + */ + function triggerValidatorsExit(bytes[] calldata pubkeys) external virtual payable onlyPufferModuleManager { + ModuleStorage storage $ = _getPufferModuleStorage(); + + IEigenPodTypes.WithdrawalRequest[] memory requests = new IEigenPodTypes.WithdrawalRequest[](pubkeys.length); + for (uint256 i = 0; i < pubkeys.length; i++) { + requests[i] = IEigenPodTypes.WithdrawalRequest({ + pubkey: pubkeys[i], + amountGwei: 0 // This means full exit. Only value supported for 0x01 validators + }); + } + uint256 oldBalance = address(this).balance - msg.value; + $.eigenPod.requestWithdrawal{value: msg.value}(requests); + uint256 excessAmount = address(this).balance - oldBalance; + if (excessAmount > 0) { + Address.sendValue(payable(PUFFER_MODULE_MANAGER), excessAmount); + } + } + /** * @notice Sets the rewards claimer to `claimer` for the PufferModule */ diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index f7d6f619..564eda04 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -9,6 +9,7 @@ import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { RestakingOperator } from "./RestakingOperator.sol"; import { IPufferModuleManager } from "./interface/IPufferModuleManager.sol"; import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; @@ -26,6 +27,9 @@ import { PufferModule } from "./PufferModule.sol"; * @custom:security-contact security@puffer.fi */ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, UUPSUpgradeable { + using Address for address; + using Address for address payable; + address public immutable PUFFER_MODULE_BEACON; address public immutable RESTAKING_OPERATOR_BEACON; address public immutable PUFFER_PROTOCOL; @@ -239,6 +243,28 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, emit PufferModuleUndelegated(moduleName); } + /** + * @notice Triggers the validators exit for the given pubkeys + * @param moduleName The name of the Puffer module + * @param pubkeys The pubkeys of the validators to exit + * @dev Restricted to the DAO + * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * to the caller from the EigenPod + */ + function triggerValidatorsExit(bytes32 moduleName, bytes[] calldata pubkeys) external virtual payable restricted { + address moduleAddress = IPufferProtocol(PUFFER_PROTOCOL).getModuleAddress(moduleName); + + uint256 oldBalance = address(this).balance - msg.value; + PufferModule(payable(moduleAddress)).triggerValidatorsExit{value: msg.value}(pubkeys); + uint256 excessAmount = address(this).balance - oldBalance; + if (excessAmount > 0) { + Address.sendValue(payable(msg.sender), excessAmount); + } + + emit ValidatorsExitTriggered(moduleName, pubkeys); + } + /** * @notice Calls the callRegisterOperatorToAVS function on the target restaking operator * @param restakingOperator is the address of the restaking operator diff --git a/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol b/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol index b465f711..03943b0f 100644 --- a/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol +++ b/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol @@ -4,6 +4,7 @@ pragma solidity >=0.5.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../libraries/BeaconChainProofs.sol"; +import "./ISemVerMixin.sol"; import "./IEigenPodManager.sol"; interface IEigenPodErrors { @@ -42,8 +43,6 @@ interface IEigenPodErrors { /// @dev Thrown when amount exceeds `restakedExecutionLayerGwei`. error InsufficientWithdrawableBalance(); - /// @dev Thrown when provided `amountGwei` is not a multiple of gwei. - error AmountMustBeMultipleOfGwei(); /// Validator Status @@ -60,6 +59,17 @@ interface IEigenPodErrors { /// @dev Thrown when a validator has not been slashed on the beacon chain. error ValidatorNotSlashedOnBeaconChain(); + /// Consolidation and Withdrawal Requests + + /// @dev Thrown when a predeploy request is initiated with insufficient msg.value + error InsufficientFunds(); + /// @dev Thrown when refunding excess fees from a predeploy fails + error RefundFailed(); + /// @dev Thrown when calling the predeploy fails + error PredeployFailed(); + /// @dev Thrown when querying a predeploy for its current fee fails + error FeeQueryFailed(); + /// Misc /// @dev Thrown when an invalid block root is returned by the EIP-4788 oracle. @@ -68,24 +78,28 @@ interface IEigenPodErrors { error MsgValueNot32ETH(); /// @dev Thrown when provided `beaconTimestamp` is too far in the past. error BeaconTimestampTooFarInPast(); + /// @dev Thrown when the pectraForkTimestamp returned from the EigenPodManager is zero + error ForkTimestampZero(); } interface IEigenPodTypes { enum VALIDATOR_STATUS { - INACTIVE, // doesn't exist + INACTIVE, // doesnt exist ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod WITHDRAWN // withdrawn from the Beacon Chain } + /** + * @param validatorIndex index of the validator on the beacon chain + * @param restakedBalanceGwei amount of beacon chain ETH restaked on EigenLayer in gwei + * @param lastCheckpointedAt timestamp of the validator's most recent balance update + * @param status last recorded status of the validator + */ struct ValidatorInfo { - // index of the validator in the beacon chain uint64 validatorIndex; - // amount of beacon chain ETH restaked on EigenLayer in gwei uint64 restakedBalanceGwei; - //timestamp of the validator's most recent balance update uint64 lastCheckpointedAt; - // status of the validator VALIDATOR_STATUS status; } @@ -96,6 +110,30 @@ interface IEigenPodTypes { int64 balanceDeltasGwei; uint64 prevBeaconBalanceGwei; } + + /** + * @param srcPubkey the pubkey of the source validator for the consolidation + * @param targetPubkey the pubkey of the target validator for the consolidation + * @dev Note that if srcPubkey == targetPubkey, this is a "switch request," and will + * change the validator's withdrawal credential type from 0x01 to 0x02. + * For more notes on usage, see `requestConsolidation` + */ + struct ConsolidationRequest { + bytes srcPubkey; + bytes targetPubkey; + } + + /** + * @param pubkey the pubkey of the validator to withdraw from + * @param amountGwei the amount (in gwei) to withdraw from the beacon chain to the pod + * @dev Note that if amountGwei == 0, this is a "full exit request," and will fully exit + * the validator to the pod. + * For more notes on usage, see `requestWithdrawal` + */ + struct WithdrawalRequest { + bytes pubkey; + uint64 amountGwei; + } } interface IEigenPodEvents is IEigenPodTypes { @@ -131,6 +169,18 @@ interface IEigenPodEvents is IEigenPodTypes { /// @notice Emitted when a validaor is proven to have 0 balance at a given checkpoint event ValidatorWithdrawn(uint64 indexed checkpointTimestamp, uint40 indexed validatorIndex); + + /// @notice Emitted when a consolidation request is initiated where source == target + event SwitchToCompoundingRequested(bytes32 indexed validatorPubkeyHash); + + /// @notice Emitted when a standard consolidation request is initiated + event ConsolidationRequested(bytes32 indexed sourcePubkeyHash, bytes32 indexed targetPubkeyHash); + + /// @notice Emitted when a withdrawal request is initiated where request.amountGwei == 0 + event ExitRequested(bytes32 indexed validatorPubkeyHash); + + /// @notice Emitted when a partial withdrawal request is initiated + event WithdrawalRequested(bytes32 indexed validatorPubkeyHash, uint64 withdrawalAmountGwei); } /** @@ -140,19 +190,20 @@ interface IEigenPodEvents is IEigenPodTypes { * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts */ -interface IEigenPod is IEigenPodErrors, IEigenPodEvents { +interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager - function initialize(address owner) external; + function initialize( + address owner + ) external; /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + /// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract. function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; /** - * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address - * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. - * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `restakedExecutionLayerGwei` exceeds the - * `amountWei` input (when converted to GWEI). - * @dev Reverts if `amountWei` is not a whole Gwei amount + * @notice Transfers `amountWei` from this contract to the `recipient`. Only callable by the EigenPodManager as part + * of the DelegationManager's withdrawal flow. + * @dev `amountWei` is not required to be a whole Gwei amount. Amounts less than a Gwei multiple may be unrecoverable due to Gwei conversion. */ function withdrawRestakedBeaconChainETH(address recipient, uint256 amount) external; @@ -168,7 +219,9 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner * to prevent accidentally starting a checkpoint that will not increase their shares */ - function startCheckpoint(bool revertIfNoBalance) external; + function startCheckpoint( + bool revertIfNoBalance + ) external; /** * @dev Progress the current checkpoint towards completion by submitting one or more validator @@ -243,17 +296,112 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { BeaconChainProofs.ValidatorProof calldata proof ) external; + /// @notice Allows the owner or proof submitter to initiate one or more requests to + /// consolidate their validators on the beacon chain. + /// @param requests An array of requests consisting of the source and target pubkeys + /// of the validators to be consolidated + /// @dev Both the source and target validator MUST have active withdrawal credentials + /// pointed at the pod + /// @dev The consolidation request predeploy requires a fee is sent with each request; + /// this is pulled from msg.value. After submitting all requests, any remaining fee is + /// refunded to the caller by calling its fallback function. + /// @dev This contract exposes `getConsolidationRequestFee` to query the current fee for + /// a single request. If submitting multiple requests in a single block, the total fee + /// is equal to (fee * requests.length). This fee is updated at the end of each block. + /// + /// (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation for details) + /// + /// @dev Note on beacon chain behavior: + /// - If request.srcPubkey == request.targetPubkey, this is a "switch" consolidation. Once + /// processed on the beacon chain, the validator's withdrawal credentials will be changed + /// to compounding (0x02). + /// - The rest of the notes assume src != target. + /// - The target validator MUST already have 0x02 credentials. The source validator can have either. + /// - Consoldiation sets the source validator's exit_epoch and withdrawable_epoch, similar to an exit. + /// When the exit epoch is reached, an epoch sweep will process the consolidation and transfer balance + /// from the source to the target validator. + /// - Consolidation transfers min(srcValidator.effective_balance, state.balance[srcIndex]) to the target. + /// This may not be the entirety of the source validator's balance; any remainder will be moved to the + /// pod when hit by a subsequent withdrawal sweep. + /// + /// @dev Note that consolidation requests CAN FAIL for a variety of reasons. Failures occur when the request + /// is processed on the beacon chain, and are invisible to the pod. The pod and predeploy cannot guarantee + /// a request will succeed; it's up to the pod owner to determine this for themselves. If your request fails, + /// you can retry by initiating another request via this method. + /// + /// Some requirements that are NOT checked by the pod: + /// - If request.srcPubkey == request.targetPubkey, the validator MUST have 0x01 credentials + /// - If request.srcPubkey != request.targetPubkey, the target validator MUST have 0x02 credentials + /// - Both the source and target validators MUST be active and MUST NOT have initiated exits + /// - The source validator MUST NOT have pending partial withdrawal requests (via `requestWithdrawal`) + /// - If the source validator is slashed after requesting consolidation (but before processing), + /// the consolidation will be skipped. + /// + /// For further reference, see consolidation processing at block and epoch boundaries: + /// - Block: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_consolidation_request + /// - Epoch: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_pending_consolidations + function requestConsolidation( + ConsolidationRequest[] calldata requests + ) external payable; + + /// @notice Allows the owner or proof submitter to initiate one or more requests to + /// withdraw funds from validators on the beacon chain. + /// @param requests An array of requests consisting of the source validator and an + /// amount to withdraw + /// @dev The withdrawal request predeploy requires a fee is sent with each request; + /// this is pulled from msg.value. After submitting all requests, any remaining fee is + /// refunded to the caller by calling its fallback function. + /// @dev This contract exposes `getWithdrawalRequestFee` to query the current fee for + /// a single request. If submitting multiple requests in a single block, the total fee + /// is equal to (fee * requests.length). This fee is updated at the end of each block. + /// + /// (See https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule for details) + /// + /// @dev Note on beacon chain behavior: + /// - Withdrawal requests have two types: full exit requests, and partial exit requests. + /// Partial exit requests will be skipped if the validator has 0x01 withdrawal credentials. + /// If you want your validators to have access to partial exits, use `requestConsolidation` + /// to change their withdrawal credentials to compounding (0x02). + /// - If request.amount == 0, this is a FULL exit request. A full exit request initiates a + /// standard validator exit. + /// - Other amounts are treated as PARTIAL exit requests. A partial exit request will NOT result + /// in a validator with less than 32 ETH balance. Any requested amount above this is ignored. + /// - The actual amount withdrawn for a partial exit is given by the formula: + /// min(request.amount, state.balances[vIdx] - 32 ETH - pending_balance_to_withdraw) + /// (where `pending_balance_to_withdraw` is the sum of any outstanding partial exit requests) + /// (Note that this means you may request more than is actually withdrawn!) + /// + /// @dev Note that withdrawal requests CAN FAIL for a variety of reasons. Failures occur when the request + /// is processed on the beacon chain, and are invisible to the pod. The pod and predeploy cannot guarantee + /// a request will succeed; it's up to the pod owner to determine this for themselves. If your request fails, + /// you can retry by initiating another request via this method. + /// + /// Some requirements that are NOT checked by the pod: + /// - request.pubkey MUST be a valid validator pubkey + /// - request.pubkey MUST belong to a validator whose withdrawal credentials are this pod + /// - If request.amount is for a partial exit, the validator MUST have 0x02 withdrawal credentials + /// - If request.amount is for a full exit, the validator MUST NOT have any pending partial exits + /// - The validator MUST be active and MUST NOT have initiated exit + /// + /// For further reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_withdrawal_request + function requestWithdrawal( + WithdrawalRequest[] calldata requests + ) external payable; + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; /// @notice Allows the owner of a pod to update the proof submitter, a permissioned - /// address that can call `startCheckpoint` and `verifyWithdrawalCredentials`. + /// address that can call various EigenPod methods, but cannot trigger asset withdrawals + /// from the DelegationManager. /// @dev Note that EITHER the podOwner OR proofSubmitter can access these methods, /// so it's fine to set your proofSubmitter to 0 if you want the podOwner to be the /// only address that can call these methods. /// @param newProofSubmitter The new proof submitter address. If set to 0, only the - /// pod owner will be able to call `startCheckpoint` and `verifyWithdrawalCredentials` - function setProofSubmitter(address newProofSubmitter) external; + /// pod owner will be able to call EigenPod methods. + function setProofSubmitter( + address newProofSubmitter + ) external; /** * @@ -267,7 +415,8 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { /// @dev If this address is NOT set, only the podOwner can call `startCheckpoint` and `verifyWithdrawalCredentials` function proofSubmitter() external view returns (address); - /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer), + /// @notice Native ETH in the pod that has been accounted for in a checkpoint (denominated in gwei). + /// This amount is withdrawable from the pod via the DelegationManager withdrawal flow. function withdrawableRestakedExecutionLayerGwei() external view returns (uint64); /// @notice The single EigenPodManager for EigenLayer @@ -277,16 +426,24 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { function podOwner() external view returns (address); /// @notice Returns the validatorInfo struct for the provided pubkeyHash - function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory); + function validatorPubkeyHashToInfo( + bytes32 validatorPubkeyHash + ) external view returns (ValidatorInfo memory); /// @notice Returns the validatorInfo struct for the provided pubkey - function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory); + function validatorPubkeyToInfo( + bytes calldata validatorPubkey + ) external view returns (ValidatorInfo memory); - /// @notice This returns the status of a given validator - function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS); + /// @notice Returns the validator status for a given validator pubkey hash + function validatorStatus( + bytes32 pubkeyHash + ) external view returns (VALIDATOR_STATUS); - /// @notice This returns the status of a given validator pubkey - function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS); + /// @notice Returns the validator status for a given validator pubkey + function validatorStatus( + bytes calldata validatorPubkey + ) external view returns (VALIDATOR_STATUS); /// @notice Number of validators with proven withdrawal credentials, who do not have proven full withdrawals function activeValidatorCount() external view returns (uint256); @@ -298,6 +455,8 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { function currentCheckpointTimestamp() external view returns (uint64); /// @notice Returns the currently-active checkpoint + /// To save gas on checkpoint creation, we don't delete checkpoints when they're completed. + /// If there's not an active checkpoint, this method returns an empty Checkpoint. function currentCheckpoint() external view returns (Checkpoint memory); /// @notice For each checkpoint, the total balance attributed to exited validators, in gwei @@ -328,11 +487,25 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { /// - The final partial withdrawal for an exited validator will be likely be included in this mapping. /// i.e. if a validator was last checkpointed at 32.1 ETH before exiting, the next checkpoint will calculate their /// "exited" amount to be 32.1 ETH rather than 32 ETH. - function checkpointBalanceExitedGwei(uint64) external view returns (uint64); + function checkpointBalanceExitedGwei( + uint64 + ) external view returns (uint64); /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` /// @param timestamp of the block for which the parent block root will be returned. MUST correspond /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method /// will revert. - function getParentBlockRoot(uint64 timestamp) external view returns (bytes32); + function getParentBlockRoot( + uint64 timestamp + ) external view returns (bytes32); + + /// @notice Returns the fee required to add a consolidation request to the EIP-7251 predeploy this block. + /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7251#fee-calculation + /// Consider overestimating the amount sent to ensure the fee does not update before your transaction. + function getConsolidationRequestFee() external view returns (uint256); + + /// @notice Returns the current fee required to add a withdrawal request to the EIP-7002 predeploy. + /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule + /// Consider overestimating the amount sent to ensure the fee does not update before your transaction. + function getWithdrawalRequestFee() external view returns (uint256); } diff --git a/mainnet-contracts/src/interface/Eigenlayer-Slashing/ISemVerMixin.sol b/mainnet-contracts/src/interface/Eigenlayer-Slashing/ISemVerMixin.sol new file mode 100644 index 00000000..206cf38d --- /dev/null +++ b/mainnet-contracts/src/interface/Eigenlayer-Slashing/ISemVerMixin.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/// @title ISemVerMixin +/// @notice A mixin interface that provides semantic versioning functionality. +/// @dev Follows SemVer 2.0.0 specification (https://semver.org/) +interface ISemVerMixin { + /// @notice Returns the semantic version string of the contract. + /// @return The version string in SemVer format (e.g., "v1.1.1") + function version() external view returns (string memory); +} diff --git a/mainnet-contracts/src/interface/IPufferModuleManager.sol b/mainnet-contracts/src/interface/IPufferModuleManager.sol index fa5b754f..31b9963e 100644 --- a/mainnet-contracts/src/interface/IPufferModuleManager.sol +++ b/mainnet-contracts/src/interface/IPufferModuleManager.sol @@ -73,6 +73,13 @@ interface IPufferModuleManager { */ event PufferModuleUndelegated(bytes32 indexed moduleName); + /** + * @notice Emitted when the validators exit is triggered + * @param moduleName the module name to be undelegated + * @dev Signature "0x456e0aba5f7f36ec541f2f550d3f5895eb7d1ae057f45e8683952ac182254e5d" + */ + event ValidatorsExitTriggered(bytes32 indexed moduleName, bytes[] pubkeys); + /** * @notice Emitted when the restaking operator avs signature proof is updated * @param restakingOperator is the address of the restaking operator From d5b1a1e345037dce182682c79083d7de4c55a385 Mon Sep 17 00:00:00 2001 From: Eladio Date: Wed, 14 May 2025 18:33:01 +0200 Subject: [PATCH 02/82] Added role to trigger validators exit --- mainnet-contracts/script/Roles.sol | 1 + mainnet-contracts/script/SetupAccess.s.sol | 15 +++++++++++++-- mainnet-contracts/src/PufferModuleManager.sol | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/mainnet-contracts/script/Roles.sol b/mainnet-contracts/script/Roles.sol index f969b0b8..dfcd3df1 100644 --- a/mainnet-contracts/script/Roles.sol +++ b/mainnet-contracts/script/Roles.sol @@ -13,6 +13,7 @@ uint64 constant ROLE_ID_OPERATIONS_PAYMASTER = 23; uint64 constant ROLE_ID_OPERATIONS_COORDINATOR = 24; uint64 constant ROLE_ID_WITHDRAWAL_FINALIZER = 25; uint64 constant ROLE_ID_REVENUE_DEPOSITOR = 26; +uint64 constant ROLE_ID_VALIDATOR_EXITOR = 27; // Role assigned to validator ticket price setter uint64 constant ROLE_ID_VT_PRICER = 25; diff --git a/mainnet-contracts/script/SetupAccess.s.sol b/mainnet-contracts/script/SetupAccess.s.sol index 00d6907f..eb077ca6 100644 --- a/mainnet-contracts/script/SetupAccess.s.sol +++ b/mainnet-contracts/script/SetupAccess.s.sol @@ -28,7 +28,8 @@ import { ROLE_ID_PUFFER_PROTOCOL, ROLE_ID_DAO, ROLE_ID_OPERATIONS_COORDINATOR, - ROLE_ID_VT_PRICER + ROLE_ID_VT_PRICER, + ROLE_ID_VALIDATOR_EXITOR } from "../script/Roles.sol"; contract SetupAccess is BaseScript { @@ -153,7 +154,7 @@ contract SetupAccess is BaseScript { } function _setupPufferModuleManagerAccess() internal view returns (bytes[] memory) { - bytes[] memory calldatas = new bytes[](2); + bytes[] memory calldatas = new bytes[](3); // Dao selectors bytes4[] memory selectors = new bytes4[](7); @@ -181,6 +182,16 @@ contract SetupAccess is BaseScript { ROLE_ID_OPERATIONS_PAYMASTER ); + // ValidatorExitor selectors + bytes4[] memory validatorExitorSelectors = new bytes4[](1); + validatorExitorSelectors[0] = PufferModuleManager.triggerValidatorsExit.selector; + + calldatas[2] = abi.encodeWithSelector( + AccessManager.setTargetFunctionRole.selector, + pufferDeployment.moduleManager, + validatorExitorSelectors, + ROLE_ID_VALIDATOR_EXITOR + ); return calldatas; } diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index 564eda04..614dde84 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -247,16 +247,16 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, * @notice Triggers the validators exit for the given pubkeys * @param moduleName The name of the Puffer module * @param pubkeys The pubkeys of the validators to exit - * @dev Restricted to the DAO + * @dev Restricted to the VALIDATOR_EXITOR * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function triggerValidatorsExit(bytes32 moduleName, bytes[] calldata pubkeys) external virtual payable restricted { + function triggerValidatorsExit(bytes32 moduleName, bytes[] calldata pubkeys) external payable virtual restricted { address moduleAddress = IPufferProtocol(PUFFER_PROTOCOL).getModuleAddress(moduleName); uint256 oldBalance = address(this).balance - msg.value; - PufferModule(payable(moduleAddress)).triggerValidatorsExit{value: msg.value}(pubkeys); + PufferModule(payable(moduleAddress)).triggerValidatorsExit{ value: msg.value }(pubkeys); uint256 excessAmount = address(this).balance - oldBalance; if (excessAmount > 0) { Address.sendValue(payable(msg.sender), excessAmount); From 607aa382086649b5ef8b805f96b5b41089b08ea4 Mon Sep 17 00:00:00 2001 From: Eladio Date: Wed, 14 May 2025 18:33:31 +0200 Subject: [PATCH 03/82] Removed references to enclave in PufferProtocol and adapted tests (WIP) --- ...GenerateBLSKeysAndRegisterValidators.s.sol | 3 +- mainnet-contracts/src/GuardianModule.sol | 14 ++ mainnet-contracts/src/PufferModule.sol | 6 +- mainnet-contracts/src/PufferProtocol.sol | 51 +---- .../Eigenlayer-Slashing/IEigenPod.sol | 44 +--- .../src/interface/IGuardianModule.sol | 15 ++ .../src/interface/IPufferProtocol.sol | 15 +- .../src/struct/ValidatorKeyData.sol | 1 - .../test/handlers/PufferProtocolHandler.sol | 55 +---- .../test/unit/PufferProtocol.t.sol | 214 ++++++------------ 10 files changed, 126 insertions(+), 292 deletions(-) diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index 649ac165..97c6c690 100644 --- a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol +++ b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol @@ -103,8 +103,7 @@ contract GenerateBLSKeysAndRegisterValidators is Script { signature: stdJson.readBytes(registrationJson, ".signature"), depositDataRoot: stdJson.readBytes32(registrationJson, ".deposit_data_root"), blsEncryptedPrivKeyShares: blsEncryptedPrivKeyShares, - blsPubKeySet: stdJson.readBytes(registrationJson, ".bls_pub_key_set"), - raveEvidence: "" + blsPubKeySet: stdJson.readBytes(registrationJson, ".bls_pub_key_set") }); Permit memory pufETHPermit = _signPermit({ diff --git a/mainnet-contracts/src/GuardianModule.sol b/mainnet-contracts/src/GuardianModule.sol index fec650c9..c25f4878 100644 --- a/mainnet-contracts/src/GuardianModule.sol +++ b/mainnet-contracts/src/GuardianModule.sol @@ -17,6 +17,7 @@ import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; * @title Guardian module * @author Puffer Finance * @dev This contract is responsible for storing enclave keys and validation of guardian's EOA/Enclave signatures + * * @dev Some of these functions are no longer used since enclaves have been deprecated * @custom:security-contact security@puffer.fi */ contract GuardianModule is AccessManaged, IGuardianModule { @@ -38,6 +39,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @notice Enclave Verifier smart contract + * @dev DEPRECATED */ IEnclaveVerifier public immutable ENCLAVE_VERIFIER; @@ -53,11 +55,13 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @dev MRSIGNER value for SGX + * @dev DEPRECATED */ bytes32 internal _mrsigner; /** * @dev MRENCLAVE value for SGX + * @dev DEPRECATED */ bytes32 internal _mrenclave; @@ -77,6 +81,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @dev Mapping of a Guardian's EOA to enclave data + * @dev DEPRECATED */ mapping(address guardian => GuardianData data) internal _guardianEnclaves; @@ -139,6 +144,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function validateProvisionNode( uint256 pufferModuleIndex, @@ -220,6 +226,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function validateGuardiansEnclaveSignatures(bytes[] calldata enclaveSignatures, bytes32 signedMessageHash) public @@ -240,6 +247,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule * @dev Restricted to the DAO + * @dev DEPRECATED */ function setGuardianEnclaveMeasurements(bytes32 newMrEnclave, bytes32 newMrSigner) external restricted { emit MrEnclaveChanged(_mrenclave, newMrEnclave); @@ -298,6 +306,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function rotateGuardianKey(uint256 blockNumber, bytes calldata pubKey, RaveEvidence calldata evidence) external { address guardian = msg.sender; @@ -341,6 +350,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getGuardiansEnclaveAddress(address guardian) external view returns (address) { return _guardianEnclaves[guardian].enclaveAddress; @@ -348,6 +358,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getGuardiansEnclaveAddresses() public view returns (address[] memory) { uint256 guardiansLength = _guardians.length(); @@ -367,6 +378,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getGuardiansEnclavePubkeys() external view returns (bytes[] memory) { uint256 guardiansLength = _guardians.length(); @@ -381,6 +393,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getMrenclave() external view returns (bytes32) { return _mrenclave; @@ -388,6 +401,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getMrsigner() external view returns (bytes32) { return _mrsigner; diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index 4e77333e..10abbe0a 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -203,7 +203,7 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function triggerValidatorsExit(bytes[] calldata pubkeys) external virtual payable onlyPufferModuleManager { + function triggerValidatorsExit(bytes[] calldata pubkeys) external payable virtual onlyPufferModuleManager { ModuleStorage storage $ = _getPufferModuleStorage(); IEigenPodTypes.WithdrawalRequest[] memory requests = new IEigenPodTypes.WithdrawalRequest[](pubkeys.length); @@ -211,10 +211,10 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { requests[i] = IEigenPodTypes.WithdrawalRequest({ pubkey: pubkeys[i], amountGwei: 0 // This means full exit. Only value supported for 0x01 validators - }); + }); } uint256 oldBalance = address(this).balance - msg.value; - $.eigenPod.requestWithdrawal{value: msg.value}(requests); + $.eigenPod.requestWithdrawal{ value: msg.value }(requests); uint256 excessAmount = address(this).balance - oldBalance; if (excessAmount > 0) { Address.sendValue(payable(PUFFER_MODULE_MANAGER), excessAmount); diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index d910547e..2cf3bfd0 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -56,14 +56,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad uint256 internal constant _BLS_PUB_KEY_LENGTH = 48; /** - * @dev ETH Amount required to be deposited as a bond if the node operator uses SGX + * @dev ETH Amount required to be deposited as a bond */ - uint256 internal constant _ENCLAVE_VALIDATOR_BOND = 1 ether; - - /** - * @dev ETH Amount required to be deposited as a bond if the node operator doesn't use SGX - */ - uint256 internal constant _NO_ENCLAVE_VALIDATOR_BOND = 2 ether; + uint256 internal constant VALIDATOR_BOND = 2 ether; /** * @dev Default "PUFFER_MODULE_0" module @@ -198,14 +193,12 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); - uint256 validatorBondInETH = data.raveEvidence.length > 0 ? _ENCLAVE_VALIDATOR_BOND : _NO_ENCLAVE_VALIDATOR_BOND; - // If the node operator is paying for the bond in ETH and wants to transfer VT from their wallet, the ETH amount they send must be equal the bond amount - if (vtPermit.amount != 0 && pufETHPermit.amount == 0 && msg.value != validatorBondInETH) { + if (vtPermit.amount != 0 && pufETHPermit.amount == 0 && msg.value != VALIDATOR_BOND) { revert InvalidETHAmount(); } - uint256 vtPayment = pufETHPermit.amount == 0 ? msg.value - validatorBondInETH : msg.value; + uint256 vtPayment = pufETHPermit.amount == 0 ? msg.value - VALIDATOR_BOND : msg.value; uint256 receivedVtAmount; // If the VT permit amount is zero, that means that the user is paying for VT with ETH @@ -228,10 +221,10 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // If the pufETH permit amount is zero, that means that the user is paying the bond with ETH if (pufETHPermit.amount == 0) { // Mint pufETH by depositing ETH and store the bond amount - bondAmount = PUFFER_VAULT.depositETH{ value: validatorBondInETH }(address(this)); + bondAmount = PUFFER_VAULT.depositETH{ value: VALIDATOR_BOND }(address(this)); } else { // Calculate the pufETH amount that we need to transfer from the user - bondAmount = PUFFER_VAULT.convertToShares(validatorBondInETH); + bondAmount = PUFFER_VAULT.convertToShares(VALIDATOR_BOND); _callPermit(address(PUFFER_VAULT), pufETHPermit); // slither-disable-next-line unchecked-transfer @@ -251,11 +244,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @inheritdoc IPufferProtocol * @dev Restricted to Puffer Paymaster */ - function provisionNode( - bytes[] calldata guardianEnclaveSignatures, - bytes calldata validatorSignature, - bytes32 depositRootHash - ) external restricted { + function provisionNode(bytes calldata validatorSignature, bytes32 depositRootHash) external restricted { if (depositRootHash != BEACON_DEPOSIT_CONTRACT.get_deposit_root()) { revert InvalidDepositRootHash(); } @@ -275,7 +264,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad $: $, moduleName: moduleName, index: index, - guardianEnclaveSignatures: guardianEnclaveSignatures, validatorSignature: validatorSignature }); @@ -612,20 +600,14 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad /** * @notice Returns necessary information to make Guardian's life easier */ - function getPayload(bytes32 moduleName, bool usingEnclave) - external - view - returns (bytes[] memory, bytes memory, uint256, uint256) - { + function getPayload(bytes32 moduleName) external view returns (bytes memory, uint256, uint256) { ProtocolStorage storage $ = _getPufferProtocolStorage(); - bytes[] memory pubKeys = GUARDIAN_MODULE.getGuardiansEnclavePubkeys(); bytes memory withdrawalCredentials = getWithdrawalCredentials(address($.modules[moduleName])); uint256 threshold = GUARDIAN_MODULE.getThreshold(); - uint256 validatorBond = usingEnclave ? _ENCLAVE_VALIDATOR_BOND : _NO_ENCLAVE_VALIDATOR_BOND; - uint256 ethAmount = validatorBond + ($.minimumVtAmount * PUFFER_ORACLE.getValidatorTicketPrice()) / 1 ether; + uint256 ethAmount = VALIDATOR_BOND + ($.minimumVtAmount * PUFFER_ORACLE.getValidatorTicketPrice()) / 1 ether; - return (pubKeys, withdrawalCredentials, threshold, ethAmount); + return (withdrawalCredentials, threshold, ethAmount); } /** @@ -669,7 +651,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad ++$.moduleLimits[moduleName].numberOfRegisteredValidators; } emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); - emit ValidatorKeyRegistered(data.blsPubKey, pufferModuleIndex, moduleName, (data.raveEvidence.length > 0)); + emit ValidatorKeyRegistered(data.blsPubKey, pufferModuleIndex, moduleName); } function _setValidatorLimitPerModule(bytes32 moduleName, uint128 limit) internal { @@ -770,7 +752,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad ProtocolStorage storage $, bytes32 moduleName, uint256 index, - bytes[] calldata guardianEnclaveSignatures, bytes calldata validatorSignature ) internal { bytes memory validatorPubKey = $.validators[moduleName][index].pubKey; @@ -780,16 +761,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad bytes32 depositDataRoot = LibBeaconchainContract.getDepositDataRoot(validatorPubKey, validatorSignature, withdrawalCredentials); - // Check the signatures (reverts if invalid) - GUARDIAN_MODULE.validateProvisionNode({ - pufferModuleIndex: index, - pubKey: validatorPubKey, - signature: validatorSignature, - depositDataRoot: depositDataRoot, - withdrawalCredentials: withdrawalCredentials, - guardianEnclaveSignatures: guardianEnclaveSignatures - }); - PufferModule module = $.modules[moduleName]; // Transfer 32 ETH to the module diff --git a/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol b/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol index 03943b0f..f4288fdf 100644 --- a/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol +++ b/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol @@ -192,9 +192,7 @@ interface IEigenPodEvents is IEigenPodTypes { */ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager - function initialize( - address owner - ) external; + function initialize(address owner) external; /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. /// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract. @@ -219,9 +217,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { * @param revertIfNoBalance Forces a revert if the pod ETH balance is 0. This allows the pod owner * to prevent accidentally starting a checkpoint that will not increase their shares */ - function startCheckpoint( - bool revertIfNoBalance - ) external; + function startCheckpoint(bool revertIfNoBalance) external; /** * @dev Progress the current checkpoint towards completion by submitting one or more validator @@ -340,9 +336,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// For further reference, see consolidation processing at block and epoch boundaries: /// - Block: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_consolidation_request /// - Epoch: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_pending_consolidations - function requestConsolidation( - ConsolidationRequest[] calldata requests - ) external payable; + function requestConsolidation(ConsolidationRequest[] calldata requests) external payable; /// @notice Allows the owner or proof submitter to initiate one or more requests to /// withdraw funds from validators on the beacon chain. @@ -384,9 +378,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// - The validator MUST be active and MUST NOT have initiated exit /// /// For further reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_withdrawal_request - function requestWithdrawal( - WithdrawalRequest[] calldata requests - ) external payable; + function requestWithdrawal(WithdrawalRequest[] calldata requests) external payable; /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; @@ -399,9 +391,7 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// only address that can call these methods. /// @param newProofSubmitter The new proof submitter address. If set to 0, only the /// pod owner will be able to call EigenPod methods. - function setProofSubmitter( - address newProofSubmitter - ) external; + function setProofSubmitter(address newProofSubmitter) external; /** * @@ -426,24 +416,16 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { function podOwner() external view returns (address); /// @notice Returns the validatorInfo struct for the provided pubkeyHash - function validatorPubkeyHashToInfo( - bytes32 validatorPubkeyHash - ) external view returns (ValidatorInfo memory); + function validatorPubkeyHashToInfo(bytes32 validatorPubkeyHash) external view returns (ValidatorInfo memory); /// @notice Returns the validatorInfo struct for the provided pubkey - function validatorPubkeyToInfo( - bytes calldata validatorPubkey - ) external view returns (ValidatorInfo memory); + function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory); /// @notice Returns the validator status for a given validator pubkey hash - function validatorStatus( - bytes32 pubkeyHash - ) external view returns (VALIDATOR_STATUS); + function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS); /// @notice Returns the validator status for a given validator pubkey - function validatorStatus( - bytes calldata validatorPubkey - ) external view returns (VALIDATOR_STATUS); + function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS); /// @notice Number of validators with proven withdrawal credentials, who do not have proven full withdrawals function activeValidatorCount() external view returns (uint256); @@ -487,17 +469,13 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// - The final partial withdrawal for an exited validator will be likely be included in this mapping. /// i.e. if a validator was last checkpointed at 32.1 ETH before exiting, the next checkpoint will calculate their /// "exited" amount to be 32.1 ETH rather than 32 ETH. - function checkpointBalanceExitedGwei( - uint64 - ) external view returns (uint64); + function checkpointBalanceExitedGwei(uint64) external view returns (uint64); /// @notice Query the 4788 oracle to get the parent block root of the slot with the given `timestamp` /// @param timestamp of the block for which the parent block root will be returned. MUST correspond /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method /// will revert. - function getParentBlockRoot( - uint64 timestamp - ) external view returns (bytes32); + function getParentBlockRoot(uint64 timestamp) external view returns (bytes32); /// @notice Returns the fee required to add a consolidation request to the EIP-7251 predeploy this block. /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7251#fee-calculation diff --git a/mainnet-contracts/src/interface/IGuardianModule.sol b/mainnet-contracts/src/interface/IGuardianModule.sol index 7958f090..2a435b72 100644 --- a/mainnet-contracts/src/interface/IGuardianModule.sol +++ b/mainnet-contracts/src/interface/IGuardianModule.sol @@ -8,6 +8,7 @@ import { StoppedValidatorInfo } from "../struct/StoppedValidatorInfo.sol"; /** * @title IGuardianModule interface * @author Puffer Finance + * @dev Some of these functions are no longer used since enclaves have been deprecated */ interface IGuardianModule { /** @@ -19,6 +20,7 @@ interface IGuardianModule { /** * @notice Thrown when the RAVE evidence is not valid * @dev Signature "0x2b3c629b" + * @dev DEPRECATED */ error InvalidRAVE(); @@ -64,23 +66,27 @@ interface IGuardianModule { * @param guardianEnclave is the enclave address * @param pubKey is the public key * @dev Signature "0x14720919b20fceff2a396c4973d37c6087e4619d40c8f4003d8e44ee127461a2" + * @dev DEPRECATED */ event RotatedGuardianKey(address guardian, address guardianEnclave, bytes pubKey); /** * @notice Emitted when the mrenclave value is changed * @dev Signature "0x1ff2c57ef9a384cea0c482d61fec8d708967d266f03266e301c6786f7209904a" + * @dev DEPRECATED */ event MrEnclaveChanged(bytes32 oldMrEnclave, bytes32 newMrEnclave); /** * @notice Emitted when the mrsigner value is changed * @dev Signature "0x1a1fe271c5533136fccd1c6df515ca1f227d95822bfe78b9dd93debf3d709ae6" + * @dev DEPRECATED */ event MrSignerChanged(bytes32 oldMrSigner, bytes32 newMrSigner); /** * @notice Returns the enclave address registered to `guardian` + * @dev DEPRECATED */ function getGuardiansEnclaveAddress(address guardian) external view returns (address); @@ -95,6 +101,7 @@ interface IGuardianModule { /** * @notice Sets the values for mrEnclave and mrSigner to `newMrenclave` and `newMrsigner` + * @dev DEPRECATED */ function setGuardianEnclaveMeasurements(bytes32 newMrenclave, bytes32 newMrsigner) external; @@ -109,6 +116,7 @@ interface IGuardianModule { /** * @notice Returns the enclave verifier + * @dev DEPRECATED */ function ENCLAVE_VERIFIER() external view returns (IEnclaveVerifier); @@ -128,6 +136,7 @@ interface IGuardianModule { * @notice Validates the node provisioning calldata * @dev The order of the signatures is important * The order of the signatures MUST the same as the order of the guardians in the guardian module + * @dev DEPRECATED * @param pufferModuleIndex is the validator index in Puffer * @param pubKey The public key * @param signature The signature @@ -198,6 +207,7 @@ interface IGuardianModule { /** * @dev Validates the signatures of the guardians' enclave signatures + * @dev DEPRECATED * @param enclaveSignatures The array of enclave signatures * @param signedMessageHash The hash of the signed message * @return A boolean indicating whether the signatures are valid @@ -221,6 +231,7 @@ interface IGuardianModule { /** * @notice Rotates guardian's key * @dev If he caller is not a valid guardian or if the RAVE evidence is not valid the tx will revert + * @dev DEPRECATED * @param blockNumber is the block number * @param pubKey is the public key of the new signature * @param evidence is the RAVE evidence @@ -229,11 +240,13 @@ interface IGuardianModule { /** * @notice Returns the guardians enclave addresses + * @dev DEPRECATED */ function getGuardiansEnclaveAddresses() external view returns (address[] memory); /** * @notice Returns the guardians enclave public keys + * @dev DEPRECATED */ function getGuardiansEnclavePubkeys() external view returns (bytes[] memory); @@ -246,11 +259,13 @@ interface IGuardianModule { /** * @notice Returns the mrenclave value + * @dev DEPRECATED */ function getMrenclave() external view returns (bytes32); /** * @notice Returns the mrsigner value + * @dev DEPRECATED */ function getMrsigner() external view returns (bytes32); } diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index f6a87a69..f4752c92 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -145,12 +145,9 @@ interface IPufferProtocol { * @param pubKey is the validator public key * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain * @param moduleName is the staking Module - * @param usingEnclave is indicating if the validator is using secure enclave - * @dev Signature "0xc73344cf227e056eee8d82aee54078c9b55323b61d17f61587eb570873f8e319" + * @dev Signature "0x6b9febc68231d6c196b22b02f442fa6dc3148ee90b6e83d5b978c11833587159" */ - event ValidatorKeyRegistered( - bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, bool usingEnclave - ); + event ValidatorKeyRegistered(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName); /** * @notice Emitted when the Validator exited and stopped validating @@ -278,14 +275,10 @@ interface IPufferProtocol { function getModuleAddress(bytes32 moduleName) external view returns (address); /** - * @notice Provisions the next node that is in line for provisioning if the `guardianEnclaveSignatures` are valid + * @notice Provisions the next node that is in line for provisioning * @dev You can check who is next for provisioning by calling `getNextValidatorToProvision` method */ - function provisionNode( - bytes[] calldata guardianEnclaveSignatures, - bytes calldata validatorSignature, - bytes32 depositRootHash - ) external; + function provisionNode(bytes calldata validatorSignature, bytes32 depositRootHash) external; /** * @notice Returns the deposit_data_root diff --git a/mainnet-contracts/src/struct/ValidatorKeyData.sol b/mainnet-contracts/src/struct/ValidatorKeyData.sol index 78512fbd..cad91953 100644 --- a/mainnet-contracts/src/struct/ValidatorKeyData.sol +++ b/mainnet-contracts/src/struct/ValidatorKeyData.sol @@ -10,5 +10,4 @@ struct ValidatorKeyData { bytes32 depositDataRoot; bytes[] blsEncryptedPrivKeyShares; bytes blsPubKeySet; - bytes raveEvidence; } diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index 5c9cc944..b5137fa1 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -499,10 +499,7 @@ contract PufferProtocolHandler is Test { ProvisioningData memory validatorData = _validatorQueue[moduleName][nextIdx]; if (validatorData.status == Status.PENDING) { - bytes memory sig = _getPubKey(validatorData.pubKeypart); - - bytes[] memory signatures = _getGuardianSignatures(sig); - pufferProtocol.provisionNode(signatures, mockValidatorSignature, bytes32(0)); + pufferProtocol.provisionNode(mockValidatorSignature, bytes32(0)); ghost_validators_validating.push(ProvisionedValidator({ moduleName: moduleName, idx: nextIdx })); @@ -553,9 +550,8 @@ contract PufferProtocolHandler is Test { withdrawalCredentials: withdrawalCredentials }), blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48), - raveEvidence: new bytes(1) // Guardians are checking it off chain - }); + blsPubKeySet: new bytes(48) + }); return validatorData; } @@ -587,7 +583,7 @@ contract PufferProtocolHandler is Test { uint256 bond = 1 ether; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, true); + emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( validatorKeyData, moduleName, emptyPermit, emptyPermit ); @@ -595,49 +591,6 @@ contract PufferProtocolHandler is Test { return (smoothingCommitment + bond); } - // Copied from PufferProtocol.t.sol - function _getGuardianSignatures(bytes memory pubKey) internal view returns (bytes[] memory) { - (bytes32 moduleName, uint256 pendingIdx) = pufferProtocol.getNextValidatorToProvision(); - Validator memory validator = pufferProtocol.getValidatorInfo(moduleName, pendingIdx); - // If there is no module return empty byte array - if (validator.module == address(0)) { - return new bytes[](0); - } - bytes memory withdrawalCredentials = pufferProtocol.getWithdrawalCredentials(validator.module); - - bytes32 digest = LibGuardianMessages._getBeaconDepositMessageToBeSigned( - pendingIdx, - pubKey, - mockValidatorSignature, - withdrawalCredentials, - pufferProtocol.getDepositDataRoot({ - pubKey: pubKey, - signature: mockValidatorSignature, - withdrawalCredentials: withdrawalCredentials - }) - ); - - return _getGuardianEnclaveSignatures(digest); - } - - function _getGuardianEnclaveSignatures(bytes32 digest) internal view returns (bytes[] memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SKEnclave, digest); - bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - (v, r, s) = vm.sign(guardian2SKEnclave, digest); - bytes memory signature2 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - (v, r, s) = vm.sign(guardian3SKEnclave, digest); - bytes memory signature3 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - bytes[] memory guardianSignatures = new bytes[](3); - guardianSignatures[0] = signature1; - guardianSignatures[1] = signature2; - guardianSignatures[2] = signature3; - - return guardianSignatures; - } - function _getGuardianEOASignatures(bytes32 digest) internal returns (bytes[] memory) { // Create Guardian wallets (, uint256 guardian1SK) = makeAddrAndKey("guardian1"); diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index bff105a3..f17be577 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -20,7 +20,7 @@ import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol" contract PufferProtocolTest is UnitTestHelper { using ECDSA for bytes32; - event ValidatorKeyRegistered(bytes pubKey, uint256 indexed, bytes32 indexed, bool); + event ValidatorKeyRegistered(bytes pubKey, uint256 indexed, bytes32 indexed); event SuccessfullyProvisioned(bytes pubKey, uint256 indexed, bytes32 indexed); event ModuleWeightsChanged(bytes32[] oldWeights, bytes32[] newWeights); @@ -132,11 +132,9 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(moduleName, PUFFER_MODULE_0, "module"); assertEq(idx, 1, "idx should be 1"); - bytes[] memory signatures = _getGuardianSignatures(_getPubKey(bytes32("bob"))); - vm.expectEmit(true, true, true, true); emit SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); moduleSelectionIndex = pufferProtocol.getModuleSelectIndex(); assertEq(moduleSelectionIndex, 1, "module idx changed"); } @@ -190,7 +188,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq( validatorTicket.balanceOf(address(pufferProtocol)), - ((amount - 1 ether) * 1 ether) / vtPrice, + ((amount - 2 ether) * 1 ether) / vtPrice, "VT after for pufferProtocol" ); } @@ -226,7 +224,7 @@ contract PufferProtocolTest is UnitTestHelper { // Set validator limit and try registering that many validators function test_fuzz_register_many_validators(uint8 numberOfValidatorsToProvision) external { for (uint256 i = 0; i < uint256(numberOfValidatorsToProvision); ++i) { - vm.deal(address(this), 2 ether); + vm.deal(address(this), 3 ether); _registerValidatorKey(bytes32(i), PUFFER_MODULE_0); } } @@ -249,12 +247,11 @@ contract PufferProtocolTest is UnitTestHelper { signature: new bytes(0), depositDataRoot: bytes32(""), blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48), - raveEvidence: new bytes(0) // No rave - }); + blsPubKeySet: new bytes(48) + }); vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, false); + emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey{ value: vtPrice + 2 ether }( validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit ); @@ -276,8 +273,7 @@ contract PufferProtocolTest is UnitTestHelper { signature: new bytes(0), depositDataRoot: bytes32(""), blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(144), - raveEvidence: new bytes(1) + blsPubKeySet: new bytes(144) }); vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); @@ -287,13 +283,8 @@ contract PufferProtocolTest is UnitTestHelper { } function test_get_payload() public view { - (bytes[] memory guardianPubKeys,, uint256 threshold,) = pufferProtocol.getPayload(PUFFER_MODULE_0, false); + (, uint256 threshold,) = pufferProtocol.getPayload(PUFFER_MODULE_0); - assertEq(guardianPubKeys[0], guardian1EnclavePubKey, "guardian1"); - assertEq(guardianPubKeys[1], guardian2EnclavePubKey, "guardian2"); - assertEq(guardianPubKeys[2], guardian3EnclavePubKey, "guardian3"); - - assertEq(guardianPubKeys.length, 3, "pubkeys len"); assertEq(threshold, 1, "threshold"); } @@ -302,12 +293,8 @@ contract PufferProtocolTest is UnitTestHelper { (, uint256 idx) = pufferProtocol.getNextValidatorToProvision(); assertEq(type(uint256).max, idx, "module"); - // Invalid signatures - bytes[] memory signatures = - _getGuardianSignatures(hex"0000000000000000000000000000000000000000000000000000000000000000"); - vm.expectRevert(); // panic - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } // If the deposit root is not bytes(0), it must match match the one returned from the beacon contract @@ -315,13 +302,12 @@ contract PufferProtocolTest is UnitTestHelper { _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); bytes memory validatorSignature = _validatorSignature(); - bytes[] memory guardianSignatures = _getGuardianSignatures(_getPubKey(zeroPubKeyPart)); vm.expectRevert(IPufferProtocol.InvalidDepositRootHash.selector); - pufferProtocol.provisionNode(guardianSignatures, validatorSignature, bytes32("badDepositRoot")); // "depositRoot" is hardcoded in the mock + pufferProtocol.provisionNode(validatorSignature, bytes32("badDepositRoot")); // "depositRoot" is hardcoded in the mock // now it works - pufferProtocol.provisionNode(guardianSignatures, validatorSignature, DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(validatorSignature, DEFAULT_DEPOSIT_ROOT); } function test_register_multiple_validators_and_skipProvisioning(bytes32 alicePubKeyPart, bytes32 bobPubKeyPart) @@ -358,19 +344,15 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(pufferProtocol.getPendingValidatorIndex(PUFFER_MODULE_0), 5, "next pending validator index"); - bytes[] memory signatures = _getGuardianSignatures(zeroPubKey); - // 1. provision zero key vm.expectEmit(true, true, true, true); emit SuccessfullyProvisioned(zeroPubKey, 0, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); - - bytes[] memory bobSignatures = _getGuardianSignatures(bobPubKey); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Provision Bob that is not zero pubKey vm.expectEmit(true, true, true, true); emit SuccessfullyProvisioned(bobPubKey, 1, PUFFER_MODULE_0); - pufferProtocol.provisionNode(bobSignatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); Validator memory bobValidator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 1); @@ -378,10 +360,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); - signatures = _getGuardianSignatures(zeroPubKey); - emit SuccessfullyProvisioned(zeroPubKey, 3, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Get validators Validator[] memory registeredValidators = pufferProtocol.getValidators(PUFFER_MODULE_0); @@ -427,12 +407,10 @@ contract PufferProtocolTest is UnitTestHelper { assertTrue(nextModule == PUFFER_MODULE_0, "module selection"); assertTrue(nextId == 0, "module selection"); - bytes[] memory signatures = _getGuardianSignatures(_getPubKey(bytes32("bob"))); - // Provision Bob that is not zero pubKey vm.expectEmit(true, true, true, true); emit SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 0, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -440,11 +418,9 @@ contract PufferProtocolTest is UnitTestHelper { // Id is zero, because that is the first in this queue assertTrue(nextId == 0, "module id"); - signatures = _getGuardianSignatures(_getPubKey(bytes32("benjamin"))); - vm.expectEmit(true, true, true, true); emit SuccessfullyProvisioned(_getPubKey(bytes32("benjamin")), 0, EIGEN_DA); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -464,23 +440,18 @@ contract PufferProtocolTest is UnitTestHelper { assertTrue(nextId == 1, "module id"); // Provisioning of rocky should fail, because jason is next in line - signatures = _getGuardianSignatures(_getPubKey(bytes32("rocky"))); vm.expectRevert(Unauthorized.selector); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); - - signatures = _getGuardianSignatures(_getPubKey(bytes32("jason"))); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Provision Jason - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); - signatures = _getGuardianSignatures(_getPubKey(bytes32("rocky"))); - // Rocky is now in line assertTrue(nextModule == CRAZY_GAINS, "module selection"); assertTrue(nextId == 0, "module id"); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -491,11 +462,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.getNextValidatorToBeProvisionedIndex(PUFFER_MODULE_0), 1, "next idx for no restaking module" ); - signatures = _getGuardianSignatures(_getPubKey(bytes32("alice"))); - vm.expectEmit(true, true, true, true); emit SuccessfullyProvisioned(_getPubKey(bytes32("alice")), 1, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } function test_create_puffer_module() public { @@ -561,7 +530,7 @@ contract PufferProtocolTest is UnitTestHelper { // Register validator key by paying SC in ETH and depositing bond in pufETH vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); + emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey{ value: sc }(data, PUFFER_MODULE_0, permit, emptyPermit); // Alice has some dust in her wallet, because VT purchase changes the exchange rate, meaning pufETH is worth more assertEq(pufferVault.balanceOf(alice), 1696417975049392, "1696417975049392 pufETH after for alice"); @@ -596,7 +565,7 @@ contract PufferProtocolTest is UnitTestHelper { // Register validator key by paying SC in ETH and depositing bond in pufETH vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); + emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey{ value: sc }(data, PUFFER_MODULE_0, permit, emptyPermit); // Alice has some dust in her wallet, because VT purchase changes the exchange rate, meaning pufETH is worth more @@ -644,7 +613,7 @@ contract PufferProtocolTest is UnitTestHelper { ); vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); + emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey(data, PUFFER_MODULE_0, pufETHPermit, vtPermit); assertEq(pufferVault.balanceOf(alice), leftOverPufETH, "alice should have some leftover pufETH"); @@ -687,7 +656,7 @@ contract PufferProtocolTest is UnitTestHelper { pufETHPermit.amount = pufferVault.convertToShares(bond); vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); + emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey(data, PUFFER_MODULE_0, pufETHPermit, vtPermit); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH after for alice"); @@ -724,7 +693,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 bond = 1 ether; vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); + emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey{ value: bond }(data, PUFFER_MODULE_0, emptyPermit, permit); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH after for alice"); @@ -755,10 +724,9 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(address(pufferVault), 100 ether); _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - bytes[] memory guardianSignatures = _getGuardianSignatures(_getPubKey(bytes32("alice"))); // Register and provision Alice // Alice may be an active validator or it can be exited, doesn't matter - pufferProtocol.provisionNode(guardianSignatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Register another validator with using the same data _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); @@ -766,7 +734,7 @@ contract PufferProtocolTest is UnitTestHelper { // Try to provision it with the original message (replay attack) // It should revert vm.expectRevert(Unauthorized.selector); - pufferProtocol.provisionNode(guardianSignatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } function test_validator_limit_per_module() external { @@ -792,7 +760,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 startTimestamp = 1707411226; // Alice registers one validator and we provision it - vm.deal(alice, 2 ether); + vm.deal(alice, 3 ether); vm.deal(NoRestakingModule, 200 ether); vm.startPrank(alice); @@ -801,13 +769,13 @@ contract PufferProtocolTest is UnitTestHelper { assertApproxEqAbs( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, + 2 ether, 1, - "~1 pufETH in protocol" + "~2 pufETH in protocol" ); // bond + something for the validator registration - assertEq(address(pufferVault).balance, 1001.2835 ether, "vault eth balance"); + assertEq(address(pufferVault).balance, 1002.2835 ether, "vault eth balance"); Validator memory validator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 0); @@ -815,9 +783,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Didn't claim the bond yet assertEq(pufferVault.balanceOf(alice), 0, "alice has zero pufETH"); @@ -842,7 +808,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(pufferVault.balanceOf(alice), validator.bond, "alice got the pufETH"); // 1 wei diff assertApproxEqAbs( - pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1 ether, 1, "assets owned by alice" + pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 2 ether, 1, "assets owned by alice" ); // Alice doesn't withdraw her VT's right away @@ -1122,7 +1088,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 30 ether, "protocol has 30 VT"); assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 1 ether, 1, "protocol should have ~1 eth bond" + _getUnderlyingETHAmount(address(pufferProtocol)), 2 ether, 1, "protocol should have ~2 eth bond" ); vm.startPrank(alice); @@ -1152,7 +1118,7 @@ contract PufferProtocolTest is UnitTestHelper { _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" ); - assertApproxEqAbs(_getUnderlyingETHAmount(address(alice)), 1 ether, 1, "alice got back the bond"); + assertApproxEqAbs(_getUnderlyingETHAmount(address(alice)), 2 ether, 1, "alice got back the bond"); // We've removed the validator data, meaning the validator status is 0 (UNINITIALIZED) vm.expectRevert(abi.encodeWithSelector(IPufferProtocol.InvalidValidatorState.selector, 0)); @@ -1245,9 +1211,9 @@ contract PufferProtocolTest is UnitTestHelper { ); // Alice got more because she earned the rewards from Bob's registration - assertGe(_getUnderlyingETHAmount(address(alice)), 1 ether, "alice got back the bond gt"); + assertGe(_getUnderlyingETHAmount(address(alice)), 2 ether, "alice got back the bond gt"); - assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 1 ether, 1, "bob got back the bond"); + assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 2 ether, 1, "bob got back the bond"); } // Batch claim of different amounts @@ -1414,9 +1380,9 @@ contract PufferProtocolTest is UnitTestHelper { ); // Alice got more because she earned the rewards from Bob's registration - assertGe(_getUnderlyingETHAmount(address(alice)), 1 ether, "alice got back the bond gt"); + assertGe(_getUnderlyingETHAmount(address(alice)), 2 ether, "alice got back the bond gt"); - assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 1 ether, 1, "bob got back the bond"); + assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 2 ether, 1, "bob got back the bond"); } function test_batch_vs_multiple_single_withdrawals() public { @@ -1466,9 +1432,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Give funds to modules vm.deal(NoRestakingModule, 200 ether); @@ -1520,9 +1484,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.deal(NoRestakingModule, 200 ether); @@ -1572,9 +1534,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.deal(NoRestakingModule, 200 ether); @@ -1632,9 +1592,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.deal(NoRestakingModule, 200 ether); @@ -1684,9 +1642,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.deal(NoRestakingModule, 200 ether); @@ -1727,9 +1683,7 @@ contract PufferProtocolTest is UnitTestHelper { _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); vm.stopPrank(); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Alice exited after 1 day _executeFullWithdrawal( @@ -1757,9 +1711,7 @@ contract PufferProtocolTest is UnitTestHelper { _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); vm.stopPrank(); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.startPrank(DAO); pufferProtocol.changeMinimumVTAmount(35 ether); @@ -1784,29 +1736,31 @@ contract PufferProtocolTest is UnitTestHelper { // User purchases a lot of VT using ETH, but uses Permit to transfer pufETH function test_purchase_big_amount_of_vt() public { bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); + vm.deal(alice, 11 ether); - // Alice mints 1 ETH of pufETH + // Alice mints 2 ETH of pufETH vm.startPrank(alice); - pufferVault.depositETH{ value: 1 ether }(alice); + pufferVault.depositETH{ value: 2 ether }(alice); assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 1 ether, "1 pufETH before for alice"); + assertEq(pufferVault.balanceOf(alice), 2 ether, "1 pufETH before for alice"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - // Generate Permit data for 1 pufETH to the protocol + // Generate Permit data for 2 pufETH to the protocol Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), 1 ether, block.timestamp), pufferVault.DOMAIN_SEPARATOR() + _testTemps("alice", address(pufferProtocol), 2 ether, block.timestamp), pufferVault.DOMAIN_SEPARATOR() ); // Register validator key by paying SC in ETH and depositing bond in pufETH vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); + emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey{ value: 9 ether }(data, PUFFER_MODULE_0, permit, emptyPermit); // Because alice purchased VT in the registration TX, it modified the exchange rate and we take less pufETH from her. - assertEq(pufferVault.balanceOf(alice), 8424921124709635, "alice has 8424921124709635 pufETH after registering"); + assertEq( + pufferVault.balanceOf(alice), 16833167574628528, "alice has 16833167574628528 pufETH after registering" + ); } // Alice uses Permit for VT and pays for the bond with ETH, but sends more ETH than needed @@ -1854,45 +1808,6 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(validatorTicket.balanceOf(bob), 50 ether, "bob got the VT"); } - function _getGuardianSignatures(bytes memory pubKey) internal view returns (bytes[] memory) { - (bytes32 moduleName, uint256 pendingIdx) = pufferProtocol.getNextValidatorToProvision(); - Validator memory validator = pufferProtocol.getValidatorInfo(moduleName, pendingIdx); - // If there is no module return empty byte array - if (validator.module == address(0)) { - return new bytes[](0); - } - bytes memory withdrawalCredentials = pufferProtocol.getWithdrawalCredentials(validator.module); - - bytes32 digest = LibGuardianMessages._getBeaconDepositMessageToBeSigned( - pendingIdx, - pubKey, - _validatorSignature(), - withdrawalCredentials, - pufferProtocol.getDepositDataRoot({ - pubKey: pubKey, - signature: _validatorSignature(), - withdrawalCredentials: withdrawalCredentials - }) - ); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SKEnclave, digest); - bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - (v, r, s) = vm.sign(guardian2SKEnclave, digest); - (v, r, s) = vm.sign(guardian3SKEnclave, digest); - bytes memory signature2 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - (v, r, s) = vm.sign(guardian3SKEnclave, digest); - bytes memory signature3 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - bytes[] memory guardianSignatures = new bytes[](3); - guardianSignatures[0] = signature1; - guardianSignatures[1] = signature2; - guardianSignatures[2] = signature3; - - return guardianSignatures; - } - function _getGuardianSignaturesForSkipping() internal view returns (bytes[] memory) { (bytes32 moduleName, uint256 pendingIdx) = pufferProtocol.getNextValidatorToProvision(); @@ -1975,9 +1890,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalCredentials: withdrawalCredentials }), blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48), - raveEvidence: bytes("mock rave") // Guardians are checking it off chain - }); + blsPubKeySet: new bytes(48) + }); return validatorData; } @@ -2013,18 +1927,18 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, moduleName); uint256 idx = pufferProtocol.getPendingValidatorIndex(moduleName); - uint256 bond = 1 ether; + uint256 bond = 2 ether; // Empty permit means that the node operator is paying with ETH for both bond & VT in the registration transaction vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, idx, moduleName, true); + emit ValidatorKeyRegistered(pubKey, idx, moduleName); pufferProtocol.registerValidatorKey{ value: (vtPrice + bond) }( validatorKeyData, moduleName, emptyPermit, emptyPermit ); } /** - * @dev Registers and provisions a new validator with 1 ETH bond (enclave) and 30 VTs (see _registerValidatorKey) + * @dev Registers and provisions a new validator with 2 ETH bond and 30 VTs (see _registerValidatorKey) */ function _registerAndProvisionNode(bytes32 pubKeyPart, bytes32 moduleName, address nodeOperator) internal { vm.deal(nodeOperator, 10 ether); @@ -2033,9 +1947,7 @@ contract PufferProtocolTest is UnitTestHelper { _registerValidatorKey(pubKeyPart, moduleName); vm.stopPrank(); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(pubKeyPart)), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } /** From 03e6b6db2208e41150ae0bbbf1fe515ac9e51a63 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 16 May 2025 10:31:48 +0200 Subject: [PATCH 04/82] Changed unused enclave variables to deprecated. Removed unused code --- ...GenerateBLSKeysAndRegisterValidators.s.sol | 5 +-- mainnet-contracts/src/PufferProtocol.sol | 10 ------ .../src/interface/IPufferProtocol.sol | 12 ------- .../src/struct/ValidatorKeyData.sol | 5 +-- .../test/handlers/PufferProtocolHandler.sol | 5 +-- .../test/unit/PufferProtocol.t.sol | 33 +++++-------------- 6 files changed, 18 insertions(+), 52 deletions(-) diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index 97c6c690..d110e974 100644 --- a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol +++ b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol @@ -102,8 +102,9 @@ contract GenerateBLSKeysAndRegisterValidators is Script { blsPubKey: stdJson.readBytes(registrationJson, ".bls_pub_key"), signature: stdJson.readBytes(registrationJson, ".signature"), depositDataRoot: stdJson.readBytes32(registrationJson, ".deposit_data_root"), - blsEncryptedPrivKeyShares: blsEncryptedPrivKeyShares, - blsPubKeySet: stdJson.readBytes(registrationJson, ".bls_pub_key_set") + deprecated_blsEncryptedPrivKeyShares: new bytes[](3), + deprecated_blsPubKeySet: new bytes(48), + deprecated_raveEvidence: new bytes(0) }); Permit memory pufETHPermit = _signPermit({ diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 2cf3bfd0..7549e856 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -706,16 +706,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad if (data.blsPubKey.length != _BLS_PUB_KEY_LENGTH) { revert InvalidBLSPubKey(); } - - // Every guardian needs to receive a share - if (data.blsEncryptedPrivKeyShares.length != GUARDIAN_MODULE.getGuardians().length) { - revert InvalidBLSPrivateKeyShares(); - } - - // blsPubKeySet is for a subset of guardians and because of that we use .getThreshold() - if (data.blsPubKeySet.length != (GUARDIAN_MODULE.getThreshold() * _BLS_PUB_KEY_LENGTH)) { - revert InvalidBLSPublicKeySet(); - } } function _changeMinimumVTAmount(uint256 newMinimumVtAmount) internal { diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index f4752c92..d28c83c7 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -26,12 +26,6 @@ interface IPufferProtocol { */ error InvalidDepositRootHash(); - /** - * @notice Thrown when the number of BLS public key shares doesn't match guardians threshold number - * @dev Signature "0x8cdea6a6" - */ - error InvalidBLSPublicKeySet(); - /** * @notice Thrown when the node operator tries to withdraw VTs from the PufferProtocol but has active/pending validators * @dev Signature "0x22242546" @@ -50,12 +44,6 @@ interface IPufferProtocol { */ error ValidatorLimitForModuleReached(); - /** - * @notice Thrown when the number of BLS private key shares doesn't match guardians number - * @dev Signature "0x2c8f9aa3" - */ - error InvalidBLSPrivateKeyShares(); - /** * @notice Thrown when the BLS public key is not valid * @dev Signature "0x7eef7967" diff --git a/mainnet-contracts/src/struct/ValidatorKeyData.sol b/mainnet-contracts/src/struct/ValidatorKeyData.sol index cad91953..40726ba8 100644 --- a/mainnet-contracts/src/struct/ValidatorKeyData.sol +++ b/mainnet-contracts/src/struct/ValidatorKeyData.sol @@ -8,6 +8,7 @@ struct ValidatorKeyData { bytes blsPubKey; bytes signature; bytes32 depositDataRoot; - bytes[] blsEncryptedPrivKeyShares; - bytes blsPubKeySet; + bytes[] deprecated_blsEncryptedPrivKeyShares; + bytes deprecated_blsPubKeySet; + bytes deprecated_raveEvidence; } diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index b5137fa1..99f53d9f 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -549,8 +549,9 @@ contract PufferProtocolHandler is Test { signature: mockValidatorSignature, withdrawalCredentials: withdrawalCredentials }), - blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48) + deprecated_blsEncryptedPrivKeyShares: new bytes[](3), + deprecated_blsPubKeySet: new bytes(48), + deprecated_raveEvidence: new bytes(0) }); return validatorData; diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index f17be577..6f8e0be7 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -146,24 +146,6 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.createPufferModule(PUFFER_MODULE_0); } - // Invalid pub key shares length - function test_register_invalid_pubkey_shares_length() public { - ValidatorKeyData memory data = _getMockValidatorKeyData(new bytes(48), PUFFER_MODULE_0); - data.blsPubKeySet = new bytes(22); // Invalid length - - vm.expectRevert(IPufferProtocol.InvalidBLSPublicKeySet.selector); - pufferProtocol.registerValidatorKey{ value: 4 ether }(data, PUFFER_MODULE_0, emptyPermit, emptyPermit); - } - - // Invalid private key shares length - function test_register_invalid_privKey_shares() public { - ValidatorKeyData memory data = _getMockValidatorKeyData(new bytes(48), PUFFER_MODULE_0); - data.blsEncryptedPrivKeyShares = new bytes[](2); // we have 3 guardians, and we try to give 2 priv key shares - - vm.expectRevert(IPufferProtocol.InvalidBLSPrivateKeyShares.selector); - pufferProtocol.registerValidatorKey{ value: 4 ether }(data, PUFFER_MODULE_0, emptyPermit, emptyPermit); - } - // Try registering with invalid module function test_register_to_invalid_module() public { uint256 smoothingCommitment = pufferOracle.getValidatorTicketPrice() * 30; @@ -246,8 +228,9 @@ contract PufferProtocolTest is UnitTestHelper { blsPubKey: pubKey, // key length must be 48 byte signature: new bytes(0), depositDataRoot: bytes32(""), - blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48) + deprecated_blsEncryptedPrivKeyShares: new bytes[](3), + deprecated_blsPubKeySet: new bytes(48), + deprecated_raveEvidence: new bytes(0) }); vm.expectEmit(true, true, true, true); @@ -272,8 +255,9 @@ contract PufferProtocolTest is UnitTestHelper { blsPubKey: hex"aeaa", // invalid key signature: new bytes(0), depositDataRoot: bytes32(""), - blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(144) + deprecated_blsEncryptedPrivKeyShares: new bytes[](3), + deprecated_blsPubKeySet: new bytes(48), + deprecated_raveEvidence: new bytes(0) }); vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); @@ -1889,8 +1873,9 @@ contract PufferProtocolTest is UnitTestHelper { signature: validatorSignature, withdrawalCredentials: withdrawalCredentials }), - blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48) + deprecated_blsEncryptedPrivKeyShares: new bytes[](3), + deprecated_blsPubKeySet: new bytes(48), + deprecated_raveEvidence: new bytes(0) }); return validatorData; From 0a81b7ec1af757ab1cd64e4d24a7acc9fa767123 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 16 May 2025 10:33:53 +0200 Subject: [PATCH 05/82] Removed getPayload function --- mainnet-contracts/src/PufferProtocol.sol | 13 ------------- mainnet-contracts/test/unit/PufferProtocol.t.sol | 6 ------ 2 files changed, 19 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 7549e856..12caaeb4 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -597,19 +597,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad return $.minimumVtAmount; } - /** - * @notice Returns necessary information to make Guardian's life easier - */ - function getPayload(bytes32 moduleName) external view returns (bytes memory, uint256, uint256) { - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - bytes memory withdrawalCredentials = getWithdrawalCredentials(address($.modules[moduleName])); - uint256 threshold = GUARDIAN_MODULE.getThreshold(); - uint256 ethAmount = VALIDATOR_BOND + ($.minimumVtAmount * PUFFER_ORACLE.getValidatorTicketPrice()) / 1 ether; - - return (withdrawalCredentials, threshold, ethAmount); - } - /** * @inheritdoc IPufferProtocol */ diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 6f8e0be7..38687100 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -266,12 +266,6 @@ contract PufferProtocolTest is UnitTestHelper { ); } - function test_get_payload() public view { - (, uint256 threshold,) = pufferProtocol.getPayload(PUFFER_MODULE_0); - - assertEq(threshold, 1, "threshold"); - } - // Try to provision a validator when there is nothing to provision function test_provision_reverts() public { (, uint256 idx) = pufferProtocol.getNextValidatorToProvision(); From bddc3495b821770244e6f50c588eca4b2d883e12 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 16 May 2025 16:02:22 +0200 Subject: [PATCH 06/82] Adapted remaining failing tests --- .../test/unit/PufferProtocol.t.sol | 124 +++++++----------- 1 file changed, 51 insertions(+), 73 deletions(-) diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 38687100..40f723d7 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -417,10 +417,6 @@ contract PufferProtocolTest is UnitTestHelper { assertTrue(nextModule == EIGEN_DA, "module selection"); assertTrue(nextId == 1, "module id"); - // Provisioning of rocky should fail, because jason is next in line - vm.expectRevert(Unauthorized.selector); - pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); - // Provision Jason pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); @@ -486,16 +482,16 @@ contract PufferProtocolTest is UnitTestHelper { uint256 expectedMint = pufferVault.previewDeposit(1 ether); assertGt(expectedMint, 0, "should expect more pufETH"); - // Alice mints 1 ETH of pufETH + // Alice mints 2 ETH of pufETH vm.startPrank(alice); - uint256 minted = pufferVault.depositETH{ value: 1 ether }(alice); + uint256 minted = pufferVault.depositETH{ value: 2 ether }(alice); assertGt(minted, 0, "should mint pufETH"); // approve pufETH to pufferProtocol pufferVault.approve(address(pufferProtocol), type(uint256).max); assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 1 ether, "1 pufETH before for alice"); + assertEq(pufferVault.balanceOf(alice), 2 ether, "2 pufETH before for alice"); // In this case, the only important data on permit is the amount // Permit call will fail, but the amount is reused @@ -511,11 +507,11 @@ contract PufferProtocolTest is UnitTestHelper { emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey{ value: sc }(data, PUFFER_MODULE_0, permit, emptyPermit); // Alice has some dust in her wallet, because VT purchase changes the exchange rate, meaning pufETH is worth more - assertEq(pufferVault.balanceOf(alice), 1696417975049392, "1696417975049392 pufETH after for alice"); + assertEq(pufferVault.balanceOf(alice), 3389455624732864, "3389455624732864 pufETH after for alice"); uint256 protocolPufETHBalance = pufferVault.balanceOf(address(pufferProtocol)); - assertApproxEqRel(protocolPufETHBalance, 0.998303582024950608 ether, pointZeroZeroTwo, "~0.998 pufETH after"); + assertApproxEqRel(protocolPufETHBalance, 1.996607164 ether, pointZeroZeroTwo, "~1.996 pufETH after"); assertApproxEqRel( - pufferVault.convertToAssets(protocolPufETHBalance), 1 ether, pointZeroZeroOne, "1 ETH worth of pufETH after" + pufferVault.convertToAssets(protocolPufETHBalance), 2 ether, pointZeroZeroOne, "2 ETH worth of pufETH after" ); } @@ -524,12 +520,12 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory pubKey = _getPubKey(bytes32("alice")); vm.deal(alice, 10 ether); - // Alice mints 1 ETH of pufETH + // Alice mints 2 ETH of pufETH vm.startPrank(alice); - pufferVault.depositETH{ value: 1 ether }(alice); + pufferVault.depositETH{ value: 2 ether }(alice); assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 1 ether, "1 pufETH before for alice"); + assertEq(pufferVault.balanceOf(alice), 2 ether, "1 pufETH before for alice"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); // Generate Permit data for 2 pufETH to the protocol @@ -547,12 +543,12 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.registerValidatorKey{ value: sc }(data, PUFFER_MODULE_0, permit, emptyPermit); // Alice has some dust in her wallet, because VT purchase changes the exchange rate, meaning pufETH is worth more - assertEq(pufferVault.balanceOf(alice), 1696417975049392, "1696417975049392 pufETH after for alice"); + assertEq(pufferVault.balanceOf(alice), 3389455624732864, "3389455624732864 pufETH after for alice"); uint256 protocolPufETHBalance = pufferVault.balanceOf(address(pufferProtocol)); - assertEq(protocolPufETHBalance, 0.998303582024950608 ether, "~0.99 pufETH after"); + assertEq(protocolPufETHBalance, 1.996610544375267136 ether, "~1.99 pufETH after"); assertApproxEqRel( - pufferVault.convertToAssets(protocolPufETHBalance), 1 ether, pointZeroZeroOne, "1 ETH worth of pufETH after" + pufferVault.convertToAssets(protocolPufETHBalance), 2 ether, pointZeroZeroOne, "1 ETH worth of pufETH after" ); } @@ -564,24 +560,24 @@ contract PufferProtocolTest is UnitTestHelper { uint256 numberOfDays = 200; uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; - // Alice mints 1 ETH of pufETH + // Alice mints 2 ETH of pufETH vm.startPrank(alice); // Purchase pufETH - pufferVault.depositETH{ value: 1 ether }(alice); + pufferVault.depositETH{ value: 2 ether }(alice); // Alice purchases VT validatorTicket.purchaseValidatorTicket{ value: amount }(alice); // Because Alice purchased a lot of VT's, it changed the conversion rate // Because of that the registerValidatorKey will .transferFrom a smaller amount of pufETH - uint256 leftOverPufETH = pufferVault.balanceOf(alice) - pufferVault.convertToShares(1 ether); + uint256 leftOverPufETH = pufferVault.balanceOf(alice) - pufferVault.convertToShares(2 ether); assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 1 ether, "1 pufETH before for alice"); + assertEq(pufferVault.balanceOf(alice), 2 ether, "2 pufETH before for alice"); assertEq(validatorTicket.balanceOf(alice), _upscaleTo18Decimals(numberOfDays), "VT before for alice"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 bond = 1 ether; + uint256 bond = 2 ether; Permit memory pufETHPermit = _signPermit( _testTemps("alice", address(pufferProtocol), bond, block.timestamp), pufferVault.DOMAIN_SEPARATOR() ); @@ -596,7 +592,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(pufferVault.balanceOf(alice), leftOverPufETH, "alice should have some leftover pufETH"); assertEq(validatorTicket.balanceOf(alice), 0, "0 vt after for alice"); - assertApproxEqRel(pufferVault.balanceOf(address(pufferProtocol)), bond, 0.002e18, "1 pufETH after"); + assertApproxEqRel(pufferVault.balanceOf(address(pufferProtocol)), bond, 0.002e18, "2 pufETH after"); } // Node operator can deposit both VT and pufETH with .approve @@ -610,19 +606,19 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); // Alice purchases VT validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - // Alice mints 1 ETH of pufETH - pufferVault.depositETH{ value: 1 ether }(alice); + // Alice mints 2 ETH of pufETH + pufferVault.depositETH{ value: 2 ether }(alice); assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); // 1 wei diff assertApproxEqAbs( - pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1 ether, 1, "1 pufETH before for alice" + pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 2 ether, 1, "2 pufETH before for alice" ); assertEq(validatorTicket.balanceOf(alice), _upscaleTo18Decimals(numberOfDays), "VT before for alice"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 bond = 1 ether; + uint256 bond = 2 ether; pufferVault.approve(address(pufferProtocol), type(uint256).max); validatorTicket.approve(address(pufferProtocol), type(uint256).max); @@ -667,15 +663,14 @@ contract PufferProtocolTest is UnitTestHelper { validatorTicket.DOMAIN_SEPARATOR() ); - // Alice is using SGX - uint256 bond = 1 ether; + uint256 bond = 2 ether; vm.expectEmit(true, true, true, true); emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey{ value: bond }(data, PUFFER_MODULE_0, emptyPermit, permit); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH after for alice"); - assertApproxEqRel(pufferVault.balanceOf(address(pufferProtocol)), 1 ether, pointZeroFive, "~1 pufETH after"); + assertApproxEqRel(pufferVault.balanceOf(address(pufferProtocol)), 2 ether, pointZeroFive, "~2 pufETH after"); } // Node operator can deposit Bond in pufETH @@ -698,23 +693,6 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.registerValidatorKey{ value: 0.1 ether }(data, PUFFER_MODULE_0, permit, emptyPermit); } - function test_validator_griefing_attack() external { - vm.deal(address(pufferVault), 100 ether); - - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - // Register and provision Alice - // Alice may be an active validator or it can be exited, doesn't matter - pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); - - // Register another validator with using the same data - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - - // Try to provision it with the original message (replay attack) - // It should revert - vm.expectRevert(Unauthorized.selector); - pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); - } - function test_validator_limit_per_module() external { _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); @@ -726,7 +704,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 smoothingCommitment = pufferOracle.getValidatorTicketPrice(); bytes memory pubKey = _getPubKey(bytes32("bob")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 bond = 1 ether; + uint256 bond = 2 ether; vm.expectRevert(IPufferProtocol.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( @@ -1406,7 +1384,7 @@ contract PufferProtocolTest is UnitTestHelper { // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1439,14 +1417,14 @@ contract PufferProtocolTest is UnitTestHelper { // Bad dept is shared between all pufETH holders assertApproxEqRel( pufferVault.balanceOf(address(pufferProtocol)), - 1 ether, + 2 ether, pointZeroOne, - "1 ETH worth of pufETH in the protocol" + "2 ETH worth of pufETH in the protocol" ); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); } - // Register 2 validators, provision 1, slash 1.5 whole validator bond owned by node operator + // Register 2 validators, provision 1, slash 2.5 whole validator bond owned by node operator // Case 2 function test_slashing_case_2() public { vm.deal(alice, 10 ether); @@ -1458,7 +1436,7 @@ contract PufferProtocolTest is UnitTestHelper { // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1475,7 +1453,7 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - withdrawalAmount: 30.5 ether, + withdrawalAmount: 29.5 ether, wasSlashed: true }); @@ -1489,14 +1467,14 @@ contract PufferProtocolTest is UnitTestHelper { // Bad dept is shared between all pufETH holders assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, + 2 ether, pointZeroOne, - "1 ether ETH worth of pufETH in the protocol" + "2 ether ETH worth of pufETH in the protocol" ); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); } - // Register 2 validators, provision 1, slash 1 whole validator bond (1 ETH) + // Register 2 validators, provision 1, slash 1 whole validator bond (2 ETH) // Case 3 function test_slashing_case_3() public { vm.deal(alice, 10 ether); @@ -1508,7 +1486,7 @@ contract PufferProtocolTest is UnitTestHelper { // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1525,7 +1503,7 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - withdrawalAmount: 31 ether, + withdrawalAmount: 30 ether, wasSlashed: true }); @@ -1539,17 +1517,17 @@ contract PufferProtocolTest is UnitTestHelper { // 1 ETH gives you less pufETH after the `retrieveBond` call, meaning it is better than before (slightly) assertGt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); - // Alice has a little over 1 ETH because she earned something for paying the VT on the second validator registration + // Alice has a little over 2 ETH because she earned something for paying the VT on the second validator registration assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, + 2 ether, pointZeroZeroOne, - "1 ETH worth of pufETH in the protocol" + "2 ETH worth of pufETH in the protocol" ); assertGt( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, - "1 ETH worth of pufETH in the protocol gt" + 2 ether, + "2 ETH worth of pufETH in the protocol gt" ); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); } @@ -1566,7 +1544,7 @@ contract PufferProtocolTest is UnitTestHelper { // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1593,12 +1571,12 @@ contract PufferProtocolTest is UnitTestHelper { // Exchange rate stays the same assertEq(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); - // Alice has ~ 1 ETH locked in the protocol + // Alice has ~ 2 ETH locked in the protocol assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, + 2 ether, pointZeroZeroOne, - "1 ETH worth of pufETH in the protocol" + "2 ETH worth of pufETH in the protocol" ); // Alice got a little over 0.9 ETH worth of pufETH because she earned something for paying the VT on the second validator registration assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 0.9 ether, ">0.9 ETH worth of pufETH alice"); @@ -1616,7 +1594,7 @@ contract PufferProtocolTest is UnitTestHelper { // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1646,12 +1624,12 @@ contract PufferProtocolTest is UnitTestHelper { // Alice has ~ 1 ETH locked in the protocol assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, + 2 ether, pointZeroZeroOne, - "1 ETH worth of pufETH in the protocol" + "2 ETH worth of pufETH in the protocol" ); - // Alice got a little over 1 ETH worth of pufETH because she earned something for paying the VT on the second validator registration - assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1 ether, ">1 ETH worth of pufETH alice"); + // Alice got a little over 2 ETH worth of pufETH because she earned something for paying the VT on the second validator registration + assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 2 ether, ">2 ETH worth of pufETH alice"); } function test_validator_early_exit_dos() public { @@ -1840,7 +1818,7 @@ contract PufferProtocolTest is UnitTestHelper { hex"8aa088146c8c6ca6d8ad96648f20e791be7c449ce7035a6bd0a136b8c7b7867f730428af8d4a2b69658bfdade185d6110b938d7a59e98d905e922d53432e216dc88c3384157d74200d3f2de51d31737ce19098ff4d4f54f77f0175e23ac98da5"; } - // Generates a mock validator data for SGX 1 ETH case + // Generates a mock validator data for 2 ETH case function _getMockValidatorKeyData(bytes memory pubKey, bytes32 moduleName) internal view From 1f7b50827b2b473960a6f671d6510c9df48e5338 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 16 May 2025 18:07:19 +0200 Subject: [PATCH 07/82] Added tests for new flow --- .../Eigenlayer-Slashing/IEigenPod.sol | 2 +- .../test/mocks/EigenPodManagerMock.sol | 12 ++ .../test/unit/PufferModuleManager.t.sol | 131 +++++++++++++++++- 3 files changed, 142 insertions(+), 3 deletions(-) diff --git a/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol b/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol index f4288fdf..9473ff6a 100644 --- a/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol +++ b/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol @@ -84,7 +84,7 @@ interface IEigenPodErrors { interface IEigenPodTypes { enum VALIDATOR_STATUS { - INACTIVE, // doesnt exist + INACTIVE, // does not exist ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod WITHDRAWN // withdrawn from the Beacon Chain diff --git a/mainnet-contracts/test/mocks/EigenPodManagerMock.sol b/mainnet-contracts/test/mocks/EigenPodManagerMock.sol index 67312086..6ea5e424 100644 --- a/mainnet-contracts/test/mocks/EigenPodManagerMock.sol +++ b/mainnet-contracts/test/mocks/EigenPodManagerMock.sol @@ -6,9 +6,21 @@ import "src/interface/Eigenlayer-Slashing/IEigenPodManager.sol"; import "src/interface/Eigenlayer-Slashing/IAllocationManager.sol"; contract EigenPodMock { + + uint256 private constant WITHDRAWAL_FEE = 0.0001 ether; + + struct WithdrawalRequest { + bytes pubkey; + uint64 amountGwei; + } + function startCheckpoint(bool) external { } function setProofSubmitter(address) external { } + + function requestWithdrawal(WithdrawalRequest[] calldata requests) external payable { + payable(msg.sender).transfer(msg.value - requests.length * WITHDRAWAL_FEE); + } } contract EigenPodManagerMock is IEigenPodManager, Test { diff --git a/mainnet-contracts/test/unit/PufferModuleManager.t.sol b/mainnet-contracts/test/unit/PufferModuleManager.t.sol index afd284a2..b93f258f 100644 --- a/mainnet-contracts/test/unit/PufferModuleManager.t.sol +++ b/mainnet-contracts/test/unit/PufferModuleManager.t.sol @@ -5,12 +5,13 @@ import { UnitTestHelper } from "../helpers/UnitTestHelper.sol"; import { PufferModule } from "../../src/PufferModule.sol"; import { PufferProtocol } from "../../src/PufferProtocol.sol"; import { IPufferModuleManager } from "../../src/interface/IPufferModuleManager.sol"; +import { PufferModuleManager } from "../../src/PufferModuleManager.sol"; import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { Merkle } from "murky/Merkle.sol"; import { ISignatureUtils } from "src/interface/Eigenlayer-Slashing/ISignatureUtils.sol"; import { Unauthorized } from "../../src/Errors.sol"; -import { ROLE_ID_OPERATIONS_PAYMASTER } from "../../script/Roles.sol"; +import { ROLE_ID_OPERATIONS_PAYMASTER, ROLE_ID_VALIDATOR_EXITOR } from "../../script/Roles.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IDelegationManager } from "src/interface/Eigenlayer-Slashing/IDelegationManager.sol"; import { IDelegationManagerTypes } from "src/interface/Eigenlayer-Slashing/IDelegationManager.sol"; @@ -22,7 +23,8 @@ import { IAllocationManagerTypes } from "src/interface/Eigenlayer-Slashing/IAllo import { IAllocationManager } from "src/interface/Eigenlayer-Slashing/IAllocationManager.sol"; import { IRewardsCoordinator } from "src/interface/Eigenlayer-Slashing/IRewardsCoordinator.sol"; import { InvalidAddress } from "../../src/Errors.sol"; - +import { IAccessManaged } from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; +import { console } from "forge-std/console.sol"; contract PufferModuleUpgrade { function getMagicValue() external pure returns (uint256) { return 1337; @@ -37,15 +39,28 @@ contract PufferModuleManagerTest is UnitTestHelper { bytes32 CRAZY_GAINS = bytes32("CRAZY_GAINS"); + bytes32 MOCK_MODULE = bytes32("MOCK_MODULE"); + + address validatorExitor = makeAddr("validatorExitor"); + + uint256 EXIT_FEE = 0.0001 ether; + function setUp() public override { super.setUp(); vm.deal(address(this), 1000 ether); + vm.deal(validatorExitor, 3 ether); + bytes memory cd = new GenerateSlashingELCalldata().run(address(pufferModuleManager)); vm.startPrank(timelock); accessManager.grantRole(ROLE_ID_OPERATIONS_PAYMASTER, address(this), 0); + accessManager.grantRole(ROLE_ID_VALIDATOR_EXITOR, validatorExitor, 0); + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = PufferModuleManager.triggerValidatorsExit.selector; + accessManager.setTargetFunctionRole(address(pufferModuleManager), selectors, ROLE_ID_VALIDATOR_EXITOR); + (bool success,) = address(accessManager).call(cd); assertTrue(success, "should succeed"); @@ -335,6 +350,118 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.stopPrank(); } + function test_triggerValidatorsExitExactFee1() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + + vm.startPrank(validatorExitor); + + vm.expectEmit(true, true, true, true); + emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); + // Verify we get the fee back + pufferModuleManager.triggerValidatorsExit{ value: EXIT_FEE }(MOCK_MODULE, pubkeys); + vm.stopPrank(); + } + + + function test_triggerValidatorsExitExactFee2() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](2); + pubkeys[0] = bytes("0x1234"); + pubkeys[1] = bytes("0x4321"); + + vm.startPrank(validatorExitor); + + vm.expectEmit(true, true, true, true); + emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); + // Verify we get the fee back + pufferModuleManager.triggerValidatorsExit{ value: 2*EXIT_FEE }(MOCK_MODULE, pubkeys); + vm.stopPrank(); + } + + + function test_triggerValidatorsExitExcessFee() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + + vm.startPrank(validatorExitor); + + uint256 initialBalance = validatorExitor.balance; + + + vm.expectEmit(true, true, true, true); + emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); + + pufferModuleManager.triggerValidatorsExit{ value: 1 ether }(MOCK_MODULE, pubkeys); + + // Calculate expected balance: initial - gas costs + uint256 expectedBalance = initialBalance - EXIT_FEE; + + // Verify the balance change accounting for gas + assertEq(validatorExitor.balance, expectedBalance, "Should get the fee back minus gas costs"); + + vm.stopPrank(); + } + + + function test_triggerValidatorsExitExcessFee2() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](2); + pubkeys[0] = bytes("0x1234"); + pubkeys[1] = bytes("0x4321"); + + vm.startPrank(validatorExitor); + + uint256 initialBalance = validatorExitor.balance; + + + vm.expectEmit(true, true, true, true); + emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); + + pufferModuleManager.triggerValidatorsExit{ value: 1 ether }(MOCK_MODULE, pubkeys); + + // Calculate expected balance: initial - gas costs + uint256 expectedBalance = initialBalance - 2 * EXIT_FEE; + + // Verify the balance change accounting for gas + assertEq(validatorExitor.balance, expectedBalance, "Should get the fee back minus gas costs"); + + vm.stopPrank(); + } + + function test_triggerValidatorsExitNoFee() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + + vm.startPrank(validatorExitor); + + vm.expectRevert(); // panic underflow when subtracting fee + pufferModuleManager.triggerValidatorsExit(MOCK_MODULE, pubkeys); + vm.stopPrank(); + } + + function test_triggerValidatorsExitUnauthorized() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + + vm.startPrank(bob); + + vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, bob)); + pufferModuleManager.triggerValidatorsExit(MOCK_MODULE, pubkeys); + + vm.stopPrank(); + } + function _createPufferModule(bytes32 moduleName) internal returns (address module) { vm.assume(pufferProtocol.getModuleAddress(moduleName) == address(0)); vm.assume(bytes32("NO_VALIDATORS") != moduleName); From f5f8a40f629b48f5153b5f5742401dbc308a54ba Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 16 May 2025 18:07:36 +0200 Subject: [PATCH 08/82] forge fmt --- .../test/mocks/EigenPodManagerMock.sol | 1 - .../test/unit/PufferModuleManager.t.sol | 16 ++++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/mainnet-contracts/test/mocks/EigenPodManagerMock.sol b/mainnet-contracts/test/mocks/EigenPodManagerMock.sol index 6ea5e424..054c9ef7 100644 --- a/mainnet-contracts/test/mocks/EigenPodManagerMock.sol +++ b/mainnet-contracts/test/mocks/EigenPodManagerMock.sol @@ -6,7 +6,6 @@ import "src/interface/Eigenlayer-Slashing/IEigenPodManager.sol"; import "src/interface/Eigenlayer-Slashing/IAllocationManager.sol"; contract EigenPodMock { - uint256 private constant WITHDRAWAL_FEE = 0.0001 ether; struct WithdrawalRequest { diff --git a/mainnet-contracts/test/unit/PufferModuleManager.t.sol b/mainnet-contracts/test/unit/PufferModuleManager.t.sol index b93f258f..b2fda713 100644 --- a/mainnet-contracts/test/unit/PufferModuleManager.t.sol +++ b/mainnet-contracts/test/unit/PufferModuleManager.t.sol @@ -25,6 +25,7 @@ import { IRewardsCoordinator } from "src/interface/Eigenlayer-Slashing/IRewardsC import { InvalidAddress } from "../../src/Errors.sol"; import { IAccessManaged } from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; import { console } from "forge-std/console.sol"; + contract PufferModuleUpgrade { function getMagicValue() external pure returns (uint256) { return 1337; @@ -365,7 +366,6 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.stopPrank(); } - function test_triggerValidatorsExitExactFee2() public { _createPufferModule(MOCK_MODULE); @@ -378,11 +378,10 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); // Verify we get the fee back - pufferModuleManager.triggerValidatorsExit{ value: 2*EXIT_FEE }(MOCK_MODULE, pubkeys); + pufferModuleManager.triggerValidatorsExit{ value: 2 * EXIT_FEE }(MOCK_MODULE, pubkeys); vm.stopPrank(); } - function test_triggerValidatorsExitExcessFee() public { _createPufferModule(MOCK_MODULE); @@ -391,8 +390,7 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.startPrank(validatorExitor); - uint256 initialBalance = validatorExitor.balance; - + uint256 initialBalance = validatorExitor.balance; vm.expectEmit(true, true, true, true); emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); @@ -400,7 +398,7 @@ contract PufferModuleManagerTest is UnitTestHelper { pufferModuleManager.triggerValidatorsExit{ value: 1 ether }(MOCK_MODULE, pubkeys); // Calculate expected balance: initial - gas costs - uint256 expectedBalance = initialBalance - EXIT_FEE; + uint256 expectedBalance = initialBalance - EXIT_FEE; // Verify the balance change accounting for gas assertEq(validatorExitor.balance, expectedBalance, "Should get the fee back minus gas costs"); @@ -408,7 +406,6 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.stopPrank(); } - function test_triggerValidatorsExitExcessFee2() public { _createPufferModule(MOCK_MODULE); @@ -418,8 +415,7 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.startPrank(validatorExitor); - uint256 initialBalance = validatorExitor.balance; - + uint256 initialBalance = validatorExitor.balance; vm.expectEmit(true, true, true, true); emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); @@ -427,7 +423,7 @@ contract PufferModuleManagerTest is UnitTestHelper { pufferModuleManager.triggerValidatorsExit{ value: 1 ether }(MOCK_MODULE, pubkeys); // Calculate expected balance: initial - gas costs - uint256 expectedBalance = initialBalance - 2 * EXIT_FEE; + uint256 expectedBalance = initialBalance - 2 * EXIT_FEE; // Verify the balance change accounting for gas assertEq(validatorExitor.balance, expectedBalance, "Should get the fee back minus gas costs"); From 8c82c52530f1685336a278a97ca70f66cd1d1d8f Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 26 May 2025 13:59:56 +0200 Subject: [PATCH 09/82] Changed flow from triggerValidatorsExit to requestWithdrawal --- mainnet-contracts/script/SetupAccess.s.sol | 15 ++-- mainnet-contracts/src/PufferModule.sol | 17 +++-- mainnet-contracts/src/PufferModuleManager.sol | 26 +++++-- mainnet-contracts/src/PufferProtocol.sol | 37 ++++++++++ .../src/interface/IPufferModuleManager.sol | 18 ++++- .../src/interface/IPufferProtocol.sol | 20 ++++++ .../test/unit/PufferModuleManager.t.sol | 72 ++++++++++++++----- 7 files changed, 169 insertions(+), 36 deletions(-) diff --git a/mainnet-contracts/script/SetupAccess.s.sol b/mainnet-contracts/script/SetupAccess.s.sol index eb077ca6..be0018bc 100644 --- a/mainnet-contracts/script/SetupAccess.s.sol +++ b/mainnet-contracts/script/SetupAccess.s.sol @@ -154,7 +154,7 @@ contract SetupAccess is BaseScript { } function _setupPufferModuleManagerAccess() internal view returns (bytes[] memory) { - bytes[] memory calldatas = new bytes[](3); + bytes[] memory calldatas = new bytes[](4); // Dao selectors bytes4[] memory selectors = new bytes4[](7); @@ -183,15 +183,22 @@ contract SetupAccess is BaseScript { ); // ValidatorExitor selectors - bytes4[] memory validatorExitorSelectors = new bytes4[](1); - validatorExitorSelectors[0] = PufferModuleManager.triggerValidatorsExit.selector; + bytes4[] memory requestWithdrawalSelector = new bytes4[](1); + requestWithdrawalSelector[0] = PufferModuleManager.requestWithdrawal.selector; calldatas[2] = abi.encodeWithSelector( AccessManager.setTargetFunctionRole.selector, pufferDeployment.moduleManager, - validatorExitorSelectors, + requestWithdrawalSelector, ROLE_ID_VALIDATOR_EXITOR ); + + calldatas[3] = abi.encodeWithSelector( + AccessManager.setTargetFunctionRole.selector, + pufferDeployment.moduleManager, + requestWithdrawalSelector, + ROLE_ID_PUFFER_PROTOCOL + ); return calldatas; } diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index 10abbe0a..f7c2c532 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -196,22 +196,27 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { } /** - * @notice Triggers the validators exit for the given pubkeys + * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. + * If the amount is 0, the withdrawal is total and the validator will be fully exited. + * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. * @param pubkeys The pubkeys of the validators to exit + * @param gweiAmounts The amounts of the validators to exit, in Gwei * @dev Only callable by the PufferModuleManager * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function triggerValidatorsExit(bytes[] calldata pubkeys) external payable virtual onlyPufferModuleManager { + function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) + external + payable + virtual + onlyPufferModuleManager + { ModuleStorage storage $ = _getPufferModuleStorage(); IEigenPodTypes.WithdrawalRequest[] memory requests = new IEigenPodTypes.WithdrawalRequest[](pubkeys.length); for (uint256 i = 0; i < pubkeys.length; i++) { - requests[i] = IEigenPodTypes.WithdrawalRequest({ - pubkey: pubkeys[i], - amountGwei: 0 // This means full exit. Only value supported for 0x01 validators - }); + requests[i] = IEigenPodTypes.WithdrawalRequest({ pubkey: pubkeys[i], amountGwei: gweiAmounts[i] }); } uint256 oldBalance = address(this).balance - msg.value; $.eigenPod.requestWithdrawal{ value: msg.value }(requests); diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index 614dde84..63eeb9d2 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -244,25 +244,39 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, } /** - * @notice Triggers the validators exit for the given pubkeys - * @param moduleName The name of the Puffer module + * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. + * If the amount is 0, the withdrawal is total and the validator will be fully exited. + * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. + * @param moduleName The name of the module * @param pubkeys The pubkeys of the validators to exit - * @dev Restricted to the VALIDATOR_EXITOR + * @param gweiAmounts The amounts of the validators to exit, in Gwei + * @dev Restricted to the VALIDATOR_EXITOR role and the PufferProtocol * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function triggerValidatorsExit(bytes32 moduleName, bytes[] calldata pubkeys) external payable virtual restricted { + function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) + external + payable + virtual + restricted + { + if (pubkeys.length == 0) { + revert InputArrayLengthZero(); + } + if (pubkeys.length != gweiAmounts.length) { + revert InputArrayLengthMismatch(); + } address moduleAddress = IPufferProtocol(PUFFER_PROTOCOL).getModuleAddress(moduleName); uint256 oldBalance = address(this).balance - msg.value; - PufferModule(payable(moduleAddress)).triggerValidatorsExit{ value: msg.value }(pubkeys); + PufferModule(payable(moduleAddress)).requestWithdrawal{ value: msg.value }(pubkeys, gweiAmounts); uint256 excessAmount = address(this).balance - oldBalance; if (excessAmount > 0) { Address.sendValue(payable(msg.sender), excessAmount); } - emit ValidatorsExitTriggered(moduleName, pubkeys); + emit WithdrawalRequested(moduleName, pubkeys, gweiAmounts); } /** diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 12caaeb4..420aeac9 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -276,6 +276,43 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad $.validators[moduleName][index].status = Status.ACTIVE; } + /** + * @inheritdoc IPufferProtocol + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + */ + function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) + external + restricted + { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + // validate pubkeys belong to that node + + uint256 pendingValidatorIndex = $.pendingValidatorIndices[moduleName]; + + bool correct; + bytes32 pubkeyHash; + for (uint256 i = 0; i < pubkeys.length; i++) { + correct = false; + pubkeyHash = keccak256(pubkeys[i]); + for (uint256 j = 0; j < pendingValidatorIndex; j++) { + Validator memory validator = $.validators[moduleName][j]; + if ( + validator.node == msg.sender && validator.status == Status.ACTIVE + && keccak256(validator.pubKey) == pubkeyHash + ) { + correct = true; + break; + } + } + if (!correct) { + revert InvalidValidator(); + } + } + + PUFFER_MODULE_MANAGER.requestWithdrawal(moduleName, pubkeys, gweiAmounts); + } + /** * @inheritdoc IPufferProtocol * @dev Restricted to Puffer Paymaster diff --git a/mainnet-contracts/src/interface/IPufferModuleManager.sol b/mainnet-contracts/src/interface/IPufferModuleManager.sol index 31b9963e..50734ca0 100644 --- a/mainnet-contracts/src/interface/IPufferModuleManager.sol +++ b/mainnet-contracts/src/interface/IPufferModuleManager.sol @@ -14,6 +14,16 @@ interface IPufferModuleManager { */ error ForbiddenModuleName(); + /** + * @notice Thrown if the input array length mismatch + */ + error InputArrayLengthMismatch(); + + /** + * @notice Thrown if the input array length is zero + */ + error InputArrayLengthZero(); + /** * @notice Emitted when the Custom Call from the restakingOperator is successful * @dev Signature "0x80b240e4b7a31d61bdee28b97592a7c0ad486cb27d11ee5c6b90530db4e949ff" @@ -74,11 +84,13 @@ interface IPufferModuleManager { event PufferModuleUndelegated(bytes32 indexed moduleName); /** - * @notice Emitted when the validators exit is triggered + * @notice Emitted when a withdrawal is requested * @param moduleName the module name to be undelegated - * @dev Signature "0x456e0aba5f7f36ec541f2f550d3f5895eb7d1ae057f45e8683952ac182254e5d" + * @param pubkeys the pubkeys of the validators to exit + * @param gweiAmounts the amounts of the validators to exit, in Gwei + * @dev Signature "0x8de190edd50136636ef6acd43f071508e34713f6dbaf117f74cc92322e32a387" */ - event ValidatorsExitTriggered(bytes32 indexed moduleName, bytes[] pubkeys); + event WithdrawalRequested(bytes32 indexed moduleName, bytes[] pubkeys, uint64[] gweiAmounts); /** * @notice Emitted when the restaking operator avs signature proof is updated diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index d28c83c7..da808f55 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -74,6 +74,12 @@ interface IPufferProtocol { */ error Failed(); + /** + * @notice Thrown if the validator is not valid + * @dev Signature "0x682a6e7c" + */ + error InvalidValidator(); + /** * @notice Emitted when the number of active validators changes * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" @@ -195,6 +201,20 @@ interface IPufferProtocol { */ function withdrawValidatorTickets(uint96 amount, address recipient) external; + /** + * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. + * If the amount is 0, the withdrawal is total and the validator will be fully exited. + * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. + * @param moduleName The name of the module + * @param pubkeys The pubkeys of the validators to exit + * @param gweiAmounts The amounts of the validators to exit, in Gwei + * @dev Restricted to Node Operators + * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * to the caller from the EigenPod + */ + function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external; + /** * @notice Batch settling of validator withdrawals * diff --git a/mainnet-contracts/test/unit/PufferModuleManager.t.sol b/mainnet-contracts/test/unit/PufferModuleManager.t.sol index b2fda713..15aa6923 100644 --- a/mainnet-contracts/test/unit/PufferModuleManager.t.sol +++ b/mainnet-contracts/test/unit/PufferModuleManager.t.sol @@ -59,7 +59,7 @@ contract PufferModuleManagerTest is UnitTestHelper { accessManager.grantRole(ROLE_ID_OPERATIONS_PAYMASTER, address(this), 0); accessManager.grantRole(ROLE_ID_VALIDATOR_EXITOR, validatorExitor, 0); bytes4[] memory selectors = new bytes4[](1); - selectors[0] = PufferModuleManager.triggerValidatorsExit.selector; + selectors[0] = PufferModuleManager.requestWithdrawal.selector; accessManager.setTargetFunctionRole(address(pufferModuleManager), selectors, ROLE_ID_VALIDATOR_EXITOR); (bool success,) = address(accessManager).call(cd); @@ -351,51 +351,54 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.stopPrank(); } - function test_triggerValidatorsExitExactFee1() public { + function test_requestWithdrawalExactFee1() public { _createPufferModule(MOCK_MODULE); bytes[] memory pubkeys = new bytes[](1); pubkeys[0] = bytes("0x1234"); + uint64[] memory gweiAmounts = new uint64[](1); vm.startPrank(validatorExitor); vm.expectEmit(true, true, true, true); - emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); + emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); // Verify we get the fee back - pufferModuleManager.triggerValidatorsExit{ value: EXIT_FEE }(MOCK_MODULE, pubkeys); + pufferModuleManager.requestWithdrawal{ value: EXIT_FEE }(MOCK_MODULE, pubkeys, gweiAmounts); vm.stopPrank(); } - function test_triggerValidatorsExitExactFee2() public { + function test_requestWithdrawalExactFee2() public { _createPufferModule(MOCK_MODULE); bytes[] memory pubkeys = new bytes[](2); pubkeys[0] = bytes("0x1234"); pubkeys[1] = bytes("0x4321"); + uint64[] memory gweiAmounts = new uint64[](2); vm.startPrank(validatorExitor); vm.expectEmit(true, true, true, true); - emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); + emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); // Verify we get the fee back - pufferModuleManager.triggerValidatorsExit{ value: 2 * EXIT_FEE }(MOCK_MODULE, pubkeys); + pufferModuleManager.requestWithdrawal{ value: 2 * EXIT_FEE }(MOCK_MODULE, pubkeys, gweiAmounts); vm.stopPrank(); } - function test_triggerValidatorsExitExcessFee() public { + function test_requestWithdrawalExcessFee() public { _createPufferModule(MOCK_MODULE); bytes[] memory pubkeys = new bytes[](1); pubkeys[0] = bytes("0x1234"); + uint64[] memory gweiAmounts = new uint64[](1); vm.startPrank(validatorExitor); uint256 initialBalance = validatorExitor.balance; vm.expectEmit(true, true, true, true); - emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); + emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); - pufferModuleManager.triggerValidatorsExit{ value: 1 ether }(MOCK_MODULE, pubkeys); + pufferModuleManager.requestWithdrawal{ value: 1 ether }(MOCK_MODULE, pubkeys, gweiAmounts); // Calculate expected balance: initial - gas costs uint256 expectedBalance = initialBalance - EXIT_FEE; @@ -406,21 +409,22 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.stopPrank(); } - function test_triggerValidatorsExitExcessFee2() public { + function test_requestWithdrawalExcessFee2() public { _createPufferModule(MOCK_MODULE); bytes[] memory pubkeys = new bytes[](2); pubkeys[0] = bytes("0x1234"); pubkeys[1] = bytes("0x4321"); + uint64[] memory gweiAmounts = new uint64[](2); vm.startPrank(validatorExitor); uint256 initialBalance = validatorExitor.balance; vm.expectEmit(true, true, true, true); - emit IPufferModuleManager.ValidatorsExitTriggered(MOCK_MODULE, pubkeys); + emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); - pufferModuleManager.triggerValidatorsExit{ value: 1 ether }(MOCK_MODULE, pubkeys); + pufferModuleManager.requestWithdrawal{ value: 1 ether }(MOCK_MODULE, pubkeys, gweiAmounts); // Calculate expected balance: initial - gas costs uint256 expectedBalance = initialBalance - 2 * EXIT_FEE; @@ -431,29 +435,63 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.stopPrank(); } - function test_triggerValidatorsExitNoFee() public { + function test_requestWithdrawalNoFee() public { _createPufferModule(MOCK_MODULE); bytes[] memory pubkeys = new bytes[](1); pubkeys[0] = bytes("0x1234"); + uint64[] memory gweiAmounts = new uint64[](1); vm.startPrank(validatorExitor); vm.expectRevert(); // panic underflow when subtracting fee - pufferModuleManager.triggerValidatorsExit(MOCK_MODULE, pubkeys); + pufferModuleManager.requestWithdrawal(MOCK_MODULE, pubkeys, gweiAmounts); vm.stopPrank(); } - function test_triggerValidatorsExitUnauthorized() public { + function test_requestWithdrawalUnauthorized() public { _createPufferModule(MOCK_MODULE); bytes[] memory pubkeys = new bytes[](1); pubkeys[0] = bytes("0x1234"); + uint64[] memory gweiAmounts = new uint64[](1); vm.startPrank(bob); vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, bob)); - pufferModuleManager.triggerValidatorsExit(MOCK_MODULE, pubkeys); + pufferModuleManager.requestWithdrawal(MOCK_MODULE, pubkeys, gweiAmounts); + + vm.stopPrank(); + } + + function test_requestWithdrawalInputArrayLengthMismatch() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + + uint64[] memory gweiAmounts = new uint64[](2); + gweiAmounts[0] = 1 ether; + gweiAmounts[1] = 2 ether; + + vm.startPrank(validatorExitor); + + vm.expectRevert(abi.encodeWithSelector(IPufferModuleManager.InputArrayLengthMismatch.selector)); + pufferModuleManager.requestWithdrawal(MOCK_MODULE, pubkeys, gweiAmounts); + + vm.stopPrank(); + } + + function test_requestWithdrawalInputArrayLengthZero() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](0); + uint64[] memory gweiAmounts = new uint64[](0); + + vm.startPrank(validatorExitor); + + vm.expectRevert(abi.encodeWithSelector(IPufferModuleManager.InputArrayLengthZero.selector)); + pufferModuleManager.requestWithdrawal(MOCK_MODULE, pubkeys, gweiAmounts); vm.stopPrank(); } From 892634702dc0db13926f439d2322dc20bcb60a6a Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 27 May 2025 10:23:39 +0200 Subject: [PATCH 10/82] Added fee to PufferProtocol.requestWithdrawal --- mainnet-contracts/src/PufferProtocol.sol | 11 ++++++++++- mainnet-contracts/src/interface/IPufferProtocol.sol | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 420aeac9..1e0f1117 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -18,6 +18,7 @@ import { ProtocolStorage, NodeInfo, ModuleLimit } from "./struct/ProtocolStorage import { LibBeaconchainContract } from "./LibBeaconchainContract.sol"; import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; @@ -283,6 +284,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external restricted + payable { ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -310,7 +312,14 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad } } - PUFFER_MODULE_MANAGER.requestWithdrawal(moduleName, pubkeys, gweiAmounts); + uint256 oldBalance = address(this).balance - msg.value; + + PUFFER_MODULE_MANAGER.requestWithdrawal{value:msg.value}(moduleName, pubkeys, gweiAmounts); + + uint256 excessAmount = address(this).balance - oldBalance; + if (excessAmount > 0) { + Address.sendValue(payable(msg.sender), excessAmount); + } } /** diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index da808f55..bfa4cc22 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -213,7 +213,7 @@ interface IPufferProtocol { * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external; + function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external payable; /** * @notice Batch settling of validator withdrawals From 700073145398287b940ca8f0d58c9fd6112ae921 Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 27 May 2025 10:55:10 +0200 Subject: [PATCH 11/82] Refactored fee return to modifier --- mainnet-contracts/src/PufferModule.sol | 15 ++++++++----- mainnet-contracts/src/PufferModuleManager.sol | 15 ++++++++----- mainnet-contracts/src/PufferProtocol.sol | 21 +++++++++++-------- .../src/interface/IPufferProtocol.sol | 4 +++- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index f7c2c532..744001d2 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -43,6 +43,15 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { bytes32 private constant _PUFFER_MODULE_BASE_STORAGE = 0x501caad7d5b9c1542c99d193b659cbf5c57571609bcfc93d65f1e159821d6200; + modifier returnExcessFee() { + uint256 oldBalance = address(this).balance - msg.value; + _; + uint256 excessAmount = address(this).balance - oldBalance; + if (excessAmount > 0) { + Address.sendValue(payable(msg.sender), excessAmount); + } + } + constructor( IPufferProtocol protocol, address eigenPodManager, @@ -211,6 +220,7 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { payable virtual onlyPufferModuleManager + returnExcessFee { ModuleStorage storage $ = _getPufferModuleStorage(); @@ -218,12 +228,7 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { for (uint256 i = 0; i < pubkeys.length; i++) { requests[i] = IEigenPodTypes.WithdrawalRequest({ pubkey: pubkeys[i], amountGwei: gweiAmounts[i] }); } - uint256 oldBalance = address(this).balance - msg.value; $.eigenPod.requestWithdrawal{ value: msg.value }(requests); - uint256 excessAmount = address(this).balance - oldBalance; - if (excessAmount > 0) { - Address.sendValue(payable(PUFFER_MODULE_MANAGER), excessAmount); - } } /** diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index 63eeb9d2..c6ea00ab 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -42,6 +42,15 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, _; } + modifier returnExcessFee() { + uint256 oldBalance = address(this).balance - msg.value; + _; + uint256 excessAmount = address(this).balance - oldBalance; + if (excessAmount > 0) { + Address.sendValue(payable(msg.sender), excessAmount); + } + } + constructor(address pufferModuleBeacon, address restakingOperatorBeacon, address pufferProtocol) { PUFFER_MODULE_BEACON = pufferModuleBeacon; RESTAKING_OPERATOR_BEACON = restakingOperatorBeacon; @@ -260,6 +269,7 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, payable virtual restricted + returnExcessFee { if (pubkeys.length == 0) { revert InputArrayLengthZero(); @@ -269,12 +279,7 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, } address moduleAddress = IPufferProtocol(PUFFER_PROTOCOL).getModuleAddress(moduleName); - uint256 oldBalance = address(this).balance - msg.value; PufferModule(payable(moduleAddress)).requestWithdrawal{ value: msg.value }(pubkeys, gweiAmounts); - uint256 excessAmount = address(this).balance - oldBalance; - if (excessAmount > 0) { - Address.sendValue(payable(msg.sender), excessAmount); - } emit WithdrawalRequested(moduleName, pubkeys, gweiAmounts); } diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 1e0f1117..0429306e 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -96,6 +96,15 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ IBeaconDepositContract public immutable override BEACON_DEPOSIT_CONTRACT; + modifier returnExcessFee() { + uint256 oldBalance = address(this).balance - msg.value; + _; + uint256 excessAmount = address(this).balance - oldBalance; + if (excessAmount > 0) { + Address.sendValue(payable(msg.sender), excessAmount); + } + } + constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, @@ -283,8 +292,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external - restricted payable + restricted + returnExcessFee { ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -312,14 +322,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad } } - uint256 oldBalance = address(this).balance - msg.value; - - PUFFER_MODULE_MANAGER.requestWithdrawal{value:msg.value}(moduleName, pubkeys, gweiAmounts); - - uint256 excessAmount = address(this).balance - oldBalance; - if (excessAmount > 0) { - Address.sendValue(payable(msg.sender), excessAmount); - } + PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); } /** diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index bfa4cc22..8563b2c5 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -213,7 +213,9 @@ interface IPufferProtocol { * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external payable; + function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) + external + payable; /** * @notice Batch settling of validator withdrawals From 01ba5b26c96d0e27f0ab45a1fdc37b45e34232ce Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 27 May 2025 19:01:35 +0200 Subject: [PATCH 12/82] Implemented consolidation flow --- mainnet-contracts/src/PufferModule.sol | 32 ++++++- mainnet-contracts/src/PufferModuleManager.sol | 6 +- mainnet-contracts/src/PufferProtocol.sol | 94 +++++++++++++++---- .../src/interface/IPufferProtocol.sol | 41 +++++++- 4 files changed, 147 insertions(+), 26 deletions(-) diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index 744001d2..3ac83443 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -204,14 +204,40 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { return EIGEN_DELEGATION_MANAGER.undelegate(address(this)); } + /** + * @notice Requests a consolidation for the given validators. This consolidation consists on merging one validator into another one + * @param srcPubkeys The pubkeys of the validators to consolidate from + * @param targetPubkeys The pubkeys of the validators to consolidate to + * @dev Only callable by the PufferProtocol + * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * to the caller from the EigenPod + */ + function requestConsolidation(bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) + external + payable + virtual + onlyPufferProtocol + returnExcessFee + { + ModuleStorage storage $ = _getPufferModuleStorage(); + + IEigenPod.ConsolidationRequest[] memory requests = new IEigenPodTypes.ConsolidationRequest[](srcPubkeys.length); + for (uint256 i = 0; i < srcPubkeys.length; i++) { + requests[i] = + IEigenPodTypes.ConsolidationRequest({ srcPubkey: srcPubkeys[i], targetPubkey: targetPubkeys[i] }); + } + $.eigenPod.requestConsolidation{ value: msg.value }(requests); + } + /** * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. * If the amount is 0, the withdrawal is total and the validator will be fully exited. * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. - * @param pubkeys The pubkeys of the validators to exit - * @param gweiAmounts The amounts of the validators to exit, in Gwei + * @param pubkeys The pubkeys of the validators to withdraw + * @param gweiAmounts The amounts of the validators to withdraw, in Gwei * @dev Only callable by the PufferModuleManager - * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index c6ea00ab..24840322 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -257,10 +257,10 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, * If the amount is 0, the withdrawal is total and the validator will be fully exited. * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. * @param moduleName The name of the module - * @param pubkeys The pubkeys of the validators to exit - * @param gweiAmounts The amounts of the validators to exit, in Gwei + * @param pubkeys The pubkeys of the validators to withdraw + * @param gweiAmounts The amounts of the validators to withdraw, in Gwei * @dev Restricted to the VALIDATOR_EXITOR role and the PufferProtocol - * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 0429306e..a7e00852 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -288,7 +288,63 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad /** * @inheritdoc IPufferProtocol - * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + * @dev Restricted to Node Operators + */ + function requestConsolidation(bytes32 moduleName, bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) + external + payable + virtual + restricted + returnExcessFee + { + if (srcPubkeys.length == 0) { + revert InputArrayLengthZero(); + } + if (srcPubkeys.length != targetPubkeys.length) { + revert InputArrayLengthMismatch(); + } + + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + // validate pubkeys belong to that node + bool alreadyChecked; + bytes32 pubkeyHashSrc; + bytes32 pubkeyHashTarget; + uint256 pendingValidatorIndex = $.pendingValidatorIndices[moduleName]; + for (uint256 i = 0; i < srcPubkeys.length; i++) { + pubkeyHashSrc = keccak256(srcPubkeys[i]); + pubkeyHashTarget = keccak256(targetPubkeys[i]); + if (pubkeyHashSrc == pubkeyHashTarget) { + revert InvalidValidator(); + } + assembly { + let slot := keccak256(add(pubkeyHashSrc, 0x20), 0x20) + alreadyChecked := tload(slot) + tstore(slot, true) + } + // Preemptively storing it to true to save slot calculation + if (!alreadyChecked) { + _checkValidator(pendingValidatorIndex, $, pubkeyHashSrc, moduleName); + } + assembly { + let slot := keccak256(add(pubkeyHashTarget, 0x20), 0x20) + alreadyChecked := tload(slot) + tstore(slot, true) + } + // Preemptively storing it to true to save slot calculation + if (!alreadyChecked) { + _checkValidator(pendingValidatorIndex, $, pubkeyHashTarget, moduleName); + } + } + + $.modules[moduleName].requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); + + emit ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); + } + + /** + * @inheritdoc IPufferProtocol + * @dev Restricted to Node Operators */ function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external @@ -302,24 +358,10 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad uint256 pendingValidatorIndex = $.pendingValidatorIndices[moduleName]; - bool correct; bytes32 pubkeyHash; for (uint256 i = 0; i < pubkeys.length; i++) { - correct = false; pubkeyHash = keccak256(pubkeys[i]); - for (uint256 j = 0; j < pendingValidatorIndex; j++) { - Validator memory validator = $.validators[moduleName][j]; - if ( - validator.node == msg.sender && validator.status == Status.ACTIVE - && keccak256(validator.pubKey) == pubkeyHash - ) { - correct = true; - break; - } - } - if (!correct) { - revert InvalidValidator(); - } + _checkValidator(pendingValidatorIndex, $, pubkeyHash, moduleName); } PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); @@ -845,5 +887,25 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); } + function _checkValidator(uint256 numValidators, ProtocolStorage storage $, bytes32 pubkeyHash, bytes32 moduleName) + internal + view + { + bool correct; + for (uint256 j = 0; j < numValidators; j++) { + Validator memory validator = $.validators[moduleName][j]; + if ( + validator.node == msg.sender && validator.status == Status.ACTIVE + && keccak256(validator.pubKey) == pubkeyHash + ) { + correct = true; + break; + } + } + if (!correct) { + revert InvalidValidator(); + } + } + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } } diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 8563b2c5..8d3cae42 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -80,6 +80,18 @@ interface IPufferProtocol { */ error InvalidValidator(); + /** + * @notice Thrown if the input array length mismatch + * @dev Signature "0x43714afd" + */ + error InputArrayLengthMismatch(); + + /** + * @notice Thrown if the input array length is zero + * @dev Signature "0x796cc525" + */ + error InputArrayLengthZero(); + /** * @notice Emitted when the number of active validators changes * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" @@ -159,6 +171,15 @@ interface IPufferProtocol { uint256 vtBurnAmount ); + /** + * @notice Emitted when a consolidation is requested + * @param moduleName is the module name + * @param srcPubkeys is the list of pubkeys to consolidate from + * @param targetPubkeys is the list of pubkeys to consolidate to + * @dev Signature "0xdc26585f08f92fc2f54b80496c32d3c20cfa17f1e91d9afc8449c17d1b4f85bb" + */ + event ConsolidationRequested(bytes32 indexed moduleName, bytes[] srcPubkeys, bytes[] targetPubkeys); + /** * @notice Emitted when the Validator is provisioned * @param pubKey is the validator public key @@ -201,15 +222,27 @@ interface IPufferProtocol { */ function withdrawValidatorTickets(uint96 amount, address recipient) external; + /** + * @notice Requests a consolidation for the given validators. This consolidation consists on merging one validator into another one + * @param moduleName The module name of the validators to consolidate + * @param srcPubkeys The pubkeys of the validators to consolidate from + * @param targetPubkeys The pubkeys of the validators to consolidate to + * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * to the caller from the EigenPod + */ + function requestConsolidation(bytes32 moduleName, bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) + external + payable; + /** * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. * If the amount is 0, the withdrawal is total and the validator will be fully exited. * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. * @param moduleName The name of the module - * @param pubkeys The pubkeys of the validators to exit - * @param gweiAmounts The amounts of the validators to exit, in Gwei - * @dev Restricted to Node Operators - * @dev According to EIP-7002 there is a fee for each validator exit request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * @param pubkeys The pubkeys of the validators to withdraw + * @param gweiAmounts The amounts of the validators to withdraw, in Gwei + * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ From acd617ccdb072f621975162e16eb29fe575e1473 Mon Sep 17 00:00:00 2001 From: Eladio Date: Wed, 28 May 2025 10:35:02 +0200 Subject: [PATCH 13/82] Changed withdrawal credentials to type 2 --- mainnet-contracts/src/PufferModule.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index 3ac83443..c45fdb72 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -270,7 +270,7 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { function getWithdrawalCredentials() public view returns (bytes memory) { // Withdrawal credentials for EigenLayer modules are EigenPods ModuleStorage storage $ = _getPufferModuleStorage(); - return abi.encodePacked(bytes1(uint8(1)), bytes11(0), $.eigenPod); + return abi.encodePacked(bytes1(uint8(2)), bytes11(0), $.eigenPod); } /** From d6d18beacf79d02e87e74ec2d349a1830f2926b2 Mon Sep 17 00:00:00 2001 From: Eladio Date: Wed, 28 May 2025 12:04:59 +0200 Subject: [PATCH 14/82] Changed callStake to deposit directly to beacon chain contract --- mainnet-contracts/src/PufferModule.sol | 12 ------------ mainnet-contracts/src/PufferProtocol.sol | 4 +++- mainnet-contracts/test/mocks/BeaconMock.sol | 1 + .../test/unit/PufferModuleManager.t.sol | 10 ---------- 4 files changed, 4 insertions(+), 23 deletions(-) diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index c45fdb72..f519bf8e 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -105,18 +105,6 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { receive() external payable { } - /** - * @notice Starts the validator - */ - function callStake(bytes calldata pubKey, bytes calldata signature, bytes32 depositDataRoot) - external - payable - onlyPufferProtocol - { - // EigenPod is deployed in this call - EIGEN_POD_MANAGER.stake{ value: 32 ether }(pubKey, signature, depositDataRoot); - } - /** * @notice Sets the proof submitter on the EigenPod */ diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index a7e00852..0accc2bc 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -839,7 +839,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // Increase lockedETH on Puffer Oracle PUFFER_ORACLE.provisionNode(); - module.callStake({ pubKey: validatorPubKey, signature: validatorSignature, depositDataRoot: depositDataRoot }); + BEACON_DEPOSIT_CONTRACT.deposit( + validatorPubKey, module.getWithdrawalCredentials(), validatorSignature, depositDataRoot + ); } function _getVTBurnAmount(ProtocolStorage storage $, address node, StoppedValidatorInfo calldata validatorInfo) diff --git a/mainnet-contracts/test/mocks/BeaconMock.sol b/mainnet-contracts/test/mocks/BeaconMock.sol index 2bd35a48..85faf528 100644 --- a/mainnet-contracts/test/mocks/BeaconMock.sol +++ b/mainnet-contracts/test/mocks/BeaconMock.sol @@ -8,6 +8,7 @@ contract BeaconMock { function deposit(bytes calldata, bytes calldata, bytes calldata, bytes32) external payable { if (msg.value != 32 ether) { + // TODO Change this for pectra revert BadValue(); } emit StartedStaking(); diff --git a/mainnet-contracts/test/unit/PufferModuleManager.t.sol b/mainnet-contracts/test/unit/PufferModuleManager.t.sol index 15aa6923..2037d606 100644 --- a/mainnet-contracts/test/unit/PufferModuleManager.t.sol +++ b/mainnet-contracts/test/unit/PufferModuleManager.t.sol @@ -102,16 +102,6 @@ contract PufferModuleManagerTest is UnitTestHelper { assertEq(PufferModule(payable(module)).NAME(), moduleName, "bad name"); } - function test_pufferModuleAuthorization(bytes32 moduleName) public { - address module = _createPufferModule(moduleName); - - vm.expectRevert(Unauthorized.selector); - PufferModule(payable(module)).callStake("", "", ""); - - vm.expectRevert(Unauthorized.selector); - PufferModule(payable(module)).call(address(0), 0, ""); - } - function test_registerOperatorToAVS() public { vm.startPrank(DAO); RestakingOperator operator = _createRestakingOperator(); From a64a58580b5c132b50c296768a6d3d913ea420af Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 29 May 2025 11:31:15 +0200 Subject: [PATCH 15/82] validator tickets -> validation time --- mainnet-contracts/docs/PufferProtocol.md | 91 +- mainnet-contracts/foundry.toml | 2 +- .../script/DeployEverything.s.sol | 8 +- mainnet-contracts/script/DeployPuffer.s.sol | 14 +- .../DeployPufferProtocolImplementation.s.sol | 3 +- ...GenerateBLSKeysAndRegisterValidators.s.sol | 18 +- mainnet-contracts/script/SetupAccess.s.sol | 4 +- mainnet-contracts/src/PufferOracleV2.sol | 2 +- mainnet-contracts/src/PufferProtocol.sol | 410 ++++--- .../src/interface/IPufferProtocol.sol | 33 +- mainnet-contracts/src/struct/NodeInfo.sol | 6 +- .../src/struct/ProtocolStorage.sol | 5 +- .../src/struct/StoppedValidatorInfo.sol | 8 +- .../test/handlers/PufferProtocolHandler.sol | 2 +- .../test/mocks/PufferProtocolMockUpgrade.sol | 3 +- .../test/unit/PufferProtocol.t.sol | 1061 ++++++++++------- mainnet-contracts/test/unit/Timelock.t.sol | 4 +- .../test/unit/ValidatorTicket.t.sol | 188 --- 18 files changed, 1016 insertions(+), 846 deletions(-) diff --git a/mainnet-contracts/docs/PufferProtocol.md b/mainnet-contracts/docs/PufferProtocol.md index 900dc575..8bfafb72 100644 --- a/mainnet-contracts/docs/PufferProtocol.md +++ b/mainnet-contracts/docs/PufferProtocol.md @@ -23,13 +23,11 @@ The `PufferProtocol` serves as the central contract and fulfills three key funct 9. **Completion and Restaking**: Once the Beacon chain recognizes the validator, a withdrawal credentials merkle proof is submitted back to EigenLayer to enable the restaking of the validator's ETH. - ## Registering a validator ### 1. Prepare Bond and VTs -NoOps are required to deposit pufETH and Validator Tickets (VTs) to register a validator. The amount of pufETH depends on the use of an anti-slasher enclave: +NoOps are required to send enough ETH to cover for the validator bond and the validation time. -- **With an enclave**: 1 ETH worth of pufETH is required. -- **Without an enclave**: 2 ETH worth of pufETH is required. +1.5 ETH is the bond amount required. + 30 days of validation time. @@ -41,11 +39,47 @@ The `PufferProtocol` contract mandates a minimum number of VTs at registration, > function registerValidatorKey( > ValidatorKeyData calldata data, > bytes32 moduleName, -> Permit calldata pufETHPermit, -> Permit calldata vtPermit +> uint256 totalEpochsValidated, +> bytes[] calldata vtConsumptionSignature > ) > ``` +Before calling `registerValidatorKey`, Node Operators must first obtain their VT consumption data from the Puffer Backend API. This data includes: +- Total epochs validated by their active validators +- A signature from the Guardians verifying this data + +```mermaid +sequenceDiagram + participant NodeOperator + participant PufferBackend + participant PufferProtocol + participant Paymaster + participant PufferVault + participant PufferModule + + rect rgb(0, 208, 255) + NodeOperator->>PufferBackend: Request VT consumption data (API CALL) + PufferBackend-->>NodeOperator: Return VT consumption & signatures + end + + rect rgb(25, 202, 84) + NodeOperator->>PufferProtocol: registerValidatorKey(data, moduleName, totalEpochsValidated, vtConsumptionSignature) + PufferProtocol->>PufferProtocol: _checkValidatorRegistrationInputs() + PufferProtocol->>PufferProtocol: _settleVTAccounting() + PufferProtocol->>PufferVault: depositETH() - converts ETH bond to pufETH for the NoOp + PufferProtocol->>PufferProtocol: Store validator data + end + + Note over Paymaster: If there is liquidity and the registration is valid + + rect rgb(25, 202, 199) + Paymaster->>PufferProtocol: provisionNode() + PufferProtocol->>PufferVault: transferETH(32 ETH, pufferModule) + PufferProtocol->>PufferModule: callStake() + PufferModule->>BeaconChain: 32 ETH + end +``` + The NoOp must supply the following `ValidatorKeyData` struct created off-chain: - **`bytes blsPubKey`**: [BLS public key](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/keys/) generated by the NoOp. @@ -70,7 +104,6 @@ This function allows flexibility in how pufETH and VTs are supplied. Options inc - Transferring pufETH and VTs using Permit messages or prior approve transactions. - Combining both methods (e.g., minting pufETH and transferring VTs). - #### Registration side effects Successful registration adds the validator to the `PufferModule` queue. Guardians verify the data and manage keyshare custody. Once verified, they provision the validator with 32 ETH from the `PufferVault` to deploy to the `PufferModule's` `EigenPod`. @@ -90,7 +123,7 @@ The `provisionNode` function executes several critical steps in one atomic trans - It updates the `_numberOfActivePufferValidators` counter on the `PufferOracleV2` contract, reflecting the addition of a new validator on the beacon chain. #### Impact on pufETH Conversion Rate -- The provisioning of a validator involves transferring 32 ETH out of the PufferVault, which could negatively affect the pufETH:ETH conversion rate. To prevent this, the vault calculation includes the ETH amount locked as reported by `PUFFER_ORACLE.getLockedEthAmount()`. When provisioning occurs, the oracle contract’s reported locked ETH amount is increased by 32 ETH. This adjustment ensures that the vault's exchange rate remains unchanged, despite the outflow of funds. For a more detailed explanation, refer to the [`PufferOracleV2`](./PufferOracleV2.md) documentation. +- The provisioning of a validator involves transferring 32 ETH out of the PufferVault, which could negatively affect the pufETH:ETH conversion rate. To prevent this, the vault calculation includes the ETH amount locked as reported by `PUFFER_ORACLE.getLockedEthAmount()`. When provisioning occurs, the oracle contract's reported locked ETH amount is increased by 32 ETH. This adjustment ensures that the vault's exchange rate remains unchanged, despite the outflow of funds. For a more detailed explanation, refer to the [`PufferOracleV2`](./PufferOracleV2.md) documentation. ## Restaking a validator Once a validator is onboarded into a `PufferModule` and their validator is observable from the Beacon chain, their Beacon chain ETH is restaked. This process involves delegating the validator's ETH to a [`RestakingOperator`](./RestakingOperator.md), as determined by the DAO. @@ -110,7 +143,7 @@ Guardians, utilizing their enclaves, are authorized to sign and broadcast volunt - If the Node Operator fails to replenish their VTs after their locked amount has expired. #### After exiting -Post-exit, the exited validator’s ETH will be redirected to the `PufferModule's` `EigenPod`. The [`PufferModuleManager`](./PufferModuleManager.md) oversees the full withdrawal process on EigenLayer, which involves Merkle proofs and queueing, to transfer the ETH back to the `PufferModule`. +Post-exit, the exited validator's ETH will be redirected to the `PufferModule's` `EigenPod`. The [`PufferModuleManager`](./PufferModuleManager.md) oversees the full withdrawal process on EigenLayer, which involves Merkle proofs and queueing, to transfer the ETH back to the `PufferModule`. The Guardians then execute `batchHandleWithdrawals()` on the `PufferProtocol`, which returns pufETH bonds to NoOps, burns their consumed VTs, and performs necessary [accounting](./PufferOracleV2.md). The process addresses three potential scenarios: @@ -121,7 +154,7 @@ The Guardians then execute `batchHandleWithdrawals()` on the `PufferProtocol`, w **Insufficient Withdrawal** (*withdrawalAmount* < 32 ETH): - The entire *withdrawalAmount* is transferred back to the `PufferVault`. -- The missing ETH (32 ETH - *withdrawalAmount*) is burned from the NoOp’s bond. +- The missing ETH (32 ETH - *withdrawalAmount*) is burned from the NoOp's bond. - The NoOp receives the remainder of their bond, assuming losses due to inactivity penalties do not exceed the bond itself. **Validator Was Slashed**: @@ -130,9 +163,37 @@ The Guardians then execute `batchHandleWithdrawals()` on the `PufferProtocol`, w In all scenarios, the `_numberOfActivePufferValidators` count on the `PufferOracleV2` contract is decremented atomically, reducing the locked ETH amount by 32 ETH per exited validator to maintain the pufETH conversion rate. +### Depositing Validation Time + +Valiadtion time gives the node operator ability to keep operating their validator. If they run out of validation time, they will be ejected from the beacon chain. To prevent that from happening, they can deposit validation time. In the previous iteration of the protocol, validation time was represented as ERC20 token Validator Tickets (VTs). This is no longer the case, and the protocol now uses native ETH to represent validation time. The drawback of the previous design was that the node operator was left with VT tokens if ejected early because of the liquidity need for the protocol, and that forced them to sell their VT tokens on secondary markets for a lower price. The new design returns the ETH they deposit to the protocol, if they are ejected early. If they want to keep operating their validator, they can deposit more validation time. To do that, they can call the `depositValidationTime` function and send some amount of ETH to the protocol. That ETH is accounted for in the `PufferProtocol` contract, and the node operator can withdraw if when they exit all of their validators. + +```mermaid +sequenceDiagram + participant NodeOperator + participant PufferBackend + participant PufferProtocol + + rect rgb(0, 208, 255) + NodeOperator->>PufferBackend: Request VT consumption data (API CALL) + PufferBackend-->>NodeOperator: Return VT consumption & signatures + end + + rect rgb(25, 202, 84) + NodeOperator->>PufferProtocol: depositValidationTime(node, vtConsumptionAmount, vtConsumptionSignature) + PufferProtocol->>PufferProtocol: _settleVTAccounting() + end +``` + ## Managing Validator Tickets (VT) #### Understanding VT Consumption -Each validator operated by a NoOp consumes one VT per day. While the `getValidatorTicketsBalance()` function returns the total amount of VTs initially deposited by the NoOp, it does not reflect the real-time balance of VTs. This is due to the prohibitive gas costs associated with continually updating VT balances on-chain. + +Each validator operated by a NoOp consumes one VT per day. The VT consumption is tracked off-chain by the Guardians and verified through signatures. When registering a new validator, the NoOp must: + +1. Query the Puffer Backend API to obtain: + - Total epochs validated by their active validators + - Guardian signatures verifying this data + +This off-chain tracking approach is used to minimize gas costs while maintaining accurate VT consumption records. The `depositValidationTime` function is used to deposit validation time to the protocol. It is important to note that the calculation of the legaxy VT / new Validation Time is done off-chain, and per epoch. Currently, 1 day is equivalent to 225 epochs. #### Off-Chain Tracking and Visualizing VTs To efficiently manage VT consumption without incurring high on-chain costs, Guardians track VT usage off-chain. NoOps can access up-to-date VT consumption information through frontend interfaces, which provide a clear view of their current VT status. @@ -141,12 +202,12 @@ To efficiently manage VT consumption without incurring high on-chain costs, Guar Maintaining active validators requires more than just the initial deposit of a minimum of 28 VTs at registration. To ensure continuous operation and prevent ejection from the network, NoOp must periodically top up their VT balance. This is crucial as running out of VTs could lead to a validator being deactivated. #### Depositing Additional VTs -NoOps can replenish their VT supply by executing the `depositValidatorTickets(permit, nodeOperator)` function. This allows them to add VTs to their account, ensuring their validators can continue to operate without interruption. Note that the `PufferProtocol` tracks validators by wallet address, so only one function call is needed to top up VTs across all of your validators. +NoOps can replenish their VT supply by executing the `depositValidatorTickets(permit, nodeOperator)` function (legacy function) or the `depositValidationTime(node, vtConsumptionAmount, vtConsumptionSignature)`. This allows them to add VTs/Validation Time to their account, ensuring their validators can continue to operate without interruption. Note that the `PufferProtocol` tracks validators by wallet address, so only one function call is needed to top up VTs across all of your validators. It's important for NoOps to monitor their VT consumption regularly and respond proactively to avoid disruptions in their validator operations. #### Withdrawing VTs -Since VT consumption is tracked off-chain, withdrawing excess VTs, `withdrawValidatorTickets` can only be called when the NoOp has no active or pending validators. In future protocol upgrades, ZKPs will be used to allow VTs to be withdrawn while validators are still active. +Since VT consumption is tracked off-chain, withdrawing excess VTs, `withdrawValidatorTickets` can only be called when the NoOp has no active or pending validators (legacy function), the same logic applies to `withdrawValidationTime`. In future protocol upgrades, ZKPs will be used to allow VTs to be withdrawn while validators are still active. ## Validator Rewards in Puffer #### Overview of Rewards @@ -156,7 +217,7 @@ In the Puffer protocol, NoOps receive 100% of the consensus and execution reward When NoOps employ tools like MEV-Boost, execution rewards are directly sent to their designated wallet addresses. This is specified through the `fee recipient` parameter. Unlike other protocols there is no need to share these rewards with the protocol. #### Consensus Rewards -Consensus rewards are directed to the validators’ withdrawal credentials, which are linked to `EigenPods`. These rewards accumulate and, following an upcoming EigenLayer upgrade that improves partial withdrawal gas-efficiency, will be accessible for claiming through the [PufferModules](./PufferModule.md#consensus-rewards). +Consensus rewards are directed to the validators' withdrawal credentials, which are linked to `EigenPods`. These rewards accumulate and, following an upcoming EigenLayer upgrade that improves partial withdrawal gas-efficiency, will be accessible for claiming through the [PufferModules](./PufferModule.md#consensus-rewards). #### Restaking Rewards -Beyond the direct rewards from consensus and execution, Puffer validators also benefit from a share of the protocol's restaking rewards. Similar to consensus rewards, these restaking rewards are set to become claimable in future updates to EigenLayer, enhancing the overall profitability and incentive for NoOps within the Puffer ecosystem. \ No newline at end of file +Restaking rewards are claimed by the Puffer, they are periodically converted to ETH and then deposited to PufferVault. That is how Puffer is able to achieve higher yield compared to other protocols. \ No newline at end of file diff --git a/mainnet-contracts/foundry.toml b/mainnet-contracts/foundry.toml index bae8f562..59c2091a 100644 --- a/mainnet-contracts/foundry.toml +++ b/mainnet-contracts/foundry.toml @@ -29,7 +29,7 @@ optimizer = true optimizer_runs = 200 evm_version = "cancun" # is live on mainnet seed = "0x1337" -solc = "0.8.28" +solc = "0.8.30" # via_ir = true [fmt] diff --git a/mainnet-contracts/script/DeployEverything.s.sol b/mainnet-contracts/script/DeployEverything.s.sol index a32ea88e..4ec8cdda 100644 --- a/mainnet-contracts/script/DeployEverything.s.sol +++ b/mainnet-contracts/script/DeployEverything.s.sol @@ -51,8 +51,11 @@ contract DeployEverything is BaseScript { puffETHDeployment.accessManager, guardiansDeployment.guardianModule, puffETHDeployment.pufferVault ); - PufferProtocolDeployment memory pufferDeployment = - new DeployPuffer().run(guardiansDeployment, puffETHDeployment.pufferVault, pufferOracle); + address revenueDepositor = _deployRevenueDepositor(puffETHDeployment); + + PufferProtocolDeployment memory pufferDeployment = new DeployPuffer().run( + guardiansDeployment, puffETHDeployment.pufferVault, pufferOracle, payable(revenueDepositor) + ); pufferDeployment.pufferDepositor = puffETHDeployment.pufferDepositor; pufferDeployment.pufferVault = puffETHDeployment.pufferVault; @@ -61,7 +64,6 @@ contract DeployEverything is BaseScript { pufferDeployment.timelock = puffETHDeployment.timelock; BridgingDeployment memory bridgingDeployment = new DeployPufETHBridging().run(puffETHDeployment); - address revenueDepositor = _deployRevenueDepositor(puffETHDeployment); pufferDeployment.revenueDepositor = revenueDepositor; new UpgradePufETH().run(puffETHDeployment, pufferOracle, revenueDepositor); diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index ea294c78..c534db05 100644 --- a/mainnet-contracts/script/DeployPuffer.s.sol +++ b/mainnet-contracts/script/DeployPuffer.s.sol @@ -68,11 +68,12 @@ contract DeployPuffer is BaseScript { address treasury; address operationsMultisig; - function run(GuardiansDeployment calldata guardiansDeployment, address pufferVault, address oracle) - public - broadcast - returns (PufferProtocolDeployment memory) - { + function run( + GuardiansDeployment calldata guardiansDeployment, + address pufferVault, + address oracle, + address payable revenueDepositor + ) public broadcast returns (PufferProtocolDeployment memory) { accessManager = AccessManager(guardiansDeployment.accessManager); if (isMainnet()) { @@ -161,7 +162,8 @@ contract DeployPuffer is BaseScript { guardianModule: GuardianModule(payable(guardiansDeployment.guardianModule)), moduleManager: address(moduleManagerProxy), oracle: IPufferOracleV2(oracle), - beaconDepositContract: getStakingContract() + beaconDepositContract: getStakingContract(), + pufferRevenueDistributor: payable(revenueDepositor) }); } diff --git a/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol b/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol index d7fba15d..e4c8eb4a 100644 --- a/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol +++ b/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol @@ -29,7 +29,8 @@ contract DeployPufferProtocolImplementation is DeployerHelper { guardianModule: GuardianModule(payable(_getGuardianModule())), moduleManager: _getPufferModuleManager(), oracle: IPufferOracleV2(_getPufferOracle()), - beaconDepositContract: _getBeaconDepositContract() + beaconDepositContract: _getBeaconDepositContract(), + pufferRevenueDistributor: payable(_getRevenueDepositor()) }) ); diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index d110e974..c9087d43 100644 --- a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol +++ b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol @@ -107,23 +107,7 @@ contract GenerateBLSKeysAndRegisterValidators is Script { deprecated_raveEvidence: new bytes(0) }); - Permit memory pufETHPermit = _signPermit({ - to: protocolAddress, - amount: 2 ether, // Hardcoded to 2 pufETH - nonce: pufETH.nonces(msg.sender), - deadline: block.timestamp + 12 hours, - domainSeparator: pufETH.DOMAIN_SEPARATOR() - }); - - Permit memory vtPermit = _signPermit({ - to: protocolAddress, - amount: vtAmount * 1 ether, // Upscale to 10**18 - nonce: validatorTicket.nonces(msg.sender), - deadline: block.timestamp + 12 hours, - domainSeparator: validatorTicket.DOMAIN_SEPARATOR() - }); - - IPufferProtocol(protocolAddress).registerValidatorKey(validatorData, moduleName, pufETHPermit, vtPermit); + IPufferProtocol(protocolAddress).registerValidatorKey(validatorData, moduleName, 0, new bytes[](0)); registeredPubKeys.push(validatorData.blsPubKey); } diff --git a/mainnet-contracts/script/SetupAccess.s.sol b/mainnet-contracts/script/SetupAccess.s.sol index eb077ca6..83c0305d 100644 --- a/mainnet-contracts/script/SetupAccess.s.sol +++ b/mainnet-contracts/script/SetupAccess.s.sol @@ -325,11 +325,13 @@ contract SetupAccess is BaseScript { ROLE_ID_OPERATIONS_PAYMASTER ); - bytes4[] memory publicSelectors = new bytes4[](4); + bytes4[] memory publicSelectors = new bytes4[](6); publicSelectors[0] = PufferProtocol.registerValidatorKey.selector; publicSelectors[1] = PufferProtocol.depositValidatorTickets.selector; publicSelectors[2] = PufferProtocol.withdrawValidatorTickets.selector; publicSelectors[3] = PufferProtocol.revertIfPaused.selector; + publicSelectors[4] = PufferProtocol.depositValidationTime.selector; + publicSelectors[5] = PufferProtocol.withdrawValidationTime.selector; calldatas[2] = abi.encodeWithSelector( AccessManager.setTargetFunctionRole.selector, diff --git a/mainnet-contracts/src/PufferOracleV2.sol b/mainnet-contracts/src/PufferOracleV2.sol index a68e32b1..89367084 100644 --- a/mainnet-contracts/src/PufferOracleV2.sol +++ b/mainnet-contracts/src/PufferOracleV2.sol @@ -58,7 +58,7 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { PUFFER_VAULT = vault; _totalNumberOfValidators = 927122; // Oracle will be updated with the correct value _epochNumber = 268828; // Oracle will be updated with the correct value - _setMintPrice(0.01 ether); + _setMintPrice(9803921568628); // This is now price per epoch, and not per day } /** diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 12caaeb4..877a720c 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -23,6 +23,8 @@ import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; +import { NoncesUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/NoncesUpgradeable.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /** * @title PufferProtocol @@ -31,7 +33,13 @@ import { PufferModule } from "./PufferModule.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ -contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolStorage { +contract PufferProtocol is + IPufferProtocol, + AccessManagedUpgradeable, + UUPSUpgradeable, + PufferProtocolStorage, + NoncesUpgradeable +{ /** * @dev Helper struct for the full withdrawals accounting * The amounts of VT and pufETH to burn at the end of the withdrawal @@ -58,7 +66,18 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad /** * @dev ETH Amount required to be deposited as a bond */ - uint256 internal constant VALIDATOR_BOND = 2 ether; + uint256 internal constant _VALIDATOR_BOND = 1.5 ether; + + /** + * @dev Minimum validation time in epochs + * Roughly: 30 days * 225 epochs per day = 6750 epochs + */ + uint256 internal constant _MINIMUM_EPOCHS_VALIDATION = 6750; + + /** + * @dev Number of epochs per day + */ + uint256 internal constant _EPOCHS_PER_DAY = 225; /** * @dev Default "PUFFER_MODULE_0" module @@ -95,13 +114,19 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ IBeaconDepositContract public immutable override BEACON_DEPOSIT_CONTRACT; + /** + * @inheritdoc IPufferProtocol + */ + address payable public immutable PUFFER_REVENUE_DISTRIBUTOR; + constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, address moduleManager, ValidatorTicket validatorTicket, IPufferOracleV2 oracle, - address beaconDepositContract + address beaconDepositContract, + address payable pufferRevenueDistributor ) { GUARDIAN_MODULE = guardianModule; PUFFER_VAULT = PufferVaultV5(payable(address(pufferVault))); @@ -109,6 +134,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad VALIDATOR_TICKET = validatorTicket; PUFFER_ORACLE = oracle; BEACON_DEPOSIT_CONTRACT = IBeaconDepositContract(beaconDepositContract); + PUFFER_REVENUE_DISTRIBUTOR = pufferRevenueDistributor; _disableInitializers(); } @@ -121,8 +147,8 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad } __AccessManaged_init(accessManager); _createPufferModule(_PUFFER_MODULE_0); - _changeMinimumVTAmount(28 ether); // 28 Validator Tickets - _setVTPenalty(10 ether); // 10 Validator Tickets + _changeMinimumVTAmount(30 * _EPOCHS_PER_DAY); // 30 days worth of ETH is the minimum VT amount + _setVTPenalty(10 * _EPOCHS_PER_DAY); // 10 days worth of ETH is the VT penalty } /** @@ -144,10 +170,37 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad VALIDATOR_TICKET.transferFrom(msg.sender, address(this), permit.amount); ProtocolStorage storage $ = _getPufferProtocolStorage(); - $.nodeOperatorInfo[node].vtBalance += SafeCast.toUint96(permit.amount); + $.nodeOperatorInfo[node].deprecated_vtBalance += SafeCast.toUint96(permit.amount); emit ValidatorTicketsDeposited(node, msg.sender, permit.amount); } + /** + * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`) + * This ETH is used as a VT payment. + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + */ + function depositValidationTime(address node, uint256 vtConsumptionAmount, bytes[] calldata vtConsumptionSignature) + external + payable + restricted + { + require(node != address(0), InvalidAddress()); + require(msg.value > 0, InvalidETHAmount()); + + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + _settleVTAccounting({ + $: $, + node: node, + totalEpochsValidated: vtConsumptionAmount, + vtConsumptionSignature: vtConsumptionSignature, + deprecated_burntVTs: 0 + }); + + $.nodeOperatorInfo[node].validationTime += SafeCast.toUint96(msg.value); + emit ValidationTimeDeposited({ node: node, ethAmount: msg.value }); + } + /** * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol @@ -166,7 +219,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // Reverts if insufficient balance // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[msg.sender].vtBalance -= amount; + $.nodeOperatorInfo[msg.sender].deprecated_vtBalance -= amount; // slither-disable-next-line unchecked-transfer VALIDATOR_TICKET.transfer(recipient, amount); @@ -175,69 +228,93 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad } /** - * @inheritdoc IPufferProtocol + * @notice New function that allows the transaction sender (node operator) to withdraw WETH to a recipient (use this instead of `withdrawValidatorTickets`) + * The Validation time can be withdrawn if there are no active or pending validators + * The WETH is sent to the recipient + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + */ + function withdrawValidationTime(uint96 amount, address recipient) external restricted { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + // Node operator can only withdraw if they have no active or pending validators + // In the future, we plan to allow node operators to withdraw VTs even if they have active/pending validators. + if ( + $.nodeOperatorInfo[msg.sender].activeValidatorCount + $.nodeOperatorInfo[msg.sender].pendingValidatorCount + != 0 + ) { + revert ActiveOrPendingValidatorsExist(); + } + + // Reverts if insufficient balance + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[msg.sender].validationTime -= amount; + + // WETH is a contract that has a fallback function that accepts ETH, and never reverts + address weth = PUFFER_VAULT.asset(); + weth.call{ value: amount }(""); + // Transfer WETH to the recipient + ERC20(weth).transfer(recipient, amount); + + emit ValidationTimeWithdrawn(msg.sender, recipient, amount); + } + + /** + * @notice Registers a validator key and consumes the ETH for the validation time for the other active validators. * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ function registerValidatorKey( ValidatorKeyData calldata data, bytes32 moduleName, - Permit calldata pufETHPermit, - Permit calldata vtPermit + uint256 totalEpochsValidated, + bytes[] calldata vtConsumptionSignature ) external payable restricted { ProtocolStorage storage $ = _getPufferProtocolStorage(); - // Revert if the permit amounts are non zero, but the msg.value is also non zero - if (vtPermit.amount != 0 && pufETHPermit.amount != 0 && msg.value > 0) { - revert InvalidETHAmount(); - } - _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); - // If the node operator is paying for the bond in ETH and wants to transfer VT from their wallet, the ETH amount they send must be equal the bond amount - if (vtPermit.amount != 0 && pufETHPermit.amount == 0 && msg.value != VALIDATOR_BOND) { - revert InvalidETHAmount(); - } + uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); - uint256 vtPayment = pufETHPermit.amount == 0 ? msg.value - VALIDATOR_BOND : msg.value; + // The node operator must deposit 1.5 ETH or more + minimum validation time for ~30 days + // At the moment thats roughly 30 days * 225 (there is rougly 225 epochs per day) + uint256 minimumETHRequired = _VALIDATOR_BOND + (_MINIMUM_EPOCHS_VALIDATION * epochCurrentPrice); - uint256 receivedVtAmount; - // If the VT permit amount is zero, that means that the user is paying for VT with ETH - if (vtPermit.amount == 0) { - receivedVtAmount = VALIDATOR_TICKET.purchaseValidatorTicket{ value: vtPayment }(address(this)); - } else { - _callPermit(address(VALIDATOR_TICKET), vtPermit); - receivedVtAmount = vtPermit.amount; + emit ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - _VALIDATOR_BOND) }); - // slither-disable-next-line unchecked-transfer - VALIDATOR_TICKET.transferFrom(msg.sender, address(this), receivedVtAmount); - } + require(msg.value >= minimumETHRequired, InvalidETHAmount()); - if (receivedVtAmount < $.minimumVtAmount) { - revert InvalidVTAmount(); - } + _settleVTAccounting({ + $: $, + node: msg.sender, + totalEpochsValidated: totalEpochsValidated, + vtConsumptionSignature: vtConsumptionSignature, + deprecated_burntVTs: 0 + }); - uint256 bondAmount; + // The bond is converted to pufETH at the current exchange rate + uint256 pufETHBondAmount = PUFFER_VAULT.depositETH{ value: _VALIDATOR_BOND }(address(this)); - // If the pufETH permit amount is zero, that means that the user is paying the bond with ETH - if (pufETHPermit.amount == 0) { - // Mint pufETH by depositing ETH and store the bond amount - bondAmount = PUFFER_VAULT.depositETH{ value: VALIDATOR_BOND }(address(this)); - } else { - // Calculate the pufETH amount that we need to transfer from the user - bondAmount = PUFFER_VAULT.convertToShares(VALIDATOR_BOND); - _callPermit(address(PUFFER_VAULT), pufETHPermit); + uint256 pufferModuleIndex = $.pendingValidatorIndices[moduleName]; - // slither-disable-next-line unchecked-transfer - PUFFER_VAULT.transferFrom(msg.sender, address(this), bondAmount); + // No need for SafeCast + $.validators[moduleName][pufferModuleIndex] = Validator({ + pubKey: data.blsPubKey, + status: Status.PENDING, + module: address($.modules[moduleName]), + bond: uint96(pufETHBondAmount), + node: msg.sender + }); + + // Increment indices for this module and number of validators registered + unchecked { + $.nodeOperatorInfo[msg.sender].epochPrice = epochCurrentPrice; + $.nodeOperatorInfo[msg.sender].validationTime += (msg.value - _VALIDATOR_BOND); + ++$.nodeOperatorInfo[msg.sender].pendingValidatorCount; + ++$.pendingValidatorIndices[moduleName]; + ++$.moduleLimits[moduleName].numberOfRegisteredValidators; } - _storeValidatorInformation({ - $: $, - data: data, - pufETHAmount: bondAmount, - moduleName: moduleName, - vtAmount: receivedVtAmount - }); + emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); + emit ValidatorKeyRegistered(data.blsPubKey, pufferModuleIndex, moduleName); } /** @@ -276,6 +353,33 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad $.validators[moduleName][index].status = Status.ACTIVE; } + function _batchHandleWithdrawalsAccounting( + Withdrawals[] memory bondWithdrawals, + StoppedValidatorInfo[] calldata validatorInfos + ) internal { + // In this loop, we transfer back the bonds, and do the accounting that affects the exchange rate + for (uint256 i = 0; i < validatorInfos.length; ++i) { + // If the withdrawal amount is bigger than 32 ETH, we cap it to 32 ETH + // The excess is the rewards amount for that Node Operator + uint256 transferAmount = + validatorInfos[i].withdrawalAmount > 32 ether ? 32 ether : validatorInfos[i].withdrawalAmount; + //solhint-disable-next-line avoid-low-level-calls + (bool success,) = + PufferModule(payable(validatorInfos[i].module)).call(address(PUFFER_VAULT), transferAmount, ""); + if (!success) { + revert Failed(); + } + + // Skip the empty transfer (validator got slashed) + if (bondWithdrawals[i].pufETHAmount == 0) { + continue; + } + // slither-disable-next-line unchecked-transfer + PUFFER_VAULT.transfer(bondWithdrawals[i].node, bondWithdrawals[i].pufETHAmount); + } + // slither-disable-start calls-loop + } + /** * @inheritdoc IPufferProtocol * @dev Restricted to Puffer Paymaster @@ -311,11 +415,18 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // Get the burnAmount for the withdrawal at the current exchange rate uint256 burnAmount = _getBondBurnAmount({ validatorInfo: validatorInfos[i], validatorBondAmount: bondAmount }); - uint256 vtBurnAmount = _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i]); + uint256 vtBurnAmount = _getVTBurnAmount($, validatorInfos[i]); + + // We need to scope the variables to avoid stack too deep errors + { + uint256 epochValidated = validatorInfos[i].totalEpochsValidated; + bytes[] calldata vtConsumptionSignature = validatorInfos[i].vtConsumptionSignature; + burnAmounts.vt += + _useVTOrValidationTime($, validator, vtBurnAmount, epochValidated, vtConsumptionSignature); + } // Update the burnAmounts burnAmounts.pufETH += burnAmount; - burnAmounts.vt += vtBurnAmount; // Store the withdrawal amount for that node operator // nosemgrep basic-arithmetic-underflow @@ -333,7 +444,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad _decreaseNumberOfRegisteredValidators($, validatorInfos[i].moduleName); // Storage VT and the active validator count update for the Node Operator // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); --$.nodeOperatorInfo[validator.node].activeValidatorCount; delete validator.node; @@ -349,27 +459,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // Deduct 32 ETH from the `lockedETHAmount` on the PufferOracle PUFFER_ORACLE.exitValidators(validatorInfos.length); - // In this loop, we transfer back the bonds, and do the accounting that affects the exchange rate - for (uint256 i = 0; i < validatorInfos.length; ++i) { - // If the withdrawal amount is bigger than 32 ETH, we cap it to 32 ETH - // The excess is the rewards amount for that Node Operator - uint256 transferAmount = - validatorInfos[i].withdrawalAmount > 32 ether ? 32 ether : validatorInfos[i].withdrawalAmount; - //solhint-disable-next-line avoid-low-level-calls - (bool success,) = - PufferModule(payable(validatorInfos[i].module)).call(address(PUFFER_VAULT), transferAmount, ""); - if (!success) { - revert Failed(); - } - - // Skip the empty transfer (validator got slashed) - if (bondWithdrawals[i].pufETHAmount == 0) { - continue; - } - // slither-disable-next-line unchecked-transfer - PUFFER_VAULT.transfer(bondWithdrawals[i].node, bondWithdrawals[i].pufETHAmount); - } - // slither-disable-start calls-loop + _batchHandleWithdrawalsAccounting(bondWithdrawals, validatorInfos); } /** @@ -390,11 +480,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad guardianEOASignatures: guardianEOASignatures }); - uint256 vtPenalty = $.vtPenalty; - // Burn VT penalty amount from the Node Operator - VALIDATOR_TICKET.burn(vtPenalty); - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[node].vtBalance -= SafeCast.toUint96(vtPenalty); + uint256 vtPricePerEpoch = PUFFER_ORACLE.getValidatorTicketPrice(); + + $.nodeOperatorInfo[node].validationTime -= ($.vtPenaltyEpochs * vtPricePerEpoch); --$.nodeOperatorInfo[node].pendingValidatorCount; // Change the status of that validator @@ -451,7 +539,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ function getVTPenalty() external view returns (uint256) { ProtocolStorage storage $ = _getPufferProtocolStorage(); - return $.vtPenalty; + return $.vtPenaltyEpochs; } /** @@ -585,8 +673,12 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ function getValidatorTicketsBalance(address owner) public view returns (uint256) { ProtocolStorage storage $ = _getPufferProtocolStorage(); + return $.nodeOperatorInfo[owner].deprecated_vtBalance; + } - return $.nodeOperatorInfo[owner].vtBalance; + function getValidationTime(address owner) public view returns (uint256) { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + return $.nodeOperatorInfo[owner].validationTime; } /** @@ -611,36 +703,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ function revertIfPaused() external restricted { } - function _storeValidatorInformation( - ProtocolStorage storage $, - ValidatorKeyData calldata data, - uint256 pufETHAmount, - bytes32 moduleName, - uint256 vtAmount - ) internal { - uint256 pufferModuleIndex = $.pendingValidatorIndices[moduleName]; - - // No need for SafeCast - $.validators[moduleName][pufferModuleIndex] = Validator({ - pubKey: data.blsPubKey, - status: Status.PENDING, - module: address($.modules[moduleName]), - bond: uint96(pufETHAmount), - node: msg.sender - }); - - $.nodeOperatorInfo[msg.sender].vtBalance += SafeCast.toUint96(vtAmount); - - // Increment indices for this module and number of validators registered - unchecked { - ++$.nodeOperatorInfo[msg.sender].pendingValidatorCount; - ++$.pendingValidatorIndices[moduleName]; - ++$.moduleLimits[moduleName].numberOfRegisteredValidators; - } - emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); - emit ValidatorKeyRegistered(data.blsPubKey, pufferModuleIndex, moduleName); - } - function _setValidatorLimitPerModule(bytes32 moduleName, uint128 limit) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); if (limit < $.moduleLimits[moduleName].numberOfRegisteredValidators) { @@ -655,8 +717,8 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad if (newPenaltyAmount > $.minimumVtAmount) { revert InvalidVTAmount(); } - emit VTPenaltyChanged($.vtPenalty, newPenaltyAmount); - $.vtPenalty = newPenaltyAmount; + emit VTPenaltyChanged($.vtPenaltyEpochs, newPenaltyAmount); + $.vtPenaltyEpochs = newPenaltyAmount; } function _setModuleWeights(bytes32[] memory newModuleWeights) internal { @@ -697,7 +759,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad function _changeMinimumVTAmount(uint256 newMinimumVtAmount) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); - if (newMinimumVtAmount < $.vtPenalty) { + if (newMinimumVtAmount < $.vtPenaltyEpochs) { revert InvalidVTAmount(); } emit MinimumVTAmountChanged($.minimumVtAmount, newMinimumVtAmount); @@ -751,32 +813,114 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad module.callStake({ pubKey: validatorPubKey, signature: validatorSignature, depositDataRoot: depositDataRoot }); } - function _getVTBurnAmount(ProtocolStorage storage $, address node, StoppedValidatorInfo calldata validatorInfo) + function _useVTOrValidationTime( + ProtocolStorage storage $, + Validator storage validator, + uint256 vtBurnAmount, + uint256 totalEpochsValidated, + bytes[] calldata vtConsumptionSignature + ) internal returns (uint256 burnedAmount) { + // Burn the VT first, then fallback to ETH from the node operator + uint256 nodeVTBalance = $.nodeOperatorInfo[validator.node].deprecated_vtBalance; + + // If the node operator has VT, we burn it first + if (nodeVTBalance > 0) { + if (nodeVTBalance >= vtBurnAmount) { + // Burn the VT first, and update the node operator VT balance + burnedAmount = vtBurnAmount; + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[validator.node].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); + + return burnedAmount; + } + + // If the node operator has less VT than the amount to burn, we burn all of it, and we use the validation time + burnedAmount = nodeVTBalance; + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[validator.node].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); + + _settleVTAccounting({ + $: $, + node: validator.node, + totalEpochsValidated: totalEpochsValidated, + vtConsumptionSignature: vtConsumptionSignature, + deprecated_burntVTs: nodeVTBalance + }); + + return burnedAmount; + } + + // If the node operator has no VT, we use the validation time + _settleVTAccounting({ + $: $, + node: validator.node, + totalEpochsValidated: totalEpochsValidated, + vtConsumptionSignature: vtConsumptionSignature, + deprecated_burntVTs: 0 + }); + } + + function _settleVTAccounting( + ProtocolStorage storage $, + address node, + uint256 totalEpochsValidated, + bytes[] calldata vtConsumptionSignature, + uint256 deprecated_burntVTs + ) internal { + // There is nothing to settle if this is the first validator for the node operator + if ($.nodeOperatorInfo[node].activeValidatorCount + $.nodeOperatorInfo[node].pendingValidatorCount == 0) { + return; + } + + // We have no way of getting the present consumed amount for the other validators on-chain, so we use Puffer Backend service to get that amount and a signature from the service + bytes32 messageHash = keccak256(abi.encode(node, totalEpochsValidated, _useNonce(node))); + + GUARDIAN_MODULE.validateGuardiansEOASignatures({ + eoaSignatures: vtConsumptionSignature, + signedMessageHash: messageHash + }); + + uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); + + uint256 meanPrice = ($.nodeOperatorInfo[node].epochPrice + epochCurrentPrice) / 2; + + uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[node].totalEpochsValidated; + + uint256 validatorTicketsBurnt = deprecated_burntVTs * 225 / 1 ether; // 1 VT = 1 DAY = 225 Epochs + + uint256 amountToConsume = + (totalEpochsValidated - previousTotalEpochsValidated - validatorTicketsBurnt) * meanPrice; + + if (amountToConsume <= $.vtPenaltyEpochs * meanPrice) { + amountToConsume = $.vtPenaltyEpochs * meanPrice; + } + + // Update the current epoch VT price for the node operator + $.nodeOperatorInfo[node].epochPrice = epochCurrentPrice; + $.nodeOperatorInfo[node].totalEpochsValidated = totalEpochsValidated; + $.nodeOperatorInfo[node].validationTime -= amountToConsume; + + address weth = PUFFER_VAULT.asset(); + + // WETH is a contract that has a fallback function that accepts ETH, and never reverts + weth.call{ value: amountToConsume }(""); + + // Transfer WETH to the Revenue Distributor, it will be slow released to the PufferVault + ERC20(weth).transfer(PUFFER_REVENUE_DISTRIBUTOR, amountToConsume); + } + + function _getVTBurnAmount(ProtocolStorage storage $, StoppedValidatorInfo calldata validatorInfo) internal view returns (uint256) { - uint256 validatedEpochs = validatorInfo.endEpoch - validatorInfo.startEpoch; + uint256 validatedEpochs = validatorInfo.totalEpochsValidated; // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up uint256 vtBurnAmount = validatedEpochs * 4444444444444445; - uint256 minimumVTAmount = $.minimumVtAmount; - uint256 nodeVTBalance = $.nodeOperatorInfo[node].vtBalance; - - // If the VT burn amount is less than the minimum VT amount that means that the node operator exited early - // If we don't penalize it, the node operator can exit early and re-register with the same VTs. - // By doing that, they can lower the APY for the pufETH holders - if (minimumVTAmount > vtBurnAmount) { - // Case when the node operator registered the validator but afterwards the DAO increases the minimum VT amount - if (nodeVTBalance < minimumVTAmount) { - return nodeVTBalance; - } - - return minimumVTAmount; - } - - return vtBurnAmount; + // Return the bigger of the two + return vtBurnAmount > $.minimumVtAmount ? vtBurnAmount : $.minimumVtAmount; } function _callPermit(address token, Permit calldata permitData) internal { diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index d28c83c7..8cb5bd08 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -80,6 +80,12 @@ interface IPufferProtocol { */ event NumberOfRegisteredValidatorsChanged(bytes32 indexed moduleName, uint256 newNumberOfRegisteredValidators); + /** + * @notice Emitted when the validation time is deposited + * @dev Signature "0xdab70193ab2d6948fc2f6da9e82794bf650dc3099e042b6510f9e5019735545c" + */ + event ValidationTimeDeposited(address indexed node, uint256 ethAmount); + /** * @notice Emitted when the new Puffer module is created * @dev Signature "0x8ad2a9260a8e9a01d1ccd66b3875bcbdf8c4d0c552bc51a7d2125d4146e1d2d6" @@ -116,6 +122,12 @@ interface IPufferProtocol { */ event ValidatorTicketsWithdrawn(address indexed node, address indexed recipient, uint256 amount); + /** + * @notice Emitted when Validation Time is withdrawn from the protocol + * @dev Signature "0xba152c9819ee6cbe5243df48eb44ae038608f08bc7e9e9042bfc32f996257781" + */ + event ValidationTimeWithdrawn(address indexed node, address indexed recipient, uint256 amount); + /** * @notice Emitted when the guardians decide to skip validator provisioning for `moduleName` * @dev Signature "0x088dc5dc64f3e8df8da5140a284d3018a717d6b009e605513bb28a2b466d38ee" @@ -173,6 +185,7 @@ interface IPufferProtocol { /** * @notice Returns Penalty for submitting a bad validator registration * @dev If the guardians skip a validator, the node operator will be penalized + * @return Number of epochs to burn for a penalty if a validator is skipped. epochs * vtPricePerEpoch = penalty in ETH * /// todo write any possible reasons for skipping a validator, here and in skipValidator method */ function getVTPenalty() external view returns (uint256); @@ -186,11 +199,13 @@ interface IPufferProtocol { /** * @notice Deposits Validator Tickets for the `node` + * DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function depositValidatorTickets(Permit calldata permit, address node) external; /** * @notice Withdraws the `amount` of Validator Tickers from the `msg.sender` to the `recipient` + * DEPRECATED - This method is deprecated and will be removed in the future upgrade * @dev Each active validator requires node operator to have at least `minimumVtAmount` locked */ function withdrawValidatorTickets(uint96 amount, address recipient) external; @@ -224,6 +239,7 @@ interface IPufferProtocol { /** * @notice Returns the Validator ticket ERC20 token + * DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function VALIDATOR_TICKET() external view returns (ValidatorTicket); @@ -247,6 +263,11 @@ interface IPufferProtocol { */ function BEACON_DEPOSIT_CONTRACT() external view returns (IBeaconDepositContract); + /** + * @notice Returns the Puffer Revenue Distributor + */ + function PUFFER_REVENUE_DISTRIBUTOR() external view returns (address payable); + /** * @notice Returns the current module weights */ @@ -302,20 +323,14 @@ interface IPufferProtocol { /** * @notice Registers a new validator key in a `moduleName` queue with a permit * @dev There is a queue per moduleName and it is FIFO - * - * If you are depositing without the permit, make sure to .approve pufETH to PufferProtocol - * and populate permit.amount with the correct amount - * * @param data The validator key data * @param moduleName The name of the module - * @param pufETHPermit The permit for the pufETH - * @param vtPermit The permit for the ValidatorTicket */ function registerValidatorKey( ValidatorKeyData calldata data, bytes32 moduleName, - Permit calldata pufETHPermit, - Permit calldata vtPermit + uint256 vtConsumptionAmount, + bytes[] calldata vtConsumptionSignature ) external payable; /** @@ -332,6 +347,7 @@ interface IPufferProtocol { * @notice Returns the amount of Validator Tickets locked in the PufferProtocol for the `owner` * The real VT balance may be different from the balance in the PufferProtocol * When the Validator is exited, the VTs are burned and the balance is decreased + * DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function getValidatorTicketsBalance(address owner) external returns (uint256); @@ -349,6 +365,7 @@ interface IPufferProtocol { /** * @notice Returns the minimum amount of Validator Tokens to run a validator + * Returns the minimum amount of Epochs a validator needs to run */ function getMinimumVtAmount() external view returns (uint256); diff --git a/mainnet-contracts/src/struct/NodeInfo.sol b/mainnet-contracts/src/struct/NodeInfo.sol index c225c188..ac63c7d3 100644 --- a/mainnet-contracts/src/struct/NodeInfo.sol +++ b/mainnet-contracts/src/struct/NodeInfo.sol @@ -7,5 +7,9 @@ pragma solidity >=0.8.0 <0.9.0; struct NodeInfo { uint64 activeValidatorCount; // Number of active validators uint64 pendingValidatorCount; // Number of pending validators (registered but not yet provisioned) - uint96 vtBalance; // Validator ticket balance + uint96 deprecated_vtBalance; // Validator ticket balance + // @dev The node operators deposit ETH, and that ETH is used to calculate the validation time for the node + uint256 validationTime; + uint256 epochPrice; + uint256 totalEpochsValidated; } diff --git a/mainnet-contracts/src/struct/ProtocolStorage.sol b/mainnet-contracts/src/struct/ProtocolStorage.sol index c87d18e2..93d2db4a 100644 --- a/mainnet-contracts/src/struct/ProtocolStorage.sol +++ b/mainnet-contracts/src/struct/ProtocolStorage.sol @@ -62,11 +62,10 @@ struct ProtocolStorage { */ uint256 minimumVtAmount; /** - * @dev Amount of VT tokens to burn for a validator penalty - * 1 VT = 1e18 + * @dev Amount of epochs to burn for a penalty if a validator is skipped * Slot 9 */ - uint256 vtPenalty; + uint256 vtPenaltyEpochs; } struct ModuleLimit { diff --git a/mainnet-contracts/src/struct/StoppedValidatorInfo.sol b/mainnet-contracts/src/struct/StoppedValidatorInfo.sol index dd1091a1..b98082aa 100644 --- a/mainnet-contracts/src/struct/StoppedValidatorInfo.sol +++ b/mainnet-contracts/src/struct/StoppedValidatorInfo.sol @@ -7,10 +7,6 @@ pragma solidity >=0.8.0 <0.9.0; struct StoppedValidatorInfo { ///@dev Module address. address module; - ///@dev Validator start epoch. - uint256 startEpoch; - ///@dev Validator stop epoch. - uint256 endEpoch; /// @dev Indicates whether the validator was slashed before stopping. bool wasSlashed; /// @dev Name of the module where the validator was participating. @@ -19,4 +15,8 @@ struct StoppedValidatorInfo { uint256 pufferModuleIndex; /// @dev Amount of funds withdrawn upon validator stoppage. uint256 withdrawalAmount; + /// @dev Total number of epochs validated by the validator. + uint256 totalEpochsValidated; + /// @dev Signature of the guardian module that consumed the validator tickets. + bytes[] vtConsumptionSignature; } diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index 99f53d9f..b94cb184 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -586,7 +586,7 @@ contract PufferProtocolHandler is Test { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( - validatorKeyData, moduleName, emptyPermit, emptyPermit + validatorKeyData, moduleName, 0, new bytes[](0) ); return (smoothingCommitment + bond); diff --git a/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol b/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol index 94e11a2b..df62b172 100644 --- a/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol +++ b/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol @@ -19,7 +19,8 @@ contract PufferProtocolMockUpgrade is PufferProtocol { address(0), ValidatorTicket(address(0)), IPufferOracleV2(address(0)), - address(0) + address(0), + payable(address(0)) ) { } } diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 40f723d7..d7bb6207 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -10,16 +10,39 @@ import { Status } from "../../src/struct/Status.sol"; import { Validator } from "../../src/struct/Validator.sol"; import { PufferProtocol } from "../../src/PufferProtocol.sol"; import { PufferModule } from "../../src/PufferModule.sol"; -import { ROLE_ID_DAO, ROLE_ID_OPERATIONS_PAYMASTER, ROLE_ID_OPERATIONS_MULTISIG } from "../../script/Roles.sol"; -import { Unauthorized } from "../../src/Errors.sol"; +import { PufferRevenueDepositor } from "../../src/PufferRevenueDepositor.sol"; +import { + ROLE_ID_DAO, + ROLE_ID_OPERATIONS_PAYMASTER, + ROLE_ID_OPERATIONS_MULTISIG, + ROLE_ID_OPERATIONS_COORDINATOR, + ROLE_ID_REVENUE_DEPOSITOR +} from "../../script/Roles.sol"; import { LibGuardianMessages } from "../../src/LibGuardianMessages.sol"; import { Permit } from "../../src/structs/Permit.sol"; import { ModuleLimit } from "../../src/struct/ProtocolStorage.sol"; import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol"; +import { NodeInfo } from "../../src/struct/NodeInfo.sol"; contract PufferProtocolTest is UnitTestHelper { using ECDSA for bytes32; + /** + * @dev New bond is reduced from 2 to 1.5 ETH + */ + uint256 BOND = 1.5 ether; + /** + * @dev Minimum validation time in epochs + * Roughly: 30 days * 225 epochs per day = 6750 epochs + */ + uint256 internal constant MINIMUM_EPOCHS_VALIDATION = 6750; + + // Eth has rougly 225 epochs per day + uint256 internal constant EPOCHS_PER_DAY = 225; + + // 1 VT is burned per 225 epochs + uint256 internal constant BURN_RATE_PER_EPOCH = 4444444444444445; + event ValidatorKeyRegistered(bytes pubKey, uint256 indexed, bytes32 indexed); event SuccessfullyProvisioned(bytes pubKey, uint256 indexed, bytes32 indexed); event ModuleWeightsChanged(bytes32[] oldWeights, bytes32[] newWeights); @@ -52,6 +75,8 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(address(this), 1000 ether); + vm.label(address(revenueDepositor), "RevenueDepositorProxy"); + // Setup roles bytes4[] memory selectors = new bytes4[](3); selectors[0] = PufferProtocol.createPufferModule.selector; @@ -65,8 +90,19 @@ contract PufferProtocolTest is UnitTestHelper { accessManager.grantRole(ROLE_ID_OPERATIONS_MULTISIG, address(this), 0); accessManager.grantRole(ROLE_ID_OPERATIONS_PAYMASTER, address(this), 0); accessManager.grantRole(ROLE_ID_OPERATIONS_MULTISIG, address(this), 0); + accessManager.grantRole(ROLE_ID_OPERATIONS_COORDINATOR, address(this), 0); + + // Grant revenue depositor roles to this contract for simplicity + bytes4[] memory revenueDepositorRole = new bytes4[](2); + revenueDepositorRole[0] = PufferRevenueDepositor.depositRevenue.selector; + revenueDepositorRole[1] = PufferRevenueDepositor.setRewardsDistributionWindow.selector; + accessManager.setTargetFunctionRole(address(revenueDepositor), revenueDepositorRole, ROLE_ID_REVENUE_DEPOSITOR); + accessManager.grantRole(ROLE_ID_REVENUE_DEPOSITOR, address(this), 0); + vm.stopPrank(); + revenueDepositor.setRewardsDistributionWindow(0); + _skipDefaultFuzzAddresses(); fuzzedAddressMapping[address(pufferProtocol)] = true; @@ -79,13 +115,24 @@ contract PufferProtocolTest is UnitTestHelper { // Setup function test_setup() public view { assertTrue(address(pufferProtocol.PUFFER_VAULT()) != address(0), "puffer vault address"); + assertTrue( + address(pufferProtocol.PUFFER_REVENUE_DISTRIBUTOR()) != address(0), "puffer revenue distributor address" + ); address module = pufferProtocol.getModuleAddress(PUFFER_MODULE_0); assertEq(PufferModule(payable(module)).NAME(), PUFFER_MODULE_0, "bad name"); } // Register validator key function test_register_validator_key() public { - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + + NodeInfo memory nodeInfo = pufferProtocol.getNodeInfo(address(this)); + assertEq(nodeInfo.activeValidatorCount, 0); + assertEq(nodeInfo.pendingValidatorCount, 1); + assertEq(nodeInfo.deprecated_vtBalance, 0); + assertEq(nodeInfo.validationTime, (30 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice())); // 30 days of VT + assertEq(nodeInfo.epochPrice, 9803921568628); // VT Price per epoch + assertEq(nodeInfo.totalEpochsValidated, 0); } // Empty queue should return NO_VALIDATORS @@ -97,8 +144,8 @@ contract PufferProtocolTest is UnitTestHelper { // Test Skipping the validator function test_skip_provisioning() public { - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("bob"), PUFFER_MODULE_0); + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("bob"), PUFFER_MODULE_0, 0); (bytes32 moduleName, uint256 idx) = pufferProtocol.getNextValidatorToProvision(); uint256 moduleSelectionIndex = pufferProtocol.getModuleSelectIndex(); @@ -153,7 +200,7 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); vm.expectRevert(IPufferProtocol.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( - validatorKeyData, bytes32("imaginary module"), emptyPermit, emptyPermit + validatorKeyData, bytes32("imaginary module"), 0, new bytes[](0) ); } @@ -161,53 +208,22 @@ contract PufferProtocolTest is UnitTestHelper { function test_register_with_non_whole_amount() public { bytes memory pubKey = _getPubKey(bytes32("charlie")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); uint256 amount = 5.11 ether; - pufferProtocol.registerValidatorKey{ value: amount }( - validatorKeyData, PUFFER_MODULE_0, emptyPermit, emptyPermit - ); + pufferProtocol.registerValidatorKey{ value: amount }(validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0)); assertEq( - validatorTicket.balanceOf(address(pufferProtocol)), - ((amount - 2 ether) * 1 ether) / vtPrice, - "VT after for pufferProtocol" + address(pufferProtocol).balance, + amount - 1.5 ether, + "protocol has the eth amount for VT, the bond is converted to pufETH" ); } - // If we are > burst threshold, treasury gets everything - function test_burst_threshold() external { - vm.roll(50401); - - _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); - _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); - _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); - - pufferOracle.setTotalNumberOfValidators( - 5, - 99999999, - _getGuardianEOASignatures( - LibGuardianMessages._getSetNumberOfValidatorsMessage({ numberOfValidators: 5, epochNumber: 99999999 }) - ) - ); - - uint256 sc = pufferOracle.getValidatorTicketPrice() * 30; - address treasury = validatorTicket.TREASURY(); - - uint256 balanceBefore = address(treasury).balance; - - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - - uint256 balanceAfter = address(treasury).balance; - - assertEq(balanceAfter, balanceBefore + sc, "treasury gets everything"); - } - // Set validator limit and try registering that many validators function test_fuzz_register_many_validators(uint8 numberOfValidatorsToProvision) external { for (uint256 i = 0; i < uint256(numberOfValidatorsToProvision); ++i) { vm.deal(address(this), 3 ether); - _registerValidatorKey(bytes32(i), PUFFER_MODULE_0); + _registerValidatorKey(address(this), bytes32(i), PUFFER_MODULE_0, 0); } } @@ -236,7 +252,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); pufferProtocol.registerValidatorKey{ value: vtPrice + 2 ether }( - validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit + validatorData, PUFFER_MODULE_0, 0, new bytes[](0) ); } @@ -262,7 +278,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( - validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit + validatorData, PUFFER_MODULE_0, 0, new bytes[](0) ); } @@ -277,7 +293,7 @@ contract PufferProtocolTest is UnitTestHelper { // If the deposit root is not bytes(0), it must match match the one returned from the beacon contract function test_provision_bad_deposit_hash() public { - _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(address(this), zeroPubKeyPart, PUFFER_MODULE_0, 0); bytes memory validatorSignature = _validatorSignature(); @@ -298,7 +314,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory bobPubKey = _getPubKey(bobPubKeyPart); // 1. validator - _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(address(this), zeroPubKeyPart, PUFFER_MODULE_0, 0); Validator memory validator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 0); assertTrue(validator.node == address(this), "node operator"); @@ -306,19 +322,19 @@ contract PufferProtocolTest is UnitTestHelper { // 2. validator vm.startPrank(bob); - _registerValidatorKey(bobPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(bob, bobPubKeyPart, PUFFER_MODULE_0, 0); vm.stopPrank(); // 3. validator vm.startPrank(alice); - _registerValidatorKey(alicePubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(alice, alicePubKeyPart, PUFFER_MODULE_0, 0); vm.stopPrank(); // 4. validator - _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(alice, zeroPubKeyPart, PUFFER_MODULE_0, 0); // 5. Validator - _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(alice, zeroPubKeyPart, PUFFER_MODULE_0, 0); assertEq(pufferProtocol.getPendingValidatorIndex(PUFFER_MODULE_0), 5, "next pending validator index"); @@ -372,13 +388,13 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(address(pufferVault), 10000 ether); - _registerValidatorKey(bytes32("bob"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("charlie"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("david"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("emma"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("benjamin"), EIGEN_DA); - _registerValidatorKey(bytes32("rocky"), CRAZY_GAINS); + _registerValidatorKey(address(this), bytes32("bob"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("charlie"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("david"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("emma"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("benjamin"), EIGEN_DA, 0); + _registerValidatorKey(address(this), bytes32("rocky"), CRAZY_GAINS, 0); (bytes32 nextModule, uint256 nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -409,7 +425,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.stopPrank(); // Now jason registers to EIGEN_DA - _registerValidatorKey(bytes32("jason"), EIGEN_DA); + _registerValidatorKey(address(this), bytes32("jason"), EIGEN_DA, 0); // If we query next validator, it should switch back to EIGEN_DA (because of the weighted selection) (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -470,231 +486,89 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectRevert(); pufferProtocol.registerValidatorKey{ value: type(uint256).max }( - validatorKeyData, PUFFER_MODULE_0, emptyPermit, emptyPermit + validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0) ); } - // Node operator can deposit Bond in pufETH - function test_register_pufETH_approve_buy_VT() external { + function test_register_validator_key_new_flow() external { bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); - - uint256 expectedMint = pufferVault.previewDeposit(1 ether); - assertGt(expectedMint, 0, "should expect more pufETH"); - - // Alice mints 2 ETH of pufETH - vm.startPrank(alice); - uint256 minted = pufferVault.depositETH{ value: 2 ether }(alice); - assertGt(minted, 0, "should mint pufETH"); - - // approve pufETH to pufferProtocol - pufferVault.approve(address(pufferProtocol), type(uint256).max); - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 2 ether, "2 pufETH before for alice"); - - // In this case, the only important data on permit is the amount - // Permit call will fail, but the amount is reused - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - Permit memory permit; - permit.amount = pufferVault.balanceOf(alice); - - // Get the smoothing commitment amount for 180 days - uint256 sc = pufferOracle.getValidatorTicketPrice() * 180; - - // Register validator key by paying SC in ETH and depositing bond in pufETH - vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); - pufferProtocol.registerValidatorKey{ value: sc }(data, PUFFER_MODULE_0, permit, emptyPermit); - // Alice has some dust in her wallet, because VT purchase changes the exchange rate, meaning pufETH is worth more - assertEq(pufferVault.balanceOf(alice), 3389455624732864, "3389455624732864 pufETH after for alice"); - uint256 protocolPufETHBalance = pufferVault.balanceOf(address(pufferProtocol)); - assertApproxEqRel(protocolPufETHBalance, 1.996607164 ether, pointZeroZeroTwo, "~1.996 pufETH after"); - assertApproxEqRel( - pufferVault.convertToAssets(protocolPufETHBalance), 2 ether, pointZeroZeroOne, "2 ETH worth of pufETH after" - ); - } - - // Node operator can deposit Bond with Permit and pay for the VT in ETH - function test_register_pufETH_permit_pay_VT() external { - bytes memory pubKey = _getPubKey(bytes32("alice")); vm.deal(alice, 10 ether); - // Alice mints 2 ETH of pufETH + uint256 amount = BOND + (pufferOracle.getValidatorTicketPrice() * MINIMUM_EPOCHS_VALIDATION); + vm.startPrank(alice); - pufferVault.depositETH{ value: 2 ether }(alice); - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 2 ether, "1 pufETH before for alice"); + assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before registration"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - // Generate Permit data for 2 pufETH to the protocol - Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), 2 ether, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); - - uint256 numberOfDays = 180; - // Get the smoothing commitment amount for 6 months - uint256 sc = pufferOracle.getValidatorTicketPrice() * numberOfDays; - // Register validator key by paying SC in ETH and depositing bond in pufETH vm.expectEmit(true, true, true, true); emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); - pufferProtocol.registerValidatorKey{ value: sc }(data, PUFFER_MODULE_0, permit, emptyPermit); - - // Alice has some dust in her wallet, because VT purchase changes the exchange rate, meaning pufETH is worth more - assertEq(pufferVault.balanceOf(alice), 3389455624732864, "3389455624732864 pufETH after for alice"); + pufferProtocol.registerValidatorKey{ value: amount }(data, PUFFER_MODULE_0, 0, new bytes[](0)); - uint256 protocolPufETHBalance = pufferVault.balanceOf(address(pufferProtocol)); - assertEq(protocolPufETHBalance, 1.996610544375267136 ether, "~1.99 pufETH after"); - assertApproxEqRel( - pufferVault.convertToAssets(protocolPufETHBalance), 2 ether, pointZeroZeroOne, "1 ETH worth of pufETH after" + assertApproxEqAbs( + pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), BOND, 1, "1 pufETH after" ); + assertEq(address(pufferProtocol).balance, amount - BOND, "amount locked in the protocol"); } - // Node operator can deposit both VT and pufETH with Permit - function test_register_both_permit() external { + function test_register_validator_key_new_flow_with_eth_deposit() external { bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); - - uint256 numberOfDays = 200; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; - - // Alice mints 2 ETH of pufETH - vm.startPrank(alice); - // Purchase pufETH - pufferVault.depositETH{ value: 2 ether }(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - - // Because Alice purchased a lot of VT's, it changed the conversion rate - // Because of that the registerValidatorKey will .transferFrom a smaller amount of pufETH - uint256 leftOverPufETH = pufferVault.balanceOf(alice) - pufferVault.convertToShares(2 ether); - - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 2 ether, "2 pufETH before for alice"); - assertEq(validatorTicket.balanceOf(alice), _upscaleTo18Decimals(numberOfDays), "VT before for alice"); - - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - - uint256 bond = 2 ether; - Permit memory pufETHPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), bond, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); - Permit memory vtPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(numberOfDays), block.timestamp), - validatorTicket.DOMAIN_SEPARATOR() - ); - - vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); - pufferProtocol.registerValidatorKey(data, PUFFER_MODULE_0, pufETHPermit, vtPermit); - - assertEq(pufferVault.balanceOf(alice), leftOverPufETH, "alice should have some leftover pufETH"); - assertEq(validatorTicket.balanceOf(alice), 0, "0 vt after for alice"); - assertApproxEqRel(pufferVault.balanceOf(address(pufferProtocol)), bond, 0.002e18, "2 pufETH after"); - } - // Node operator can deposit both VT and pufETH with .approve - function test_register_both_approve() external { - bytes memory pubKey = _getPubKey(bytes32("alice")); vm.deal(alice, 10 ether); - uint256 numberOfDays = 200; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; + uint256 amount = BOND + (pufferOracle.getValidatorTicketPrice() * MINIMUM_EPOCHS_VALIDATION); vm.startPrank(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - // Alice mints 2 ETH of pufETH - pufferVault.depositETH{ value: 2 ether }(alice); - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - // 1 wei diff - assertApproxEqAbs( - pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 2 ether, 1, "2 pufETH before for alice" - ); - assertEq(validatorTicket.balanceOf(alice), _upscaleTo18Decimals(numberOfDays), "VT before for alice"); + assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before registration"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 bond = 2 ether; - - pufferVault.approve(address(pufferProtocol), type(uint256).max); - validatorTicket.approve(address(pufferProtocol), type(uint256).max); - - Permit memory vtPermit = emptyPermit; - vtPermit.amount = _upscaleTo18Decimals(numberOfDays); // upscale to 18 decimals - - Permit memory pufETHPermit = emptyPermit; - pufETHPermit.amount = pufferVault.convertToShares(bond); - vm.expectEmit(true, true, true, true); emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); - pufferProtocol.registerValidatorKey(data, PUFFER_MODULE_0, pufETHPermit, vtPermit); + pufferProtocol.registerValidatorKey{ value: amount }(data, PUFFER_MODULE_0, 0, new bytes[](0)); + vm.stopPrank(); - assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH after for alice"); - assertEq(validatorTicket.balanceOf(alice), 0, "0 vt after for alice"); - // 1 wei diff assertApproxEqAbs( - pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), bond, 1, "1 pufETH after" + pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), BOND, 1, "1 pufETH after" ); - } - - // Node operator can pay for pufETH with ETH and use Permit for VT - function test_register_pufETH_pay_vt_approve() external { - bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); + assertEq(address(pufferProtocol).balance, amount - BOND, "amount locked in the protocol"); - uint256 numberOfDays = 30; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; + // Provision a newly registered validator + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); - vm.startPrank(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); + // 30 Days later, Alice wants to top-up more VT + vm.warp(block.timestamp + 10 days); - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH before for alice"); - assertEq(validatorTicket.balanceOf(alice), _upscaleTo18Decimals(numberOfDays), "VT before for alice"); + // reduced by 100000000000 wei + pufferOracle.setMintPrice(9703921568628); - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(numberOfDays), block.timestamp), - validatorTicket.DOMAIN_SEPARATOR() - ); + // alice validated for 10 days * 225 epochs = 2250 epochs with 1 validator + // uint256 vtBurnAmount = validatedEpochs * 4444444444444445 + uint256 validatedEpochs = 2250; - uint256 bond = 2 ether; + bytes[] memory vtConsumptionSignatures = _getGuardianSignaturesForRegistration(alice, validatedEpochs); - vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); - pufferProtocol.registerValidatorKey{ value: bond }(data, PUFFER_MODULE_0, emptyPermit, permit); - - assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH after for alice"); - assertApproxEqRel(pufferVault.balanceOf(address(pufferProtocol)), 2 ether, pointZeroFive, "~2 pufETH after"); + vm.startPrank(alice); + pufferProtocol.depositValidationTime{ value: 1 ether }(alice, validatedEpochs, vtConsumptionSignatures); + vm.stopPrank(); } - // Node operator can deposit Bond in pufETH - function test_register_validator_key_with_permit_reverts_invalid_vt_amount() external { + function testRevert_invalidETHPayment() external { bytes memory pubKey = _getPubKey(bytes32("alice")); vm.deal(alice, 100 ether); - // Alice mints 2 ETH of pufETH - vm.startPrank(alice); - pufferVault.depositETH{ value: 2 ether }(alice); - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - // Generate Permit data for 10 pufETH to the protocol - Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), 0.5 ether, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); // Underpay VT vm.expectRevert(); - pufferProtocol.registerValidatorKey{ value: 0.1 ether }(data, PUFFER_MODULE_0, permit, emptyPermit); + pufferProtocol.registerValidatorKey{ value: 0.1 ether }(data, PUFFER_MODULE_0, 0, new bytes[](0)); } function test_validator_limit_per_module() external { - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorLimitPerModuleChanged(500, 1); @@ -704,11 +578,10 @@ contract PufferProtocolTest is UnitTestHelper { uint256 smoothingCommitment = pufferOracle.getValidatorTicketPrice(); bytes memory pubKey = _getPubKey(bytes32("bob")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 bond = 2 ether; vm.expectRevert(IPufferProtocol.ValidatorLimitForModuleReached.selector); - pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( - validatorKeyData, PUFFER_MODULE_0, emptyPermit, emptyPermit + pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + BOND) }( + validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0) ); } @@ -720,18 +593,18 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(NoRestakingModule, 200 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); assertApproxEqAbs( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 2 ether, + 1.5 ether, 1, - "~2 pufETH in protocol" + "~1.5 pufETH in protocol" ); // bond + something for the validator registration - assertEq(address(pufferVault).balance, 1002.2835 ether, "vault eth balance"); + assertEq(address(pufferVault).balance, 1001.5 ether, "vault eth balance"); Validator memory validator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 0); @@ -752,8 +625,8 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(16 days, 100), + totalEpochsValidated: 16 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 16 * EPOCHS_PER_DAY), wasSlashed: false }); @@ -764,7 +637,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(pufferVault.balanceOf(alice), validator.bond, "alice got the pufETH"); // 1 wei diff assertApproxEqAbs( - pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 2 ether, 1, "assets owned by alice" + pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1.5 ether, 1, "assets owned by alice" ); // Alice doesn't withdraw her VT's right away @@ -910,12 +783,12 @@ contract PufferProtocolTest is UnitTestHelper { } function test_changeMinimumVTAmount() public { - assertEq(pufferProtocol.getMinimumVtAmount(), 28 ether, "initial value"); + assertEq(pufferProtocol.getMinimumVtAmount(), 30 * EPOCHS_PER_DAY, "initial value"); vm.startPrank(DAO); - pufferProtocol.changeMinimumVTAmount(50 ether); + pufferProtocol.changeMinimumVTAmount(50 * EPOCHS_PER_DAY); - assertEq(pufferProtocol.getMinimumVtAmount(), 50 ether, "value after change"); + assertEq(pufferProtocol.getMinimumVtAmount(), 50 * EPOCHS_PER_DAY, "value after change"); } // Alice tries to withdraw all VT before provisioning @@ -925,7 +798,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); // Register Validator key registers validator with 30 VTs - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.expectRevert(IPufferProtocol.ActiveOrPendingValidatorsExist.selector); pufferProtocol.withdrawValidatorTickets(30 ether, alice); @@ -935,10 +808,15 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + + uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), 30 ether, pointZeroZeroOne, "alice should have ~30 VTS" + pufferProtocol.getValidationTime(alice), + 30 * EPOCHS_PER_DAY * vtPrice, + pointZeroZeroOne, + "alice should have ~30 VTS" ); vm.stopPrank(); @@ -947,39 +825,38 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), - 20 ether, + pufferProtocol.getValidationTime(alice), + 20 * EPOCHS_PER_DAY * vtPrice, pointZeroZeroOne, "alice should have ~20 VTS -10 penalty" ); - - vm.startPrank(alice); - pufferProtocol.withdrawValidatorTickets(uint96(20 ether), alice); - - assertEq(validatorTicket.balanceOf(alice), 20 ether, "alice got her VT"); } function test_setVTPenalty() public { - assertEq(pufferProtocol.getVTPenalty(), 10 ether, "initial value"); + // 10 days of VT penalty + uint256 penaltyETHAmount = 10 * EPOCHS_PER_DAY; + assertEq(pufferProtocol.getVTPenalty(), penaltyETHAmount, "initial value"); + + uint256 newPenaltyAmount = 20 * EPOCHS_PER_DAY; vm.startPrank(DAO); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.VTPenaltyChanged(10 ether, 20 ether); - pufferProtocol.setVTPenalty(20 ether); + emit IPufferProtocol.VTPenaltyChanged(penaltyETHAmount, newPenaltyAmount); + pufferProtocol.setVTPenalty(newPenaltyAmount); - assertEq(pufferProtocol.getVTPenalty(), 20 ether, "value after change"); + assertEq(pufferProtocol.getVTPenalty(), newPenaltyAmount, "value after change"); } function test_setVTPenalty_bigger_than_minimum_VT_amount() public { vm.startPrank(DAO); vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); - pufferProtocol.setVTPenalty(50 ether); + pufferProtocol.setVTPenalty(50 * EPOCHS_PER_DAY); } function test_changeMinimumVTAmount_lower_than_penalty() public { vm.startPrank(DAO); vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); - pufferProtocol.changeMinimumVTAmount(9 ether); + pufferProtocol.changeMinimumVTAmount(9 * EPOCHS_PER_DAY); } function test_new_vtPenalty_works() public { @@ -989,42 +866,53 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); + uint256 vtPricePerEpoch = pufferOracle.getValidatorTicketPrice(); + assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), 30 ether, pointZeroZeroOne, "alice should have ~30 VTS" + pufferProtocol.getValidationTime(alice), + 30 * EPOCHS_PER_DAY * vtPricePerEpoch, + pointZeroZeroOne, + "alice should have ~30 VTS" ); pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); // Alice loses 20 VT's assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), 10 ether, pointZeroZeroOne, "alice should have ~20 VTS" + pufferProtocol.getValidationTime(alice), + 10 * EPOCHS_PER_DAY * vtPricePerEpoch, + pointZeroZeroOne, + "alice should have ~10 VTS" ); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Alice is not provisioned assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), 40 ether, pointZeroZeroOne, "alice should have ~40 VTS" + pufferProtocol.getValidationTime(alice), + 40 * EPOCHS_PER_DAY * vtPricePerEpoch, + pointZeroZeroOne, + "alice should have ~40 VTS" ); // Set penalty to 0 vm.startPrank(DAO); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.VTPenaltyChanged(20 ether, 0); + emit IPufferProtocol.VTPenaltyChanged(20 * EPOCHS_PER_DAY, 0); pufferProtocol.setVTPenalty(0); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), - 70 ether, + pufferProtocol.getValidationTime(alice), + 70 * EPOCHS_PER_DAY * vtPricePerEpoch, pointZeroZeroOne, "alice should have ~70 VTS register" ); @@ -1032,8 +920,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), - 70 ether, + pufferProtocol.getValidationTime(alice), + 70 * EPOCHS_PER_DAY * vtPricePerEpoch, pointZeroZeroOne, "alice should have ~70 VTS end" ); @@ -1042,16 +930,15 @@ contract PufferProtocolTest is UnitTestHelper { function test_double_withdrawal_reverts() public { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); - assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 30 ether, "protocol has 30 VT"); assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 2 ether, 1, "protocol should have ~2 eth bond" + _getUnderlyingETHAmount(address(pufferProtocol)), 1.5 ether, 1, "protocol should have ~2 eth bond" ); vm.startPrank(alice); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH ); _executeFullWithdrawal( StoppedValidatorInfo({ @@ -1059,22 +946,30 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), wasSlashed: false }) ); - // 28 got burned from Alice - assertApproxEqRel( - validatorTicket.balanceOf(address(pufferProtocol)), 2 ether, pointZeroZeroOne, "Protocol has 2 VT" - ); + // 2 days are leftover from 30 (30 is minimum for registration) + uint256 leftOverTime = 2 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(); - assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" - ); + uint256 unusedValidationTime = pufferProtocol.getValidationTime(alice); - assertApproxEqAbs(_getUnderlyingETHAmount(address(alice)), 2 ether, 1, "alice got back the bond"); + assertEq(unusedValidationTime, leftOverTime, "unused validation time"); + + // VTS got burned from puffer protocol + assertEq(address(pufferProtocol).balance, unusedValidationTime, "Protocol has some leftower ETH - unused VT"); + + vm.startPrank(alice); + pufferProtocol.withdrawValidationTime(uint96(unusedValidationTime), address(55)); + + assertEq(weth.balanceOf(address(55)), unusedValidationTime, "recipient got the validation time ETH"); + + assertApproxEqAbs(_getUnderlyingETHAmount(address(alice)), 1.5 ether, 1, "alice got back the bond"); + + bytes[] memory vtConsumptionSignature = _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY); // We've removed the validator data, meaning the validator status is 0 (UNINITIALIZED) vm.expectRevert(abi.encodeWithSelector(IPufferProtocol.InvalidValidatorState.selector, 0)); @@ -1084,8 +979,8 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: vtConsumptionSignature, wasSlashed: false }) ); @@ -1099,19 +994,19 @@ contract PufferProtocolTest is UnitTestHelper { uint256 aliceVTBalance = pufferProtocol.getValidatorTicketsBalance(alice); - assertApproxEqRel(aliceVTBalance, 2 ether, pointZeroZeroOne, "2 vt balance after"); + assertEq(aliceVTBalance, 0, "0 vt token balance after"); vm.startPrank(alice); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorTicketsWithdrawn(alice, alice, aliceVTBalance); pufferProtocol.withdrawValidatorTickets(uint96(aliceVTBalance), alice); - assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "0 vt balance after"); + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "0 vt token balance after"); assertEq(validatorTicket.balanceOf(alice), aliceVTBalance, "~20 vt alice before"); uint256 bobVTBalance = pufferProtocol.getValidatorTicketsBalance(bob); - assertApproxEqRel(bobVTBalance, 2 ether, pointZeroZeroOne, "2 vt balance before bob"); + assertEq(bobVTBalance, 0, "2 vt balance before bob"); vm.startPrank(bob); @@ -1119,8 +1014,7 @@ contract PufferProtocolTest is UnitTestHelper { emit IPufferProtocol.ValidatorTicketsWithdrawn(bob, alice, bobVTBalance); pufferProtocol.withdrawValidatorTickets(uint96(bobVTBalance), alice); - assertEq(pufferProtocol.getValidatorTicketsBalance(bob), 0, "0 vt balance after bob"); - assertApproxEqRel(validatorTicket.balanceOf(alice), 4 ether, pointZeroZeroOne, "4 vt alice after bobs gift"); + assertEq(pufferProtocol.getValidatorTicketsBalance(bob), 0, "0 vt token balance after bob"); } // Batch claim 32 ETH withdrawals @@ -1128,13 +1022,16 @@ contract PufferProtocolTest is UnitTestHelper { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); + // 28 days of epochs + uint256 epochsValidated = 28 * EPOCHS_PER_DAY; + StoppedValidatorInfo memory aliceInfo = StoppedValidatorInfo({ module: NoRestakingModule, moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: epochsValidated, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, epochsValidated), wasSlashed: false }); @@ -1143,8 +1040,8 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 1, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: epochsValidated, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, epochsValidated), wasSlashed: false }); @@ -1154,25 +1051,22 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, epochsValidated * BURN_RATE_PER_EPOCH ); - vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, epochsValidated * BURN_RATE_PER_EPOCH ); pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); - assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" - ); + assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); - // Alice got more because she earned the rewards from Bob's registration - assertGe(_getUnderlyingETHAmount(address(alice)), 2 ether, "alice got back the bond gt"); + assertEq(_getUnderlyingETHAmount(address(alice)), 1.5 ether, "alice got back the bond gt"); - assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 2 ether, 1, "bob got back the bond"); + assertEq(_getUnderlyingETHAmount(address(bob)), 1.5 ether, "bob got back the bond"); } // Batch claim of different amounts + // This one uses old validator tickets instead of new VT model function test_different_amounts_batch_claim() public { // Buy and approve VT validatorTicket.purchaseValidatorTicket{ value: 10 ether }(address(this)); @@ -1199,8 +1093,8 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(35 days, 100), + totalEpochsValidated: 35 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 35 * EPOCHS_PER_DAY), wasSlashed: false }); stopInfos[1] = StoppedValidatorInfo({ @@ -1208,8 +1102,8 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 1, withdrawalAmount: 31.9 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(bob, 28 * EPOCHS_PER_DAY), wasSlashed: false }); stopInfos[2] = StoppedValidatorInfo({ @@ -1217,8 +1111,8 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 2, withdrawalAmount: 31 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(34 days, 100), + totalEpochsValidated: 34 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(charlie, 34 * EPOCHS_PER_DAY), wasSlashed: true }); stopInfos[3] = StoppedValidatorInfo({ @@ -1226,8 +1120,8 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 3, withdrawalAmount: 31.8 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(48 days, 100), + totalEpochsValidated: 48 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(dianna, 48 * EPOCHS_PER_DAY), wasSlashed: false }); stopInfos[4] = StoppedValidatorInfo({ @@ -1235,60 +1129,62 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 4, withdrawalAmount: 31.5 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(2 days, 100), + totalEpochsValidated: 2 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(eve, 2 * EPOCHS_PER_DAY), wasSlashed: true }); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(35 days, 100)) + _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 35 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH ); vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidatorExited( _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.1 ether), - _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH ); vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidatorExited( _getPubKey(bytes32("charlie")), 2, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 2).bond, - _getVTBurnAmount(100, _getEpochNumber(34 days, 100)) + 34 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH ); // got slashed vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidatorExited( _getPubKey(bytes32("dianna")), 3, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.2 ether), - _getVTBurnAmount(100, _getEpochNumber(48 days, 100)) + 48 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH ); vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidatorExited( _getPubKey(bytes32("eve")), 4, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond, - 28 ether // minimum vt amount + 2.00000000000000025 ether // because of rounding we take a little more (28 days of VT) ); // got slashed pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); - assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" - ); + assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); - // Alice got more because she earned the rewards from the others - assertGe(_getUnderlyingETHAmount(address(alice)), 1 ether, "alice got back the bond gt"); + // // Alice got more because she earned the rewards from the others + assertGe(_getUnderlyingETHAmount(address(alice)), 1.5 ether, "alice got back the bond gt"); - // Bob got 0.9 ETH bond + some rewards from the others + // // Bob got 0.9 ETH bond + some rewards from the others assertGe(_getUnderlyingETHAmount(address(bob)), 0.9 ether, "bob got back the bond gt"); - // Charlie got 0 bond + // // Charlie got 0 bond assertEq(_getUnderlyingETHAmount(address(charlie)), 0, "charlie got 0 bond - slashed"); assertGe(_getUnderlyingETHAmount(address(dianna)), 0.8 ether, "dianna got back the bond gt"); @@ -1296,6 +1192,148 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(_getUnderlyingETHAmount(address(eve)), 0, "eve got 0 bond - slashed"); } + function test_oldVT_and_new_VT_model_only_vt_burned() public { + assertEq(pufferVault.convertToAssets(1 ether), 1 ether, "initial exchange rate is 1:1"); + + // Buy and approve VT, this changes the exchange rate + validatorTicket.purchaseValidatorTicket{ value: 1 ether }(address(this)); + validatorTicket.approve(address(pufferProtocol), 1000 ether); + + uint256 exchangeRateAfterVTPurchase = 1000945000000000000; + + // Exchange rate remained unchanged, 1 wei diff (rounding) + assertApproxEqAbs( + pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "initial exchange rate is ~1:1" + ); + + Permit memory vtPermit = emptyPermit; + vtPermit.amount = 100 ether; + pufferProtocol.depositValidatorTickets(vtPermit, alice); + + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 100 ether, "100 VT in the protocol"); + + // Alice is provisioned with 30 'new VT' and has 100 validator tickets deposited + // Total 130 'days' of validation + _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); + + uint256 initialValidationTimeAfterProvisioning = pufferProtocol.getValidationTime(alice); + + // Alice exits a validator after 65 days of validation + StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](1); + stopInfos[0] = StoppedValidatorInfo({ + moduleName: PUFFER_MODULE_0, + module: NoRestakingModule, + pufferModuleIndex: 0, + withdrawalAmount: 32 ether, + totalEpochsValidated: 65 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 65 * EPOCHS_PER_DAY), + wasSlashed: false + }); + + // Exchange rate remained unchanged, 1 wei diff (rounding) + assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); + + pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); + + // Validation time is unchanged + assertEq( + pufferProtocol.getValidationTime(alice), + initialValidationTimeAfterProvisioning, + "Alice has the same amount of validation time" + ); + + // Alice has 100-65 days of VT left + assertEq( + pufferProtocol.getValidatorTicketsBalance(alice), + 34.999999999999991875 ether, + "Alice has ~35 VT in the protocol" + ); + + // txs don't revert + vm.startPrank(alice); + pufferProtocol.withdrawValidationTime(uint96(initialValidationTimeAfterProvisioning), alice); + pufferProtocol.withdrawValidatorTickets(34.999999999999991875 ether, alice); + + // Alice has 0 VT in the protocol + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "Alice has 0 VT in the protocol"); + + // Alice has 0 validation time + assertEq(pufferProtocol.getValidationTime(alice), 0, "Alice has 0 validation time"); + } + + function test_oldVT_and_new_VT_model_only_both_burned() public { + assertEq(pufferVault.convertToAssets(1 ether), 1 ether, "initial exchange rate is 1:1"); + + // Buy and approve VT, this changes the exchange rate + validatorTicket.purchaseValidatorTicket{ value: 1 ether }(address(this)); + validatorTicket.approve(address(pufferProtocol), 1000 ether); + + uint256 exchangeRateAfterVTPurchase = 1000945000000000000; + + // Exchange rate remained unchanged, 1 wei diff (rounding) + assertApproxEqAbs( + pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "initial exchange rate is ~1:1" + ); + + Permit memory vtPermit = emptyPermit; + vtPermit.amount = 100 ether; + pufferProtocol.depositValidatorTickets(vtPermit, alice); + + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 100 ether, "100 VT in the protocol"); + + // Alice is provisioned with 30 'new VT' and has 100 validator tickets deposited + // Total 130 'days' of validation + _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); + + // Alice exits a validator after 120 days of validation + StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](1); + stopInfos[0] = StoppedValidatorInfo({ + moduleName: PUFFER_MODULE_0, + module: NoRestakingModule, + pufferModuleIndex: 0, + withdrawalAmount: 32 ether, + totalEpochsValidated: 120 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 120 * EPOCHS_PER_DAY), + wasSlashed: false + }); + + // Exchange rate remained unchanged, 1 wei diff (rounding) + assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); + + pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); + + // Nothing is changed, we didn't deposit revenue + assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); + + revenueDepositor.depositRevenue(); + + assertGt( + pufferVault.convertToAssets(1 ether), + exchangeRateAfterVTPurchase, + "exchange rate is now bigger because of revenue deposit" + ); + + // Alice has 0 VT in the protocol + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "Alice has 0 VT in the protocol"); + + // It is expected to have 10 days of validation time + uint256 expectedLeftOverValidationTime = 10 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(); + + // Alice has >0 validation time + assertEq( + pufferProtocol.getValidationTime(alice), expectedLeftOverValidationTime, "Alice has >0 validation time" + ); + + vm.startPrank(alice); + pufferProtocol.withdrawValidationTime(uint96(expectedLeftOverValidationTime), address(8888)); + + assertEq( + weth.balanceOf(address(8888)), + expectedLeftOverValidationTime, + "Recipient got WETH (validation time from Alice)" + ); + } + function test_single_withdrawal() public { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); @@ -1305,8 +1343,8 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), wasSlashed: false }); @@ -1315,19 +1353,19 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 1, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), wasSlashed: false }); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH ); // 10 days of VT _executeFullWithdrawal(aliceInfo); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH ); // 10 days of VT _executeFullWithdrawal(bobInfo); @@ -1336,9 +1374,9 @@ contract PufferProtocolTest is UnitTestHelper { ); // Alice got more because she earned the rewards from Bob's registration - assertGe(_getUnderlyingETHAmount(address(alice)), 2 ether, "alice got back the bond gt"); + assertGe(_getUnderlyingETHAmount(address(alice)), 1.5 ether, "alice got back the bond gt"); - assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 2 ether, 1, "bob got back the bond"); + assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 1.5 ether, 1, "bob got back the bond"); } function test_batch_vs_multiple_single_withdrawals() public { @@ -1378,13 +1416,14 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); + // This is because VT settlement now happens later, so the exchange rate is 1:1 + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning, 1:1"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1401,8 +1440,8 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, withdrawalAmount: 29 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), wasSlashed: true }); @@ -1417,9 +1456,9 @@ contract PufferProtocolTest is UnitTestHelper { // Bad dept is shared between all pufETH holders assertApproxEqRel( pufferVault.balanceOf(address(pufferProtocol)), - 2 ether, + 1.5 ether, pointZeroOne, - "2 ETH worth of pufETH in the protocol" + "1.5 ETH worth of pufETH in the protocol" ); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); } @@ -1430,13 +1469,13 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning, 1:1"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1451,8 +1490,8 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), withdrawalAmount: 29.5 ether, wasSlashed: true }); @@ -1460,16 +1499,16 @@ contract PufferProtocolTest is UnitTestHelper { // Burns one whole bond _executeFullWithdrawal(validatorInfo); - // 1 ETH gives you more pufETH after the `retrieveBond` call, meaning it is worse than before + // 1 ETH gives you more pufETH after the `retrieveBond` call, meaning it is better for pufETH holders assertLt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); // The other validator has less than 1 ETH in the bond // Bad dept is shared between all pufETH holders assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 2 ether, + 1.5 ether, pointZeroOne, - "2 ether ETH worth of pufETH in the protocol" + "1.5 ether ETH worth of pufETH in the protocol" ); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); } @@ -1480,18 +1519,27 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators - uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); + uint256 exchangeRateBefore = pufferVault.convertToAssets(1 ether); + // 2 bonds * 1.5 ETH + 1000 initial value in the vault + assertEq(address(pufferVault).balance, 1003 ether, "1003 ETH in the vault"); + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning, 1:1"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + // We provision one validator + assertEq(address(pufferVault).balance, 971 ether, "971 ETH in the vault"); + + // Stays the same + assertEq(pufferVault.convertToAssets(1 ether), 1 ether, "shares after provisioning"); + assertEq(weth.balanceOf(address(pufferVault)), 0 ether, "0 WETH in the vault"); + vm.deal(NoRestakingModule, 200 ether); // Now the node operators submit proofs to get back their bond @@ -1501,34 +1549,37 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - withdrawalAmount: 30 ether, + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), + withdrawalAmount: 30.9 ether, // 1.1 ETH slashed wasSlashed: true }); // Burns one whole bond _executeFullWithdrawal(validatorInfo); - // Exchange rate remains the same, it is slightly better - assertApproxEqRel( - exchangeRateBefore, pufferVault.convertToShares(1 ether), pointZeroZeroOne, "shares after retrieve" + // 30 ETH was returned to the vault + assertEq(address(pufferVault).balance, 1001.9 ether, "1001.9 ETH in the vault"); + + revenueDepositor.depositRevenue(); + + assertGt(weth.balanceOf(address(pufferVault)), 0 ether, "WETH in the vault"); + + // Exchange rate changes in favour of remaining pufETH holders + assertLt( + exchangeRateBefore, + pufferVault.convertToAssets(1 ether), + "exchange rate after validator exits is better for pufETH holders" ); - // 1 ETH gives you less pufETH after the `retrieveBond` call, meaning it is better than before (slightly) - assertGt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); - // Alice has a little over 2 ETH because she earned something for paying the VT on the second validator registration + // Alice has a little over 1.5 ETH because she earned something from herself (her own exit + slashing) assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 2 ether, - pointZeroZeroOne, - "2 ETH worth of pufETH in the protocol" - ); - assertGt( - pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 2 ether, - "2 ETH worth of pufETH in the protocol gt" + 1.5 ether, + pointZeroOne, + "1.5 ETH worth of pufETH in the protocol" ); + // Alice didn't receive any bond for that one validator exit assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); } @@ -1538,13 +1589,13 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1559,8 +1610,8 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), withdrawalAmount: 31.9 ether, wasSlashed: false }); @@ -1568,15 +1619,17 @@ contract PufferProtocolTest is UnitTestHelper { // Burns one whole bond _executeFullWithdrawal(validatorInfo); - // Exchange rate stays the same - assertEq(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); + revenueDepositor.depositRevenue(); + + // Exchange rate is better for pufETH holders + assertGt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); - // Alice has ~ 2 ETH locked in the protocol + // Alice has ~ 1.5 ETH locked in the protocol assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 2 ether, - pointZeroZeroOne, - "2 ETH worth of pufETH in the protocol" + 1.5 ether, + pointZeroOne, + "1.5 ETH worth of pufETH in the protocol" ); // Alice got a little over 0.9 ETH worth of pufETH because she earned something for paying the VT on the second validator registration assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 0.9 ether, ">0.9 ETH worth of pufETH alice"); @@ -1588,13 +1641,13 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433886374375918, "shares before provisioning"); + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning"); uint256 startTimestamp = 1707411226; vm.warp(startTimestamp); @@ -1609,8 +1662,8 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, - startEpoch: 100, - endEpoch: _getEpochNumber(15 days, 100), + totalEpochsValidated: 15 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 15 * EPOCHS_PER_DAY), withdrawalAmount: 32.1 ether, wasSlashed: false }); @@ -1618,25 +1671,27 @@ contract PufferProtocolTest is UnitTestHelper { // Burns one whole bond _executeFullWithdrawal(validatorInfo); - // Exchange rate stays the same - assertEq(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); + revenueDepositor.depositRevenue(); + + // Exchange rate is better for pufETH holders + assertGt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); // Alice has ~ 1 ETH locked in the protocol assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 2 ether, + 1.5 ether, pointZeroZeroOne, - "2 ETH worth of pufETH in the protocol" + "1.5 ETH worth of pufETH in the protocol" ); - // Alice got a little over 2 ETH worth of pufETH because she earned something for paying the VT on the second validator registration - assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 2 ether, ">2 ETH worth of pufETH alice"); + // Alice got a little over 1.5 ETH worth of pufETH because she earned something for paying the VT on the second validator registration + assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1.5 ether, ">1.5 ETH worth of pufETH alice"); } function test_validator_early_exit_dos() public { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); @@ -1648,13 +1703,17 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(1 days, 100), + totalEpochsValidated: 1 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 1 * EPOCHS_PER_DAY), wasSlashed: false }) ); - assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 2 ether, "alice got 2 VT left in the protocol"); + uint256 leftOverValidationTime = 20 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(); + + assertEq( + pufferProtocol.getValidationTime(alice), leftOverValidationTime, "alice got 20 days left in the protocol" + ); } // Alice registers one 30 VT validator @@ -1664,7 +1723,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); @@ -1680,8 +1739,8 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(3 days, 100), + totalEpochsValidated: 3 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 3 * EPOCHS_PER_DAY), wasSlashed: false }) ); @@ -1689,59 +1748,23 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0 ether, "alice got 0 VT left in the protocol"); } - // User purchases a lot of VT using ETH, but uses Permit to transfer pufETH - function test_purchase_big_amount_of_vt() public { + // User deposits a lot of ETH (validator tickets) + function test_big_eth_deposit() public { bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 11 ether); - - // Alice mints 2 ETH of pufETH - vm.startPrank(alice); - pufferVault.depositETH{ value: 2 ether }(alice); assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 2 ether, "1 pufETH before for alice"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - // Generate Permit data for 2 pufETH to the protocol - Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), 2 ether, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); - // Register validator key by paying SC in ETH and depositing bond in pufETH vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); - pufferProtocol.registerValidatorKey{ value: 9 ether }(data, PUFFER_MODULE_0, permit, emptyPermit); + emit IPufferProtocol.ValidationTimeDeposited({ node: address(this), ethAmount: 7.5 ether }); + emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0); + pufferProtocol.registerValidatorKey{ value: 9 ether }(data, PUFFER_MODULE_0, 0, new bytes[](0)); - // Because alice purchased VT in the registration TX, it modified the exchange rate and we take less pufETH from her. - assertEq( - pufferVault.balanceOf(alice), 16833167574628528, "alice has 16833167574628528 pufETH after registering" - ); - } - - // Alice uses Permit for VT and pays for the bond with ETH, but sends more ETH than needed - function test_revert_for_excess_eth() public { - bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); - - uint256 numberOfDays = 30; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; - - // Alice mints 1 ETH of pufETH - vm.startPrank(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - - Permit memory vtPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(numberOfDays), block.timestamp), - validatorTicket.DOMAIN_SEPARATOR() - ); - - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - - // User pays for the bond in pufETH, but decides to send more than the bond 1.1 eth - vm.expectRevert(IPufferProtocol.InvalidETHAmount.selector); - pufferProtocol.registerValidatorKey{ value: 1.1 ether }(data, PUFFER_MODULE_0, emptyPermit, vtPermit); + // Protocol holds 7.5 ETHER + assertEq(address(pufferProtocol).balance, 7.5 ether, "7.5 ETH in the protocol"); + assertEq(pufferVault.balanceOf(address(pufferProtocol)), 1.5 ether, "Bond in pufETH is held by the protocol"); } // Alice deposits VT to Bob and Bob has no validators in Puffer @@ -1786,6 +1809,38 @@ contract PufferProtocolTest is UnitTestHelper { return guardianSignatures; } + /** + * @notice Get the guardian signatures from the backend API for the total validated epochs by the node operator + * @param node The address of the node operator + * @param validatedEpochsTotal The total number of validated epochs (sum for all the validators and their consumption) + * @return guardianSignatures The guardian signatures + */ + function _getGuardianSignaturesForRegistration(address node, uint256 validatedEpochsTotal) + internal + view + returns (bytes[] memory) + { + uint256 nonce = pufferProtocol.nonces(node); + + bytes32 digest = keccak256(abi.encode(node, validatedEpochsTotal, nonce)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SK, digest); + bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. + + (v, r, s) = vm.sign(guardian2SK, digest); + bytes memory signature2 = abi.encodePacked(r, s, v); // note the order here is different from line above. + + (v, r, s) = vm.sign(guardian3SK, digest); + bytes memory signature3 = abi.encodePacked(r, s, v); // note the order here is different from line above. + + bytes[] memory guardianSignatures = new bytes[](3); + guardianSignatures[0] = signature1; + guardianSignatures[1] = signature2; + guardianSignatures[2] = signature3; + + return guardianSignatures; + } + function _getHandleBatchWithdrawalMessage(StoppedValidatorInfo[] memory validatorInfos) internal view @@ -1876,21 +1931,27 @@ contract PufferProtocolTest is UnitTestHelper { /** * @dev Registers validator key and pays for everything in ETH + * @dev epochValidated = sum for all all the validators and their consumption */ - function _registerValidatorKey(bytes32 pubKeyPart, bytes32 moduleName) internal { - uint256 numberOfDays = 30; - uint256 vtPrice = pufferOracle.getValidatorTicketPrice() * numberOfDays; + function _registerValidatorKey( + address nodeOperator, + bytes32 pubKeyPart, + bytes32 moduleName, + uint256 epochsValidated + ) internal { + uint256 amount = BOND + (pufferOracle.getValidatorTicketPrice() * MINIMUM_EPOCHS_VALIDATION); + bytes memory pubKey = _getPubKey(pubKeyPart); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, moduleName); uint256 idx = pufferProtocol.getPendingValidatorIndex(moduleName); - uint256 bond = 2 ether; + bytes[] memory vtConsumptionSignatures = _getGuardianSignaturesForRegistration(nodeOperator, epochsValidated); // Empty permit means that the node operator is paying with ETH for both bond & VT in the registration transaction vm.expectEmit(true, true, true, true); emit ValidatorKeyRegistered(pubKey, idx, moduleName); - pufferProtocol.registerValidatorKey{ value: (vtPrice + bond) }( - validatorKeyData, moduleName, emptyPermit, emptyPermit + pufferProtocol.registerValidatorKey{ value: amount }( + validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures ); } @@ -1901,7 +1962,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(nodeOperator, 10 ether); vm.startPrank(nodeOperator); - _registerValidatorKey(pubKeyPart, moduleName); + _registerValidatorKey(nodeOperator, pubKeyPart, moduleName, 0); vm.stopPrank(); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); @@ -1919,22 +1980,100 @@ contract PufferProtocolTest is UnitTestHelper { return amount * 1 ether; } - function _getEpochNumber(uint256 validationTimeInSeconds, uint256 startEpoch) - internal - pure - returns (uint256 endEpoch) - { - uint256 secondsInEpoch = 32 * 12; - uint256 numberOfEpochs = validationTimeInSeconds / secondsInEpoch; - return startEpoch + numberOfEpochs; - } - function _getVTBurnAmount(uint256 startEpoch, uint256 endEpoch) internal pure returns (uint256) { uint256 validatedEpochs = endEpoch - startEpoch; // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up return validatedEpochs * 4444444444444445; } + + function test_getNodeInfo() public { + // Test non-existent node + NodeInfo memory nodeInfo = pufferProtocol.getNodeInfo(address(0x123)); + assertEq(nodeInfo.activeValidatorCount, 0); + assertEq(nodeInfo.pendingValidatorCount, 0); + assertEq(nodeInfo.deprecated_vtBalance, 0); + assertEq(nodeInfo.validationTime, 0); + assertEq(nodeInfo.epochPrice, 0); + assertEq(nodeInfo.totalEpochsValidated, 0); + + // Test registered node (alice) + nodeInfo = pufferProtocol.getNodeInfo(alice); + assertEq(nodeInfo.activeValidatorCount, 0); + assertEq(nodeInfo.pendingValidatorCount, 0); + assertEq(nodeInfo.deprecated_vtBalance, 0); + assertEq(nodeInfo.validationTime, 0); + assertEq(nodeInfo.epochPrice, 0); + assertEq(nodeInfo.totalEpochsValidated, 0); + } + + function test_setVTPenalty_invalid_amount() public { + vm.startPrank(DAO); + vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); + pufferProtocol.setVTPenalty(type(uint256).max); + } + + function test_checkValidatorRegistrationInputs_invalid_pubkey() public { + bytes memory invalidPubKey = new bytes(47); // Invalid length + ValidatorKeyData memory data = _getMockValidatorKeyData(invalidPubKey, PUFFER_MODULE_0); + + vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); + pufferProtocol.registerValidatorKey{ value: 3 ether }(data, PUFFER_MODULE_0, 0, new bytes[](0)); + } + + function test_changeMinimumVTAmount_invalid_amount() public { + vm.startPrank(DAO); + vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); + pufferProtocol.changeMinimumVTAmount(0); + } + + function test_panic_batch_withdrawals() public { + // Test with zero epochs + StoppedValidatorInfo memory info = StoppedValidatorInfo({ + module: NoRestakingModule, + moduleName: PUFFER_MODULE_0, + pufferModuleIndex: 0, + withdrawalAmount: 32 ether, + totalEpochsValidated: type(uint256).max, + vtConsumptionSignature: _getGuardianSignaturesForRegistration(bob, type(uint256).max), + wasSlashed: false + }); + + StoppedValidatorInfo[] memory validatorInfos = new StoppedValidatorInfo[](1); + validatorInfos[0] = info; + + _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); + + // Panic Error is expected panic: arithmetic underflow or overflow (0x11) + vm.expectRevert(bytes("panic: arithmetic underflow or overflow (0x11)")); + pufferProtocol.batchHandleWithdrawals(validatorInfos, _getHandleBatchWithdrawalMessage(validatorInfos)); + } + + function test_useVTOrValidationTime_edge_cases() public { + // Test with zero VT and validation time + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + + // Test with maximum VT and validation time + vm.deal(alice, 1000 ether); + vm.startPrank(alice); + validatorTicket.purchaseValidatorTicket{ value: 1000 ether }(alice); + validatorTicket.approve(address(pufferProtocol), type(uint256).max); + pufferProtocol.depositValidatorTickets(emptyPermit, alice); + vm.stopPrank(); + } + + function test_settleVTAccounting_edge_cases() public { + // Test with zero VT balance + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + + // Test with maximum VT balance + vm.deal(alice, 1000 ether); + vm.startPrank(alice); + validatorTicket.purchaseValidatorTicket{ value: 1000 ether }(alice); + validatorTicket.approve(address(pufferProtocol), type(uint256).max); + pufferProtocol.depositValidatorTickets(emptyPermit, alice); + vm.stopPrank(); + } } struct MerkleProofData { diff --git a/mainnet-contracts/test/unit/Timelock.t.sol b/mainnet-contracts/test/unit/Timelock.t.sol index ce6fc788..4df51456 100644 --- a/mainnet-contracts/test/unit/Timelock.t.sol +++ b/mainnet-contracts/test/unit/Timelock.t.sol @@ -237,7 +237,8 @@ contract TimelockTest is Test { } function test_pause_depositor_slectors(address caller) public { - vm.startPrank(timelock.pauserMultisig()); + vm.assume(caller != timelock.pauserMultisig()); + vm.assume(caller != 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); // foundry default caller vm.assume(caller != address(timelock)); vm.assume(caller != address(accessManager)); @@ -249,6 +250,7 @@ contract TimelockTest is Test { selectors[0][0] = PufferDepositor.swapAndDeposit.selector; + vm.startPrank(timelock.pauserMultisig()); timelock.pauseSelectors(targets, selectors); (bool canCall, uint32 delay) = diff --git a/mainnet-contracts/test/unit/ValidatorTicket.t.sol b/mainnet-contracts/test/unit/ValidatorTicket.t.sol index 2432aa4e..6de16dac 100644 --- a/mainnet-contracts/test/unit/ValidatorTicket.t.sol +++ b/mainnet-contracts/test/unit/ValidatorTicket.t.sol @@ -80,39 +80,6 @@ contract ValidatorTicketTest is UnitTestHelper { assertEq(validatorTicket.getGuardiansFeeRate(), 1000, "new guardians fee rate"); } - function test_funds_splitting() public { - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - - uint256 amount = vtPrice * 2000; // 20000 VTs is 20 ETH - vm.deal(address(this), amount); - - address treasury = validatorTicket.TREASURY(); - - assertEq(validatorTicket.balanceOf(address(this)), 0, "should start with 0"); - assertEq(treasury.balance, 0, "treasury balance should start with 0"); - assertEq(address(guardianModule).balance, 0, "guardian balance should start with 0"); - - validatorTicket.purchaseValidatorTicket{ value: amount }(address(this)); - - // 0.5% from 20 ETH is 0.1 ETH - assertEq(address(guardianModule).balance, 0.1 ether, "guardians balance"); - // 5% from 20 ETH is 1 ETH - assertEq(treasury.balance, 1 ether, "treasury should get 1 ETH for 100 VTs"); - } - - function test_non_whole_number_purchase() public { - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - - uint256 amount = 5.123 ether; - uint256 expectedTotal = (amount * 1 ether / vtPrice); - - vm.deal(address(this), amount); - uint256 mintedAmount = validatorTicket.purchaseValidatorTicket{ value: amount }(address(this)); - - assertEq(validatorTicket.balanceOf(address(this)), expectedTotal, "VT balance"); - assertEq(mintedAmount, expectedTotal, "minted amount"); - } - function test_zero_protocol_fee_rate() public { vm.startPrank(DAO); vm.expectEmit(true, true, true, true); @@ -121,29 +88,6 @@ contract ValidatorTicketTest is UnitTestHelper { vm.stopPrank(); // because this test is reused in other test } - function test_split_funds_no_protocol_fee_rate() public { - test_zero_protocol_fee_rate(); - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 amount = vtPrice * 2000; // 20000 VTs is 20 ETH - vm.deal(address(this), amount); - - vm.expectEmit(true, true, true, true); - emit IValidatorTicket.DispersedETH(0, 0.1 ether, 19.9 ether); - validatorTicket.purchaseValidatorTicket{ value: amount }(address(this)); - - // 0.5% from 20 ETH is 0.1 ETH - assertEq(address(guardianModule).balance, 0.1 ether, "guardians balance"); - assertEq(address(validatorTicket).balance, 0, "treasury should get 0 ETH"); - } - - function test_zero_vt_purchase() public { - // No operation tx, nothing happens but doesn't revert - vm.expectEmit(true, true, true, true); - emit IValidatorTicket.DispersedETH(0, 0, 0); - validatorTicket.purchaseValidatorTicket{ value: 0 }(address(this)); - } - /// forge-config: default.allow_internal_expect_revert = true function test_overflow_protocol_fee_rate() public { vm.startPrank(DAO); @@ -163,139 +107,7 @@ contract ValidatorTicketTest is UnitTestHelper { assertEq(validatorTicket.getProtocolFeeRate(), newFeeRate, "updated"); } - function test_purchaseValidatorTicketWithPufETH() public { - uint256 vtAmount = 10 ether; - address recipient = actors[0]; - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 requiredETH = vtAmount.mulDiv(vtPrice, 1 ether, Math.Rounding.Ceil); - - uint256 expectedPufEthUsed = pufferVault.convertToSharesUp(requiredETH); - - _givePufETH(expectedPufEthUsed, recipient); - - vm.startPrank(recipient); - pufferVault.approve(address(validatorTicket), expectedPufEthUsed); - - uint256 pufEthUsed = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, vtAmount); - vm.stopPrank(); - - assertEq(pufEthUsed, expectedPufEthUsed, "PufETH used should match expected"); - assertEq(validatorTicket.balanceOf(recipient), vtAmount, "VT balance should match requested amount"); - } - - function test_purchaseValidatorTicketWithPufETH_exchangeRateChange() public { - uint256 vtAmount = 10 ether; - address recipient = actors[2]; - - uint256 exchangeRate = pufferVault.convertToAssets(1 ether); - assertEq(exchangeRate, 1 ether, "1:1 exchange rate"); - - // Simulate + 10% increase in ETH - deal(address(pufferVault), 1110 ether); - exchangeRate = pufferVault.convertToAssets(1 ether); - assertGt(exchangeRate, 1 ether, "Now exchange rate should be greater than 1"); - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 requiredETH = vtAmount.mulDiv(vtPrice, 1 ether, Math.Rounding.Ceil); - - uint256 pufEthAmount = pufferVault.convertToSharesUp(requiredETH); - - _givePufETH(pufEthAmount, recipient); - - vm.startPrank(recipient); - pufferVault.approve(address(validatorTicket), pufEthAmount); - uint256 pufEthUsed = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, vtAmount); - vm.stopPrank(); - - assertEq(pufEthUsed, pufEthAmount, "PufETH used should match expected"); - assertEq(validatorTicket.balanceOf(recipient), vtAmount, "VT balance should match requested amount"); - } - - function test_purchaseValidatorTicketWithPufETHAndPermit() public { - uint256 vtAmount = 10 ether; - address recipient = actors[2]; - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 requiredETH = vtAmount * vtPrice / 1 ether; - - uint256 pufETHToETHExchangeRate = pufferVault.convertToAssets(1 ether); - uint256 expectedPufEthUsed = (requiredETH * 1 ether) / pufETHToETHExchangeRate; - - _givePufETH(expectedPufEthUsed, recipient); - - // Create a permit - Permit memory permit = _signPermit( - _testTemps("charlie", address(validatorTicket), expectedPufEthUsed, block.timestamp), - pufferVault.DOMAIN_SEPARATOR() - ); - - vm.prank(recipient); - uint256 pufEthUsed = validatorTicket.purchaseValidatorTicketWithPufETHAndPermit(recipient, vtAmount, permit); - - assertEq(pufEthUsed, expectedPufEthUsed, "PufETH used should match expected"); - assertEq(validatorTicket.balanceOf(recipient), vtAmount, "VT balance should match requested amount"); - } - function _givePufETH(uint256 pufEthAmount, address recipient) internal { deal(address(pufferVault), recipient, pufEthAmount); } - - function test_funds_splitting_with_pufETH() public { - uint256 vtAmount = 2000 ether; // Want to mint 2000 VTs - address recipient = actors[0]; - address treasury = validatorTicket.TREASURY(); - address operationsMultisig = validatorTicket.OPERATIONS_MULTISIG(); - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 requiredETH = vtAmount.mulDiv(vtPrice, 1 ether, Math.Rounding.Ceil); - - uint256 pufEthAmount = pufferVault.convertToSharesUp(requiredETH); - - _givePufETH(pufEthAmount, recipient); - - uint256 initialTreasuryBalance = pufferVault.balanceOf(treasury); - uint256 initialOpsMultisigBalance = pufferVault.balanceOf(operationsMultisig); - uint256 initialBurnedAmount = pufferVault.totalSupply(); - - vm.startPrank(recipient); - pufferVault.approve(address(validatorTicket), pufEthAmount); - uint256 pufEthUsed = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, vtAmount); - vm.stopPrank(); - - assertEq(pufEthUsed, pufEthAmount, "PufETH used should match expected"); - assertEq(validatorTicket.balanceOf(recipient), vtAmount, "Should mint requested VTs"); - - uint256 expectedTreasuryAmount = pufEthAmount.mulDiv(500, 10000, Math.Rounding.Ceil); // 5% to treasury - uint256 expectedGuardianAmount = pufEthAmount.mulDiv(50, 10000, Math.Rounding.Ceil); // 0.5% to guardians - uint256 expectedBurnAmount = pufEthAmount - expectedTreasuryAmount - expectedGuardianAmount; - - assertEq( - pufferVault.balanceOf(treasury) - initialTreasuryBalance, - expectedTreasuryAmount, - "Treasury should receive 5% of pufETH" - ); - assertEq( - pufferVault.balanceOf(operationsMultisig) - initialOpsMultisigBalance, - expectedGuardianAmount, - "Operations Multisig should receive 0.5% of pufETH" - ); - assertEq( - initialBurnedAmount - pufferVault.totalSupply(), expectedBurnAmount, "Remaining pufETH should be burned" - ); - } - - function test_revert_zero_recipient() public { - uint256 vtAmount = 10 ether; - - vm.expectRevert(IValidatorTicket.RecipientIsZeroAddress.selector); - validatorTicket.purchaseValidatorTicketWithPufETH(address(0), vtAmount); - - Permit memory permit = _signPermit( - _testTemps("charlie", address(validatorTicket), vtAmount, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); - - vm.expectRevert(IValidatorTicket.RecipientIsZeroAddress.selector); - validatorTicket.purchaseValidatorTicketWithPufETHAndPermit(address(0), vtAmount, permit); - } } From 3d7aaaeacdfaea9f21c9653912f3f15f56c2b831 Mon Sep 17 00:00:00 2001 From: Eladio Date: Thu, 29 May 2025 18:12:36 +0200 Subject: [PATCH 16/82] Added relation pubkey => module+index in storage --- mainnet-contracts/src/PufferProtocol.sol | 79 ++++++++++++++++--- .../src/interface/IPufferProtocol.sol | 7 +- .../src/struct/ProtocolStorage.sol | 7 ++ .../src/struct/ValidatorPosition.sol | 11 +++ 4 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 mainnet-contracts/src/struct/ValidatorPosition.sol diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 0accc2bc..b693ee21 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -12,6 +12,7 @@ import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; import { ValidatorKeyData } from "./struct/ValidatorKeyData.sol"; import { Validator } from "./struct/Validator.sol"; +import { ValidatorPosition } from "./struct/ValidatorPosition.sol"; import { Permit } from "./structs/Permit.sol"; import { Status } from "./struct/Status.sol"; import { ProtocolStorage, NodeInfo, ModuleLimit } from "./struct/ProtocolStorage.sol"; @@ -24,7 +25,6 @@ import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; - /** * @title PufferProtocol * @author Puffer Finance @@ -290,7 +290,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @inheritdoc IPufferProtocol * @dev Restricted to Node Operators */ - function requestConsolidation(bytes32 moduleName, bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) + function requestConsolidation(bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) external payable virtual @@ -310,7 +310,10 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad bool alreadyChecked; bytes32 pubkeyHashSrc; bytes32 pubkeyHashTarget; - uint256 pendingValidatorIndex = $.pendingValidatorIndices[moduleName]; + pubkeyHashSrc = keccak256(srcPubkeys[0]); + ValidatorPosition memory validatorPosition = $.validatorPositions[pubkeyHashSrc]; + address moduleAddress = validatorPosition.moduleAddress; + bytes32 moduleName = PufferModule(payable(moduleAddress)).NAME(); for (uint256 i = 0; i < srcPubkeys.length; i++) { pubkeyHashSrc = keccak256(srcPubkeys[i]); pubkeyHashTarget = keccak256(targetPubkeys[i]); @@ -324,7 +327,13 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad } // Preemptively storing it to true to save slot calculation if (!alreadyChecked) { - _checkValidator(pendingValidatorIndex, $, pubkeyHashSrc, moduleName); + validatorPosition = $.validatorPositions[pubkeyHashSrc]; + require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); + Validator memory validator = $.validators[moduleName][validatorPosition.index]; + require ( + validator.node == msg.sender && validator.status == Status.ACTIVE, + InvalidValidator() + ); } assembly { let slot := keccak256(add(pubkeyHashTarget, 0x20), 0x20) @@ -333,11 +342,17 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad } // Preemptively storing it to true to save slot calculation if (!alreadyChecked) { - _checkValidator(pendingValidatorIndex, $, pubkeyHashTarget, moduleName); + validatorPosition = $.validatorPositions[pubkeyHashTarget]; + require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); + Validator memory validator = $.validators[moduleName][validatorPosition.index]; + require ( + validator.node == msg.sender && validator.status == Status.ACTIVE, + InvalidValidator() + ); } } - $.modules[moduleName].requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); + PufferModule(payable(moduleAddress)).requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); emit ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); } @@ -346,22 +361,35 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @inheritdoc IPufferProtocol * @dev Restricted to Node Operators */ - function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) + function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external payable restricted returnExcessFee { + if (pubkeys.length == 0) { + revert InputArrayLengthZero(); + } + if (pubkeys.length != gweiAmounts.length) { + revert InputArrayLengthMismatch(); + } ProtocolStorage storage $ = _getPufferProtocolStorage(); - // validate pubkeys belong to that node - - uint256 pendingValidatorIndex = $.pendingValidatorIndices[moduleName]; + // validate pubkeys belong to that node and are active - bytes32 pubkeyHash; + bytes32 pubkeyHash = keccak256(pubkeys[0]); + ValidatorPosition memory validatorPosition = $.validatorPositions[pubkeyHash]; + address moduleAddress = validatorPosition.moduleAddress; + bytes32 moduleName = PufferModule(payable(moduleAddress)).NAME(); for (uint256 i = 0; i < pubkeys.length; i++) { pubkeyHash = keccak256(pubkeys[i]); - _checkValidator(pendingValidatorIndex, $, pubkeyHash, moduleName); + validatorPosition = $.validatorPositions[pubkeyHash]; + require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); + Validator memory validator = $.validators[moduleName][validatorPosition.index]; + require ( + validator.node == msg.sender && validator.status == Status.ACTIVE, + InvalidValidator() + ); } PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); @@ -432,6 +460,8 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad delete validator.module; delete validator.status; delete validator.pubKey; + + delete $.validatorPositions[keccak256(validator.pubKey)]; } VALIDATOR_TICKET.burn(burnAmounts.vt); @@ -537,6 +567,22 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad _setVTPenalty(newPenaltyAmount); } + /** + * @notice Admin function to set the positions of the validators + * @param pubkeys The pubkeys of the validators + * @param moduleAddresses The addresses of the modules + * @param indices The indices of the validators in the modules + * @dev Restricted to the DAO + */ + function setValidatorsPositions(bytes[] calldata pubkeys, address[] calldata moduleAddresses, uint96[] calldata indices) external restricted { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + for (uint256 i = 0; i < pubkeys.length; i++) { + $.validatorPositions[keccak256(pubkeys[i])] = ValidatorPosition({ + moduleAddress: moduleAddresses[i], + index: indices[i] + }); + } + } /** * @inheritdoc IPufferProtocol */ @@ -711,15 +757,22 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad ) internal { uint256 pufferModuleIndex = $.pendingValidatorIndices[moduleName]; + address moduleAddress = address($.modules[moduleName]); + // No need for SafeCast $.validators[moduleName][pufferModuleIndex] = Validator({ pubKey: data.blsPubKey, status: Status.PENDING, - module: address($.modules[moduleName]), + module: moduleAddress, bond: uint96(pufETHAmount), node: msg.sender }); + $.validatorPositions[keccak256(data.blsPubKey)] = ValidatorPosition({ + moduleAddress: moduleAddress, + index: uint96(pufferModuleIndex) + }); + $.nodeOperatorInfo[msg.sender].vtBalance += SafeCast.toUint96(vtAmount); // Increment indices for this module and number of validators registered diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 8d3cae42..15459f10 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -224,14 +224,13 @@ interface IPufferProtocol { /** * @notice Requests a consolidation for the given validators. This consolidation consists on merging one validator into another one - * @param moduleName The module name of the validators to consolidate * @param srcPubkeys The pubkeys of the validators to consolidate from * @param targetPubkeys The pubkeys of the validators to consolidate to * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function requestConsolidation(bytes32 moduleName, bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) + function requestConsolidation(bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) external payable; @@ -239,14 +238,14 @@ interface IPufferProtocol { * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. * If the amount is 0, the withdrawal is total and the validator will be fully exited. * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. - * @param moduleName The name of the module * @param pubkeys The pubkeys of the validators to withdraw * @param gweiAmounts The amounts of the validators to withdraw, in Gwei + * @dev The pubkeys should be active validators on the same module * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) + function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external payable; diff --git a/mainnet-contracts/src/struct/ProtocolStorage.sol b/mainnet-contracts/src/struct/ProtocolStorage.sol index c87d18e2..06aa9b38 100644 --- a/mainnet-contracts/src/struct/ProtocolStorage.sol +++ b/mainnet-contracts/src/struct/ProtocolStorage.sol @@ -4,6 +4,8 @@ pragma solidity >=0.8.0 <0.9.0; import { Validator } from "../struct/Validator.sol"; import { NodeInfo } from "../struct/NodeInfo.sol"; import { PufferModule } from "../PufferModule.sol"; +import { ValidatorPosition } from "../struct/ValidatorPosition.sol"; + /** * @custom:storage-location erc7201:PufferProtocol.storage * @dev +-----------------------------------------------------------+ @@ -67,6 +69,11 @@ struct ProtocolStorage { * Slot 9 */ uint256 vtPenalty; + /** + * @dev Mapping of pubkeyHash => ValidatorPosition + * Slot 10 + */ + mapping(bytes32 pubkeyHash => ValidatorPosition validatorPosition) validatorPositions; } struct ModuleLimit { diff --git a/mainnet-contracts/src/struct/ValidatorPosition.sol b/mainnet-contracts/src/struct/ValidatorPosition.sol new file mode 100644 index 00000000..035399a8 --- /dev/null +++ b/mainnet-contracts/src/struct/ValidatorPosition.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * @dev Struct to indicate the module it belongs to and the index of the validator in the module + * @dev Packed in 1 storage slot + */ +struct ValidatorPosition { + address moduleAddress; + uint96 index; +} \ No newline at end of file From 8937e08185501711b134f3add696a3c4791a36ff Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 30 May 2025 11:14:41 +0200 Subject: [PATCH 17/82] provision flow (WIP) --- mainnet-contracts/src/PufferOracleV2.sol | 32 +++++- mainnet-contracts/src/PufferProtocol.sol | 99 +++++++++---------- .../src/interface/IPufferOracleV2.sol | 20 +++- .../src/interface/IPufferProtocol.sol | 14 +-- mainnet-contracts/src/struct/NodeInfo.sol | 2 + .../src/struct/ProtocolStorage.sol | 1 - mainnet-contracts/src/struct/Validator.sol | 1 + .../src/struct/ValidatorKeyData.sol | 1 + .../src/struct/ValidatorPosition.sol | 2 +- 9 files changed, 102 insertions(+), 70 deletions(-) diff --git a/mainnet-contracts/src/PufferOracleV2.sol b/mainnet-contracts/src/PufferOracleV2.sol index a68e32b1..d745ff31 100644 --- a/mainnet-contracts/src/PufferOracleV2.sol +++ b/mainnet-contracts/src/PufferOracleV2.sol @@ -51,6 +51,12 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { */ uint256 internal _validatorTicketPrice; + /** + * @dev Number of active batches + * Slot 4 + */ + uint256 internal _numberOfActiveBatches; + constructor(IGuardianModule guardianModule, address payable vault, address accessManager) AccessManaged(accessManager) { @@ -58,6 +64,7 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { PUFFER_VAULT = vault; _totalNumberOfValidators = 927122; // Oracle will be updated with the correct value _epochNumber = 268828; // Oracle will be updated with the correct value + _numberOfActiveBatches = 927122; // Oracle will be updated with the correct value _setMintPrice(0.01 ether); } @@ -65,10 +72,13 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { * @notice Exits the validator from the Beacon chain * @dev Restricted to PufferProtocol contract */ - function exitValidators(uint256 numberOfExits) public restricted { + function exitValidators(uint256 numberOfExits, uint256 numberOfBatchesExited) public restricted { // nosemgrep basic-arithmetic-underflow _numberOfActivePufferValidators -= numberOfExits; + // nosemgrep basic-arithmetic-underflow + _numberOfActiveBatches -= numberOfBatchesExited; emit NumberOfActiveValidators(_numberOfActivePufferValidators); + emit NumberOfActiveBatches(_numberOfActiveBatches); } /** @@ -77,11 +87,13 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { * The PufferVault balance is decreased by the same amount * @dev Restricted to PufferProtocol contract */ - function provisionNode() external restricted { + function provisionNode(uint256 numberOfBatches) external restricted { unchecked { ++_numberOfActivePufferValidators; + _numberOfActiveBatches += numberOfBatches; } emit NumberOfActiveValidators(_numberOfActivePufferValidators); + emit NumberOfActiveBatches(_numberOfActiveBatches); } /** @@ -96,9 +108,13 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { /** * @notice Updates the total number of validators * @param newTotalNumberOfValidators The new number of validators + * @param newNumActiveBatches The new number of active batches + * @param epochNumber The epoch number of the update + * @param guardianEOASignatures The guardian EOA signatures */ function setTotalNumberOfValidators( uint256 newTotalNumberOfValidators, + uint256 newNumActiveBatches, uint256 epochNumber, bytes[] calldata guardianEOASignatures ) external restricted { @@ -108,6 +124,7 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { GUARDIAN_MODULE.validateTotalNumberOfValidators(newTotalNumberOfValidators, epochNumber, guardianEOASignatures); emit TotalNumberOfValidatorsUpdated(_totalNumberOfValidators, newTotalNumberOfValidators, epochNumber); _totalNumberOfValidators = newTotalNumberOfValidators; + _numberOfActiveBatches = newNumActiveBatches; _epochNumber = epochNumber; } @@ -115,7 +132,7 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { * @inheritdoc IPufferOracle */ function getLockedEthAmount() external view returns (uint256) { - return _numberOfActivePufferValidators * 32 ether; + return _numberOfActiveBatches * 32 ether; } /** @@ -125,11 +142,18 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { return _totalNumberOfValidators; } + /** + * @inheritdoc IPufferOracleV2 + */ + function getNumberOfActiveBatches() external view returns (uint256) { + return _numberOfActiveBatches; + } + /** * @inheritdoc IPufferOracle */ function isOverBurstThreshold() external view returns (bool) { - return ((_numberOfActivePufferValidators * 100 / _totalNumberOfValidators) > _BURST_THRESHOLD); + return (((_numberOfActivePufferValidators * 100) / _totalNumberOfValidators) > _BURST_THRESHOLD); } /** diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index b693ee21..1259c78e 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -32,6 +32,7 @@ import { PufferModule } from "./PufferModule.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ + contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolStorage { /** * @dev Helper struct for the full withdrawals accounting @@ -194,13 +195,16 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad Permit calldata pufETHPermit, Permit calldata vtPermit ) external payable restricted { - ProtocolStorage storage $ = _getPufferProtocolStorage(); + // Check number of batches between 1 (32 ETH) and 64 (2048 ETH) + require(0 < data.numBatches && data.numBatches <= 64, InvalidNumberOfBatches()); // Revert if the permit amounts are non zero, but the msg.value is also non zero if (vtPermit.amount != 0 && pufETHPermit.amount != 0 && msg.value > 0) { revert InvalidETHAmount(); } + ProtocolStorage storage $ = _getPufferProtocolStorage(); + _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); // If the node operator is paying for the bond in ETH and wants to transfer VT from their wallet, the ETH amount they send must be equal the bond amount @@ -226,15 +230,16 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad revert InvalidVTAmount(); } + uint256 bondAmountEth = VALIDATOR_BOND * data.numBatches; uint256 bondAmount; // If the pufETH permit amount is zero, that means that the user is paying the bond with ETH if (pufETHPermit.amount == 0) { // Mint pufETH by depositing ETH and store the bond amount - bondAmount = PUFFER_VAULT.depositETH{ value: VALIDATOR_BOND }(address(this)); + bondAmount = PUFFER_VAULT.depositETH{ value: bondAmountEth }(address(this)); } else { // Calculate the pufETH amount that we need to transfer from the user - bondAmount = PUFFER_VAULT.convertToShares(VALIDATOR_BOND); + bondAmount = PUFFER_VAULT.convertToShares(bondAmountEth); _callPermit(address(PUFFER_VAULT), pufETHPermit); // slither-disable-next-line unchecked-transfer @@ -282,6 +287,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad --$.nodeOperatorInfo[node].pendingValidatorCount; ++$.nodeOperatorInfo[node].activeValidatorCount; + // Update numBatches now that validator becomes active + $.nodeOperatorInfo[node].numBatches += $.validators[moduleName][index].numBatches; + // Mark the validator as active $.validators[moduleName][index].status = Status.ACTIVE; } @@ -328,12 +336,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // Preemptively storing it to true to save slot calculation if (!alreadyChecked) { validatorPosition = $.validatorPositions[pubkeyHashSrc]; - require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); + require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); Validator memory validator = $.validators[moduleName][validatorPosition.index]; - require ( - validator.node == msg.sender && validator.status == Status.ACTIVE, - InvalidValidator() - ); + require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); } assembly { let slot := keccak256(add(pubkeyHashTarget, 0x20), 0x20) @@ -343,12 +348,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // Preemptively storing it to true to save slot calculation if (!alreadyChecked) { validatorPosition = $.validatorPositions[pubkeyHashTarget]; - require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); + require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); Validator memory validator = $.validators[moduleName][validatorPosition.index]; - require ( - validator.node == msg.sender && validator.status == Status.ACTIVE, - InvalidValidator() - ); + require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); } } @@ -384,12 +386,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad for (uint256 i = 0; i < pubkeys.length; i++) { pubkeyHash = keccak256(pubkeys[i]); validatorPosition = $.validatorPositions[pubkeyHash]; - require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); + require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); Validator memory validator = $.validators[moduleName][validatorPosition.index]; - require ( - validator.node == msg.sender && validator.status == Status.ACTIVE, - InvalidValidator() - ); + require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); } PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); @@ -410,6 +409,8 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad BurnAmounts memory burnAmounts; Withdrawals[] memory bondWithdrawals = new Withdrawals[](validatorInfos.length); + uint256 numExitedBatches; + // We MUST NOT do the burning/oracle update/transferring ETH from the PufferModule -> PufferVault // because it affects pufETH exchange rate @@ -423,6 +424,8 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad revert InvalidValidatorState(validator.status); } + numExitedBatches += validator.numBatches; + // Save the Node address for the bond transfer bondWithdrawals[i].node = validator.node; @@ -454,12 +457,14 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // nosemgrep basic-arithmetic-underflow $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); --$.nodeOperatorInfo[validator.node].activeValidatorCount; + $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; delete validator.node; delete validator.bond; delete validator.module; delete validator.status; delete validator.pubKey; + delete validator.numBatches; delete $.validatorPositions[keccak256(validator.pubKey)]; } @@ -467,15 +472,15 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad VALIDATOR_TICKET.burn(burnAmounts.vt); // Because we've calculated everything in the previous loop, we can do the burning PUFFER_VAULT.burn(burnAmounts.pufETH); - // Deduct 32 ETH from the `lockedETHAmount` on the PufferOracle - PUFFER_ORACLE.exitValidators(validatorInfos.length); + // Deduct 32 ETH per batch from the `lockedETHAmount` on the PufferOracle + PUFFER_ORACLE.exitValidators(validatorInfos.length, numExitedBatches); // In this loop, we transfer back the bonds, and do the accounting that affects the exchange rate for (uint256 i = 0; i < validatorInfos.length; ++i) { // If the withdrawal amount is bigger than 32 ETH, we cap it to 32 ETH // The excess is the rewards amount for that Node Operator uint256 transferAmount = - validatorInfos[i].withdrawalAmount > 32 ether ? 32 ether : validatorInfos[i].withdrawalAmount; + validatorInfos[i].withdrawalAmount > 32 ether ? 32 ether : validatorInfos[i].withdrawalAmount; // @todo: adapt to Pectra //solhint-disable-next-line avoid-low-level-calls (bool success,) = PufferModule(payable(validatorInfos[i].module)).call(address(PUFFER_VAULT), transferAmount, ""); @@ -574,18 +579,21 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @param indices The indices of the validators in the modules * @dev Restricted to the DAO */ - function setValidatorsPositions(bytes[] calldata pubkeys, address[] calldata moduleAddresses, uint96[] calldata indices) external restricted { + function setValidatorsPositions( + bytes[] calldata pubkeys, + address[] calldata moduleAddresses, + uint96[] calldata indices + ) external restricted { ProtocolStorage storage $ = _getPufferProtocolStorage(); for (uint256 i = 0; i < pubkeys.length; i++) { - $.validatorPositions[keccak256(pubkeys[i])] = ValidatorPosition({ - moduleAddress: moduleAddresses[i], - index: indices[i] - }); + $.validatorPositions[keccak256(pubkeys[i])] = + ValidatorPosition({ moduleAddress: moduleAddresses[i], index: indices[i] }); } } /** * @inheritdoc IPufferProtocol */ + function getVTPenalty() external view returns (uint256) { ProtocolStorage storage $ = _getPufferProtocolStorage(); return $.vtPenalty; @@ -765,13 +773,12 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad status: Status.PENDING, module: moduleAddress, bond: uint96(pufETHAmount), - node: msg.sender + node: msg.sender, + numBatches: data.numBatches }); - $.validatorPositions[keccak256(data.blsPubKey)] = ValidatorPosition({ - moduleAddress: moduleAddress, - index: uint96(pufferModuleIndex) - }); + $.validatorPositions[keccak256(data.blsPubKey)] = + ValidatorPosition({ moduleAddress: moduleAddress, index: uint96(pufferModuleIndex) }); $.nodeOperatorInfo[msg.sender].vtBalance += SafeCast.toUint96(vtAmount); @@ -862,6 +869,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // Case 2: // The withdrawal amount is less than 32 ETH, we burn the difference to cover up the loss for inactivity if (validatorInfo.withdrawalAmount < 32 ether) { + // @todo Adapt to Pectra pufETHBurnAmount = PUFFER_VAULT.convertToSharesUp(32 ether - validatorInfo.withdrawalAmount); } // Case 3: @@ -876,6 +884,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad bytes calldata validatorSignature ) internal { bytes memory validatorPubKey = $.validators[moduleName][index].pubKey; + uint8 numBatches = $.validators[moduleName][index].numBatches; bytes memory withdrawalCredentials = getWithdrawalCredentials($.validators[moduleName][index].module); @@ -884,15 +893,15 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad PufferModule module = $.modules[moduleName]; - // Transfer 32 ETH to the module - PUFFER_VAULT.transferETH(address(module), 32 ether); + // Transfer 32 ETH to this contract for each batch + PUFFER_VAULT.transferETH(address(this), numBatches * 32 ether); emit SuccessfullyProvisioned(validatorPubKey, index, moduleName); // Increase lockedETH on Puffer Oracle - PUFFER_ORACLE.provisionNode(); + PUFFER_ORACLE.provisionNode(numBatches); - BEACON_DEPOSIT_CONTRACT.deposit( + BEACON_DEPOSIT_CONTRACT.deposit{ value: numBatches * 32 ether }( validatorPubKey, module.getWithdrawalCredentials(), validatorSignature, depositDataRoot ); } @@ -942,25 +951,5 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); } - function _checkValidator(uint256 numValidators, ProtocolStorage storage $, bytes32 pubkeyHash, bytes32 moduleName) - internal - view - { - bool correct; - for (uint256 j = 0; j < numValidators; j++) { - Validator memory validator = $.validators[moduleName][j]; - if ( - validator.node == msg.sender && validator.status == Status.ACTIVE - && keccak256(validator.pubKey) == pubkeyHash - ) { - correct = true; - break; - } - } - if (!correct) { - revert InvalidValidator(); - } - } - function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } } diff --git a/mainnet-contracts/src/interface/IPufferOracleV2.sol b/mainnet-contracts/src/interface/IPufferOracleV2.sol index 0923f87a..b86b3835 100644 --- a/mainnet-contracts/src/interface/IPufferOracleV2.sol +++ b/mainnet-contracts/src/interface/IPufferOracleV2.sol @@ -17,6 +17,12 @@ interface IPufferOracleV2 is IPufferOracle { event NumberOfActiveValidators(uint256 numberOfActivePufferValidators); + /** + * @notice Emitted when the number of active batches is updated + * @param numberOfActiveBatches is the number of active batches + */ + event NumberOfActiveBatches(uint256 numberOfActiveBatches); + /** * @notice Emitted when the total number of validators is updated * @param oldNumberOfValidators is the old number of validators @@ -36,20 +42,28 @@ interface IPufferOracleV2 is IPufferOracle { */ function getNumberOfActiveValidators() external view returns (uint256); + /** + * @notice Returns the number of active batches of 32 ETH staked by the validators on Ethereum + */ + function getNumberOfActiveBatches() external view returns (uint256); + /** * @notice Exits `validatorNumber` validators, decreasing the `lockedETHAmount` by validatorNumber * 32 ETH. * It is called when when the validator exits the system in the `batchHandleWithdrawals` on the PufferProtocol. * In the same transaction, we are transferring full withdrawal ETH from the PufferModule to the Vault - * Decrementing the `lockedETHAmount` by 32 ETH and we burn the Node Operator's pufETH (bond) if we need to cover up the loss. + * Decrementing the `lockedETHAmount` by 32 ETH per batch and we burn the Node Operator's pufETH (bond) if we need to cover up the loss. + * @param validatorNumber is the number of validators to exit + * @param numberOfBatchesExited is the number of batches exited * @dev Restricted to PufferProtocol contract */ - function exitValidators(uint256 validatorNumber) external; + function exitValidators(uint256 validatorNumber, uint256 numberOfBatchesExited) external; /** * @notice Increases the `lockedETHAmount` on the PufferOracle by 32 ETH to account for a new deposit. * It is called when the Beacon chain receives a new deposit from the PufferProtocol. * The PufferVault's balance will simultaneously decrease by 32 ETH as the deposit is made. + * @param numberOfBatches is the number of batches to provision * @dev Restricted to PufferProtocol contract */ - function provisionNode() external; + function provisionNode(uint256 numberOfBatches) external; } diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 15459f10..ebd50a13 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -92,6 +92,12 @@ interface IPufferProtocol { */ error InputArrayLengthZero(); + /** + * @notice Thrown if the number of batches is 0 or greater than 64 + * @dev Signature "0x4ea54df9" + */ + error InvalidNumberOfBatches(); + /** * @notice Emitted when the number of active validators changes * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" @@ -230,9 +236,7 @@ interface IPufferProtocol { * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function requestConsolidation(bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) - external - payable; + function requestConsolidation(bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) external payable; /** * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. @@ -245,9 +249,7 @@ interface IPufferProtocol { * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) - external - payable; + function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external payable; /** * @notice Batch settling of validator withdrawals diff --git a/mainnet-contracts/src/struct/NodeInfo.sol b/mainnet-contracts/src/struct/NodeInfo.sol index c225c188..1c7b84be 100644 --- a/mainnet-contracts/src/struct/NodeInfo.sol +++ b/mainnet-contracts/src/struct/NodeInfo.sol @@ -8,4 +8,6 @@ struct NodeInfo { uint64 activeValidatorCount; // Number of active validators uint64 pendingValidatorCount; // Number of pending validators (registered but not yet provisioned) uint96 vtBalance; // Validator ticket balance + uint8 numBatches; // Number of batches + // @todo: Adapt with VT rework to fit a single slot } diff --git a/mainnet-contracts/src/struct/ProtocolStorage.sol b/mainnet-contracts/src/struct/ProtocolStorage.sol index 06aa9b38..1da5a879 100644 --- a/mainnet-contracts/src/struct/ProtocolStorage.sol +++ b/mainnet-contracts/src/struct/ProtocolStorage.sol @@ -14,7 +14,6 @@ import { ValidatorPosition } from "../struct/ValidatorPosition.sol"; * | | * +-----------------------------------------------------------+ */ - struct ProtocolStorage { /** * @dev Module weights diff --git a/mainnet-contracts/src/struct/Validator.sol b/mainnet-contracts/src/struct/Validator.sol index f1bddf25..03aefc0d 100644 --- a/mainnet-contracts/src/struct/Validator.sol +++ b/mainnet-contracts/src/struct/Validator.sol @@ -12,4 +12,5 @@ struct Validator { address module; // In which module is the Validator participating Status status; // Validator status bytes pubKey; // Validator public key + uint8 numBatches; // Number of batches } diff --git a/mainnet-contracts/src/struct/ValidatorKeyData.sol b/mainnet-contracts/src/struct/ValidatorKeyData.sol index 40726ba8..a62ebbdf 100644 --- a/mainnet-contracts/src/struct/ValidatorKeyData.sol +++ b/mainnet-contracts/src/struct/ValidatorKeyData.sol @@ -11,4 +11,5 @@ struct ValidatorKeyData { bytes[] deprecated_blsEncryptedPrivKeyShares; bytes deprecated_blsPubKeySet; bytes deprecated_raveEvidence; + uint8 numBatches; } diff --git a/mainnet-contracts/src/struct/ValidatorPosition.sol b/mainnet-contracts/src/struct/ValidatorPosition.sol index 035399a8..0ba9cd89 100644 --- a/mainnet-contracts/src/struct/ValidatorPosition.sol +++ b/mainnet-contracts/src/struct/ValidatorPosition.sol @@ -8,4 +8,4 @@ pragma solidity >=0.8.0 <0.9.0; struct ValidatorPosition { address moduleAddress; uint96 index; -} \ No newline at end of file +} From 3d35d5f841b07b32b79f297d3ab965f5cc268f9c Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Fri, 30 May 2025 09:15:40 +0000 Subject: [PATCH 18/82] forge fmt --- mainnet-contracts/src/struct/NodeInfo.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainnet-contracts/src/struct/NodeInfo.sol b/mainnet-contracts/src/struct/NodeInfo.sol index 1c7b84be..78fd1c3f 100644 --- a/mainnet-contracts/src/struct/NodeInfo.sol +++ b/mainnet-contracts/src/struct/NodeInfo.sol @@ -9,5 +9,5 @@ struct NodeInfo { uint64 pendingValidatorCount; // Number of pending validators (registered but not yet provisioned) uint96 vtBalance; // Validator ticket balance uint8 numBatches; // Number of batches - // @todo: Adapt with VT rework to fit a single slot + // @todo: Adapt with VT rework to fit a single slot } From b7e7d560e4d939862efa81a2f1fcd68034082429 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 30 May 2025 13:33:11 +0200 Subject: [PATCH 19/82] Removed return excess fee flow --- mainnet-contracts/src/PufferModule.sol | 11 ----------- mainnet-contracts/src/PufferModuleManager.sol | 10 ---------- mainnet-contracts/src/PufferProtocol.sol | 18 +----------------- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index f519bf8e..6fc4b270 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -43,15 +43,6 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { bytes32 private constant _PUFFER_MODULE_BASE_STORAGE = 0x501caad7d5b9c1542c99d193b659cbf5c57571609bcfc93d65f1e159821d6200; - modifier returnExcessFee() { - uint256 oldBalance = address(this).balance - msg.value; - _; - uint256 excessAmount = address(this).balance - oldBalance; - if (excessAmount > 0) { - Address.sendValue(payable(msg.sender), excessAmount); - } - } - constructor( IPufferProtocol protocol, address eigenPodManager, @@ -206,7 +197,6 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { payable virtual onlyPufferProtocol - returnExcessFee { ModuleStorage storage $ = _getPufferModuleStorage(); @@ -234,7 +224,6 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { payable virtual onlyPufferModuleManager - returnExcessFee { ModuleStorage storage $ = _getPufferModuleStorage(); diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index 24840322..4ffa88ca 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -42,15 +42,6 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, _; } - modifier returnExcessFee() { - uint256 oldBalance = address(this).balance - msg.value; - _; - uint256 excessAmount = address(this).balance - oldBalance; - if (excessAmount > 0) { - Address.sendValue(payable(msg.sender), excessAmount); - } - } - constructor(address pufferModuleBeacon, address restakingOperatorBeacon, address pufferProtocol) { PUFFER_MODULE_BEACON = pufferModuleBeacon; RESTAKING_OPERATOR_BEACON = restakingOperatorBeacon; @@ -269,7 +260,6 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, payable virtual restricted - returnExcessFee { if (pubkeys.length == 0) { revert InputArrayLengthZero(); diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 1259c78e..00c45a8b 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -19,7 +19,6 @@ import { ProtocolStorage, NodeInfo, ModuleLimit } from "./struct/ProtocolStorage import { LibBeaconchainContract } from "./LibBeaconchainContract.sol"; import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; @@ -97,15 +96,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ IBeaconDepositContract public immutable override BEACON_DEPOSIT_CONTRACT; - modifier returnExcessFee() { - uint256 oldBalance = address(this).balance - msg.value; - _; - uint256 excessAmount = address(this).balance - oldBalance; - if (excessAmount > 0) { - Address.sendValue(payable(msg.sender), excessAmount); - } - } - constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, @@ -303,7 +293,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad payable virtual restricted - returnExcessFee { if (srcPubkeys.length == 0) { revert InputArrayLengthZero(); @@ -363,12 +352,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @inheritdoc IPufferProtocol * @dev Restricted to Node Operators */ - function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) - external - payable - restricted - returnExcessFee - { + function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external payable restricted { if (pubkeys.length == 0) { revert InputArrayLengthZero(); } From 199b7d28f15a270fc7fe9b9d31f02513da0177b4 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 30 May 2025 14:05:55 +0200 Subject: [PATCH 20/82] Use moduleName + indices in withdrawal and consolidation flows --- mainnet-contracts/src/PufferProtocol.sol | 105 +++++------------- .../src/interface/IPufferProtocol.sol | 16 ++- .../src/struct/ProtocolStorage.sol | 6 - .../src/struct/ValidatorPosition.sol | 11 -- 4 files changed, 37 insertions(+), 101 deletions(-) delete mode 100644 mainnet-contracts/src/struct/ValidatorPosition.sol diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 00c45a8b..2eaba3c5 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -12,7 +12,6 @@ import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; import { ValidatorKeyData } from "./struct/ValidatorKeyData.sol"; import { Validator } from "./struct/Validator.sol"; -import { ValidatorPosition } from "./struct/ValidatorPosition.sol"; import { Permit } from "./structs/Permit.sol"; import { Status } from "./struct/Status.sol"; import { ProtocolStorage, NodeInfo, ModuleLimit } from "./struct/ProtocolStorage.sol"; @@ -288,62 +287,35 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @inheritdoc IPufferProtocol * @dev Restricted to Node Operators */ - function requestConsolidation(bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) + function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable virtual restricted { - if (srcPubkeys.length == 0) { + if (srcIndices.length == 0) { revert InputArrayLengthZero(); } - if (srcPubkeys.length != targetPubkeys.length) { + if (srcIndices.length != targetIndices.length) { revert InputArrayLengthMismatch(); } ProtocolStorage storage $ = _getPufferProtocolStorage(); - // validate pubkeys belong to that node - bool alreadyChecked; - bytes32 pubkeyHashSrc; - bytes32 pubkeyHashTarget; - pubkeyHashSrc = keccak256(srcPubkeys[0]); - ValidatorPosition memory validatorPosition = $.validatorPositions[pubkeyHashSrc]; - address moduleAddress = validatorPosition.moduleAddress; - bytes32 moduleName = PufferModule(payable(moduleAddress)).NAME(); + bytes[] memory srcPubkeys = new bytes[](srcIndices.length); + bytes[] memory targetPubkeys = new bytes[](targetIndices.length); + Validator memory validator; for (uint256 i = 0; i < srcPubkeys.length; i++) { - pubkeyHashSrc = keccak256(srcPubkeys[i]); - pubkeyHashTarget = keccak256(targetPubkeys[i]); - if (pubkeyHashSrc == pubkeyHashTarget) { - revert InvalidValidator(); - } - assembly { - let slot := keccak256(add(pubkeyHashSrc, 0x20), 0x20) - alreadyChecked := tload(slot) - tstore(slot, true) - } - // Preemptively storing it to true to save slot calculation - if (!alreadyChecked) { - validatorPosition = $.validatorPositions[pubkeyHashSrc]; - require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); - Validator memory validator = $.validators[moduleName][validatorPosition.index]; - require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); - } - assembly { - let slot := keccak256(add(pubkeyHashTarget, 0x20), 0x20) - alreadyChecked := tload(slot) - tstore(slot, true) - } - // Preemptively storing it to true to save slot calculation - if (!alreadyChecked) { - validatorPosition = $.validatorPositions[pubkeyHashTarget]; - require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); - Validator memory validator = $.validators[moduleName][validatorPosition.index]; - require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); - } + require(srcIndices[i] != targetIndices[i], InvalidValidator()); + validator = $.validators[moduleName][srcIndices[i]]; + require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); + srcPubkeys[i] = validator.pubKey; + validator = $.validators[moduleName][targetIndices[i]]; + require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); + targetPubkeys[i] = validator.pubKey; } - PufferModule(payable(moduleAddress)).requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); + $.modules[moduleName].requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); emit ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); } @@ -352,27 +324,26 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @inheritdoc IPufferProtocol * @dev Restricted to Node Operators */ - function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external payable restricted { - if (pubkeys.length == 0) { + function requestWithdrawal(bytes32 moduleName, uint256[] calldata indices, uint64[] calldata gweiAmounts) + external + payable + restricted + { + if (indices.length == 0) { revert InputArrayLengthZero(); } - if (pubkeys.length != gweiAmounts.length) { + if (indices.length != gweiAmounts.length) { revert InputArrayLengthMismatch(); } ProtocolStorage storage $ = _getPufferProtocolStorage(); - // validate pubkeys belong to that node and are active + bytes[] memory pubkeys = new bytes[](indices.length); - bytes32 pubkeyHash = keccak256(pubkeys[0]); - ValidatorPosition memory validatorPosition = $.validatorPositions[pubkeyHash]; - address moduleAddress = validatorPosition.moduleAddress; - bytes32 moduleName = PufferModule(payable(moduleAddress)).NAME(); - for (uint256 i = 0; i < pubkeys.length; i++) { - pubkeyHash = keccak256(pubkeys[i]); - validatorPosition = $.validatorPositions[pubkeyHash]; - require(validatorPosition.moduleAddress == moduleAddress, InvalidValidator()); - Validator memory validator = $.validators[moduleName][validatorPosition.index]; + // validate pubkeys belong to that node and are active + for (uint256 i = 0; i < indices.length; i++) { + Validator memory validator = $.validators[moduleName][indices[i]]; require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); + pubkeys[i] = validator.pubKey; } PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); @@ -449,8 +420,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad delete validator.status; delete validator.pubKey; delete validator.numBatches; - - delete $.validatorPositions[keccak256(validator.pubKey)]; } VALIDATOR_TICKET.burn(burnAmounts.vt); @@ -556,28 +525,9 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad _setVTPenalty(newPenaltyAmount); } - /** - * @notice Admin function to set the positions of the validators - * @param pubkeys The pubkeys of the validators - * @param moduleAddresses The addresses of the modules - * @param indices The indices of the validators in the modules - * @dev Restricted to the DAO - */ - function setValidatorsPositions( - bytes[] calldata pubkeys, - address[] calldata moduleAddresses, - uint96[] calldata indices - ) external restricted { - ProtocolStorage storage $ = _getPufferProtocolStorage(); - for (uint256 i = 0; i < pubkeys.length; i++) { - $.validatorPositions[keccak256(pubkeys[i])] = - ValidatorPosition({ moduleAddress: moduleAddresses[i], index: indices[i] }); - } - } /** * @inheritdoc IPufferProtocol */ - function getVTPenalty() external view returns (uint256) { ProtocolStorage storage $ = _getPufferProtocolStorage(); return $.vtPenalty; @@ -761,9 +711,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad numBatches: data.numBatches }); - $.validatorPositions[keccak256(data.blsPubKey)] = - ValidatorPosition({ moduleAddress: moduleAddress, index: uint96(pufferModuleIndex) }); - $.nodeOperatorInfo[msg.sender].vtBalance += SafeCast.toUint96(vtAmount); // Increment indices for this module and number of validators registered diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index ebd50a13..45bc7886 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -230,26 +230,32 @@ interface IPufferProtocol { /** * @notice Requests a consolidation for the given validators. This consolidation consists on merging one validator into another one - * @param srcPubkeys The pubkeys of the validators to consolidate from - * @param targetPubkeys The pubkeys of the validators to consolidate to + * @param moduleName The name of the module + * @param srcIndices The indices of the validators to consolidate from + * @param targetIndices The indices of the validators to consolidate to * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function requestConsolidation(bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) external payable; + function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) + external + payable; /** * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. * If the amount is 0, the withdrawal is total and the validator will be fully exited. * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. - * @param pubkeys The pubkeys of the validators to withdraw + * @param moduleName The name of the module + * @param indices The indices of the validators to withdraw * @param gweiAmounts The amounts of the validators to withdraw, in Gwei * @dev The pubkeys should be active validators on the same module * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded * to the caller from the EigenPod */ - function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external payable; + function requestWithdrawal(bytes32 moduleName, uint256[] calldata indices, uint64[] calldata gweiAmounts) + external + payable; /** * @notice Batch settling of validator withdrawals diff --git a/mainnet-contracts/src/struct/ProtocolStorage.sol b/mainnet-contracts/src/struct/ProtocolStorage.sol index 1da5a879..571d2534 100644 --- a/mainnet-contracts/src/struct/ProtocolStorage.sol +++ b/mainnet-contracts/src/struct/ProtocolStorage.sol @@ -4,7 +4,6 @@ pragma solidity >=0.8.0 <0.9.0; import { Validator } from "../struct/Validator.sol"; import { NodeInfo } from "../struct/NodeInfo.sol"; import { PufferModule } from "../PufferModule.sol"; -import { ValidatorPosition } from "../struct/ValidatorPosition.sol"; /** * @custom:storage-location erc7201:PufferProtocol.storage @@ -68,11 +67,6 @@ struct ProtocolStorage { * Slot 9 */ uint256 vtPenalty; - /** - * @dev Mapping of pubkeyHash => ValidatorPosition - * Slot 10 - */ - mapping(bytes32 pubkeyHash => ValidatorPosition validatorPosition) validatorPositions; } struct ModuleLimit { diff --git a/mainnet-contracts/src/struct/ValidatorPosition.sol b/mainnet-contracts/src/struct/ValidatorPosition.sol deleted file mode 100644 index 0ba9cd89..00000000 --- a/mainnet-contracts/src/struct/ValidatorPosition.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -/** - * @dev Struct to indicate the module it belongs to and the index of the validator in the module - * @dev Packed in 1 storage slot - */ -struct ValidatorPosition { - address moduleAddress; - uint96 index; -} From b0e59aa5bb79518ba19bd0bd527ffdc32d5133cd Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 30 May 2025 14:37:27 +0200 Subject: [PATCH 21/82] Implemented upgrading validators to type2 in PMM --- mainnet-contracts/src/PufferModuleManager.sol | 19 +++++++++++++++++++ .../src/interface/IPufferModuleManager.sol | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index 4ffa88ca..eda3ccf0 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -243,6 +243,25 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, emit PufferModuleUndelegated(moduleName); } + + /** + * @notice Upgrades the given validators to consolidating (0x02) + * @param moduleName The name of the module + * @param pubkeys The pubkeys of the validators to upgrade + * @dev The funcion does not check that the pubkeys belong to the module + * @dev Restricted to the DAO + * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * to the caller from the EigenPod + */ + function upgradeToConsolidating(bytes32 moduleName, bytes[] calldata pubkeys) external payable virtual restricted { + address moduleAddress = IPufferProtocol(PUFFER_PROTOCOL).getModuleAddress(moduleName); + + PufferModule(payable(moduleAddress)).requestConsolidation{ value: msg.value }(pubkeys, pubkeys); + + emit PufferModuleUpgradedToConsolidating(moduleName, pubkeys); + } + /** * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. * If the amount is 0, the withdrawal is total and the validator will be fully exited. diff --git a/mainnet-contracts/src/interface/IPufferModuleManager.sol b/mainnet-contracts/src/interface/IPufferModuleManager.sol index 50734ca0..146e99ce 100644 --- a/mainnet-contracts/src/interface/IPufferModuleManager.sol +++ b/mainnet-contracts/src/interface/IPufferModuleManager.sol @@ -83,6 +83,14 @@ interface IPufferModuleManager { */ event PufferModuleUndelegated(bytes32 indexed moduleName); + /** + * @notice Emitted when validators of a module are upgraded to consolidating (0x02) + * @param moduleName the module name + * @param pubkeys the pubkeys of the validators to uppgrade + * @dev Signature "0x591863087d102c41b3f4d214fefc262505274cf32ef4e08ef20184140796614a" + */ + event PufferModuleUpgradedToConsolidating(bytes32 indexed moduleName, bytes[] pubkeys); + /** * @notice Emitted when a withdrawal is requested * @param moduleName the module name to be undelegated From cb996faa40decb1bc31e39dd92dd3f8844bbff9f Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Fri, 30 May 2025 12:38:25 +0000 Subject: [PATCH 22/82] forge fmt --- mainnet-contracts/src/PufferModuleManager.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index eda3ccf0..fe663b82 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -243,7 +243,6 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, emit PufferModuleUndelegated(moduleName); } - /** * @notice Upgrades the given validators to consolidating (0x02) * @param moduleName The name of the module From 4ae75c398517bc7c8d48fbb40e5ccdb1f1b4d838 Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 2 Jun 2025 13:06:29 +0200 Subject: [PATCH 23/82] Added accounting to consolidation flow --- mainnet-contracts/src/PufferProtocol.sol | 38 ++++++++++++++++-------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 2eaba3c5..bd094c4b 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -304,15 +304,23 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad bytes[] memory srcPubkeys = new bytes[](srcIndices.length); bytes[] memory targetPubkeys = new bytes[](targetIndices.length); - Validator memory validator; + Validator storage validatorSrc; + Validator storage validatorTarget; for (uint256 i = 0; i < srcPubkeys.length; i++) { require(srcIndices[i] != targetIndices[i], InvalidValidator()); - validator = $.validators[moduleName][srcIndices[i]]; - require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); - srcPubkeys[i] = validator.pubKey; - validator = $.validators[moduleName][targetIndices[i]]; - require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); - targetPubkeys[i] = validator.pubKey; + validatorSrc = $.validators[moduleName][srcIndices[i]]; + require(validatorSrc.node == msg.sender && validatorSrc.status == Status.ACTIVE, InvalidValidator()); + srcPubkeys[i] = validatorSrc.pubKey; + validatorTarget = $.validators[moduleName][targetIndices[i]]; + require(validatorTarget.node == msg.sender && validatorTarget.status == Status.ACTIVE, InvalidValidator()); + targetPubkeys[i] = validatorTarget.pubKey; + + // Update accounting + validatorTarget.bond += validatorSrc.bond; + validatorTarget.numBatches += validatorSrc.numBatches; + + _deleteValidator(validatorSrc); + // Node info needs no update since all stays in the same node operator } $.modules[moduleName].requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); @@ -414,12 +422,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad --$.nodeOperatorInfo[validator.node].activeValidatorCount; $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; - delete validator.node; - delete validator.bond; - delete validator.module; - delete validator.status; - delete validator.pubKey; - delete validator.numBatches; + _deleteValidator(validator); } VALIDATOR_TICKET.burn(burnAmounts.vt); @@ -882,5 +885,14 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); } + function _deleteValidator(Validator storage validator) internal { + delete validator.node; + delete validator.bond; + delete validator.module; + delete validator.status; + delete validator.pubKey; + delete validator.numBatches; + } + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } } From 7f0f5c25423577bafb3008629b520f721f5f7a98 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Wed, 4 Jun 2025 08:28:34 +0200 Subject: [PATCH 24/82] update natspec, remove buggy code --- mainnet-contracts/src/PufferProtocol.sol | 11 +++-------- mainnet-contracts/src/interface/IPufferProtocol.sol | 11 +++++++++++ mainnet-contracts/test/unit/PufferProtocol.t.sol | 6 ++++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 877a720c..33b05d09 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -175,11 +175,10 @@ contract PufferProtocol is } /** - * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`) - * This ETH is used as a VT payment. + * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ - function depositValidationTime(address node, uint256 vtConsumptionAmount, bytes[] calldata vtConsumptionSignature) + function depositValidationTime(address node, uint256 totalEpochsValidated, bytes[] calldata vtConsumptionSignature) external payable restricted @@ -192,7 +191,7 @@ contract PufferProtocol is _settleVTAccounting({ $: $, node: node, - totalEpochsValidated: vtConsumptionAmount, + totalEpochsValidated: totalEpochsValidated, vtConsumptionSignature: vtConsumptionSignature, deprecated_burntVTs: 0 }); @@ -891,10 +890,6 @@ contract PufferProtocol is uint256 amountToConsume = (totalEpochsValidated - previousTotalEpochsValidated - validatorTicketsBurnt) * meanPrice; - if (amountToConsume <= $.vtPenaltyEpochs * meanPrice) { - amountToConsume = $.vtPenaltyEpochs * meanPrice; - } - // Update the current epoch VT price for the node operator $.nodeOperatorInfo[node].epochPrice = epochCurrentPrice; $.nodeOperatorInfo[node].totalEpochsValidated = totalEpochsValidated; diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 8cb5bd08..0be51d9f 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -203,6 +203,17 @@ interface IPufferProtocol { */ function depositValidatorTickets(Permit calldata permit, address node) external; + /** + * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`). + * Deposits Validation Time for the `node`. Validation Time is in native ETH. + * @param node is the node operator address + * @param totalEpochsValidated is the total number of epochs validated by that node operator + * @param vtConsumptionSignature is the signature from the guardians over the total number of epochs validated + */ + function depositValidationTime(address node, uint256 totalEpochsValidated, bytes[] calldata vtConsumptionSignature) + external + payable; + /** * @notice Withdraws the `amount` of Validator Tickers from the `msg.sender` to the `recipient` * DEPRECATED - This method is deprecated and will be removed in the future upgrade diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index d7bb6207..4a6f93a9 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -1696,6 +1696,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + // We would handle this case on the backend, the guardians would return a value + a signature to mitigate this + // Alice exited after 1 day _executeFullWithdrawal( StoppedValidatorInfo({ @@ -1703,8 +1705,8 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - totalEpochsValidated: 1 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 1 * EPOCHS_PER_DAY), + totalEpochsValidated: 10 * EPOCHS_PER_DAY, // penalty is 10 + vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 10 * EPOCHS_PER_DAY), // penalty is 10 wasSlashed: false }) ); From 8bb695824c2565f40924c8568d3cdd04a44dd0c4 Mon Sep 17 00:00:00 2001 From: Eladio Date: Thu, 5 Jun 2025 14:29:02 +0200 Subject: [PATCH 25/82] Adapted requestWithdrawal and batchHandleWithdrawals --- mainnet-contracts/src/PufferModule.sol | 3 +- mainnet-contracts/src/PufferModuleManager.sol | 3 +- mainnet-contracts/src/PufferProtocol.sol | 234 ++++++++++++------ .../src/interface/IPufferProtocol.sol | 45 +++- .../src/struct/StoppedValidatorInfo.sol | 2 + .../src/struct/WithdrawalType.sol | 11 + 6 files changed, 208 insertions(+), 90 deletions(-) create mode 100644 mainnet-contracts/src/struct/WithdrawalType.sol diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index 6fc4b270..5b8b0e5d 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -216,8 +216,7 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { * @param gweiAmounts The amounts of the validators to withdraw, in Gwei * @dev Only callable by the PufferModuleManager * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) - * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded - * to the caller from the EigenPod + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule */ function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index fe663b82..1ce4068c 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -270,8 +270,7 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, * @param gweiAmounts The amounts of the validators to withdraw, in Gwei * @dev Restricted to the VALIDATOR_EXITOR role and the PufferProtocol * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) - * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded - * to the caller from the EigenPod + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule */ function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) external diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index bd094c4b..ae677731 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -14,6 +14,7 @@ import { ValidatorKeyData } from "./struct/ValidatorKeyData.sol"; import { Validator } from "./struct/Validator.sol"; import { Permit } from "./structs/Permit.sol"; import { Status } from "./struct/Status.sol"; +import { WithdrawalType } from "./struct/WithdrawalType.sol"; import { ProtocolStorage, NodeInfo, ModuleLimit } from "./struct/ProtocolStorage.sol"; import { LibBeaconchainContract } from "./LibBeaconchainContract.sol"; import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; @@ -23,6 +24,8 @@ import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; +import { NoncesUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/NoncesUpgradeable.sol"; + /** * @title PufferProtocol * @author Puffer Finance @@ -30,8 +33,13 @@ import { PufferModule } from "./PufferModule.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ - -contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolStorage { +contract PufferProtocol is + IPufferProtocol, + AccessManagedUpgradeable, + UUPSUpgradeable, + PufferProtocolStorage, + NoncesUpgradeable +{ /** * @dev Helper struct for the full withdrawals accounting * The amounts of VT and pufETH to burn at the end of the withdrawal @@ -48,6 +56,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad struct Withdrawals { uint256 pufETHAmount; address node; + uint256 numBatches; } /** @@ -319,7 +328,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad validatorTarget.bond += validatorSrc.bond; validatorTarget.numBatches += validatorSrc.numBatches; - _deleteValidator(validatorSrc); + delete $.validators[moduleName][srcIndices[i]]; // Node info needs no update since all stays in the same node operator } @@ -332,26 +341,42 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @inheritdoc IPufferProtocol * @dev Restricted to Node Operators */ - function requestWithdrawal(bytes32 moduleName, uint256[] calldata indices, uint64[] calldata gweiAmounts) - external - payable - restricted - { - if (indices.length == 0) { - revert InputArrayLengthZero(); - } - if (indices.length != gweiAmounts.length) { - revert InputArrayLengthMismatch(); - } + function requestWithdrawal( + bytes32 moduleName, + uint256[] calldata indices, + uint64[] calldata gweiAmounts, + WithdrawalType[] calldata withdrawalType, + bytes[][] calldata validatorAmountsSignatures + ) external payable restricted { ProtocolStorage storage $ = _getPufferProtocolStorage(); bytes[] memory pubkeys = new bytes[](indices.length); + uint256 batchSizeGweis = 32 ether / 1 gwei; + // validate pubkeys belong to that node and are active for (uint256 i = 0; i < indices.length; i++) { - Validator memory validator = $.validators[moduleName][indices[i]]; - require(validator.node == msg.sender && validator.status == Status.ACTIVE, InvalidValidator()); - pubkeys[i] = validator.pubKey; + require($.validators[moduleName][indices[i]].node == msg.sender, InvalidValidator()); + pubkeys[i] = $.validators[moduleName][indices[i]].pubKey; + uint256 gweiAmount = gweiAmounts[i]; + + if (withdrawalType[i] == WithdrawalType.EXIT_VALIDATOR) { + require(gweiAmount == 0, InvalidWithdrawAmount()); + } else if (withdrawalType[i] == WithdrawalType.DOWNSIZE) { + uint256 batches = gweiAmount / batchSizeGweis; + require( + batches > $.validators[moduleName][indices[i]].numBatches && batches * batchSizeGweis == gweiAmount, + InvalidWithdrawAmount() + ); + } else if (withdrawalType[i] == WithdrawalType.WITHDRAW_REWARDS) { + bytes32 messageHash = + keccak256(abi.encode(msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(msg.sender))); + + GUARDIAN_MODULE.validateGuardiansEOASignatures({ + eoaSignatures: validatorAmountsSignatures[i], + signedMessageHash: messageHash + }); + } } PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); @@ -387,42 +412,95 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad revert InvalidValidatorState(validator.status); } - numExitedBatches += validator.numBatches; - - // Save the Node address for the bond transfer - bondWithdrawals[i].node = validator.node; - - uint96 bondAmount = validator.bond; - // Get the burnAmount for the withdrawal at the current exchange rate - uint256 burnAmount = - _getBondBurnAmount({ validatorInfo: validatorInfos[i], validatorBondAmount: bondAmount }); - uint256 vtBurnAmount = _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i]); - - // Update the burnAmounts - burnAmounts.pufETH += burnAmount; - burnAmounts.vt += vtBurnAmount; - - // Store the withdrawal amount for that node operator - // nosemgrep basic-arithmetic-underflow - bondWithdrawals[i].pufETHAmount = (bondAmount - burnAmount); - - emit ValidatorExited({ - pubKey: validator.pubKey, - pufferModuleIndex: validatorInfos[i].pufferModuleIndex, - moduleName: validatorInfos[i].moduleName, - pufETHBurnAmount: burnAmount, - vtBurnAmount: vtBurnAmount - }); - - // Decrease the number of registered validators for that module - _decreaseNumberOfRegisteredValidators($, validatorInfos[i].moduleName); - // Storage VT and the active validator count update for the Node Operator - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); - --$.nodeOperatorInfo[validator.node].activeValidatorCount; - $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; - - _deleteValidator(validator); + if (validatorInfos[i].isDownsize) { + uint8 numDownsizeBatches = uint8(validatorInfos[i].withdrawalAmount / 32 ether); + numExitedBatches += numDownsizeBatches; + + // Save the Node address for the bond transfer + bondWithdrawals[i].node = validator.node; + + // We burn the bond according to previous burn rate (before downsize) + uint256 burnAmount = _getBondBurnAmount({ + validatorInfo: validatorInfos[i], + validatorBondAmount: validator.bond, + numBatches: validator.numBatches + }); + + // However the burned part of the bond will be distributed between part of the bond returned and bond remaining (proportional to downsizing) + + // We burn the VT according to previous burn rate (before downsize) + uint256 vtBurnAmount = + _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i], validator.numBatches); + + // We update the burnAmounts + burnAmounts.pufETH += burnAmount; + burnAmounts.vt += vtBurnAmount; + + uint256 exitingBond = (validator.bond - burnAmount) * numDownsizeBatches / validator.numBatches; + + // We update the bondWithdrawals + bondWithdrawals[i].pufETHAmount = exitingBond; + bondWithdrawals[i].numBatches = numDownsizeBatches; + emit ValidatorDownsized({ + pubKey: validator.pubKey, + pufferModuleIndex: validatorInfos[i].pufferModuleIndex, + moduleName: validatorInfos[i].moduleName, + pufETHBurnAmount: burnAmount, + vtBurnAmount: vtBurnAmount, + epoch: validatorInfos[i].endEpoch, + numBatchesBefore: validator.numBatches, + numBatchesAfter: validator.numBatches - numDownsizeBatches + }); + + $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); + $.nodeOperatorInfo[validator.node].numBatches -= numDownsizeBatches; + + validator.bond -= uint96(exitingBond); + validator.numBatches -= numDownsizeBatches; + } else { + numExitedBatches += validator.numBatches; + + // Save the Node address for the bond transfer + bondWithdrawals[i].node = validator.node; + + uint96 bondAmount = validator.bond; + // Get the burnAmount for the withdrawal at the current exchange rate + uint256 burnAmount = _getBondBurnAmount({ + validatorInfo: validatorInfos[i], + validatorBondAmount: bondAmount, + numBatches: validator.numBatches + }); + uint256 vtBurnAmount = + _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i], validator.numBatches); + + // Update the burnAmounts + burnAmounts.pufETH += burnAmount; + burnAmounts.vt += vtBurnAmount; + + // Store the withdrawal amount for that node operator + // nosemgrep basic-arithmetic-underflow + bondWithdrawals[i].pufETHAmount = (bondAmount - burnAmount); + bondWithdrawals[i].numBatches = validator.numBatches; + emit ValidatorExited({ + pubKey: validator.pubKey, + pufferModuleIndex: validatorInfos[i].pufferModuleIndex, + moduleName: validatorInfos[i].moduleName, + pufETHBurnAmount: burnAmount, + vtBurnAmount: vtBurnAmount + }); + + // Decrease the number of registered validators for that module + _decreaseNumberOfRegisteredValidators($, validatorInfos[i].moduleName); + // Storage VT and the active validator count update for the Node Operator + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); + --$.nodeOperatorInfo[validator.node].activeValidatorCount; + $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; + + delete $.validators[validatorInfos[i].moduleName][ + validatorInfos[i].pufferModuleIndex + ]; + } } VALIDATOR_TICKET.burn(burnAmounts.vt); @@ -435,8 +513,10 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad for (uint256 i = 0; i < validatorInfos.length; ++i) { // If the withdrawal amount is bigger than 32 ETH, we cap it to 32 ETH // The excess is the rewards amount for that Node Operator - uint256 transferAmount = - validatorInfos[i].withdrawalAmount > 32 ether ? 32 ether : validatorInfos[i].withdrawalAmount; // @todo: adapt to Pectra + uint256 maxWithdrawalAmount = bondWithdrawals[i].numBatches * 32 ether; + uint256 transferAmount = validatorInfos[i].withdrawalAmount > maxWithdrawalAmount + ? maxWithdrawalAmount + : validatorInfos[i].withdrawalAmount; //solhint-disable-next-line avoid-low-level-calls (bool success,) = PufferModule(payable(validatorInfos[i].module)).call(address(PUFFER_VAULT), transferAmount, ""); @@ -789,11 +869,11 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad $.minimumVtAmount = newMinimumVtAmount; } - function _getBondBurnAmount(StoppedValidatorInfo calldata validatorInfo, uint256 validatorBondAmount) - internal - view - returns (uint256 pufETHBurnAmount) - { + function _getBondBurnAmount( + StoppedValidatorInfo calldata validatorInfo, + uint256 validatorBondAmount, + uint8 numBatches + ) internal view returns (uint256 pufETHBurnAmount) { // Case 1: // The Validator was slashed, we burn the whole bond for that validator if (validatorInfo.wasSlashed) { @@ -801,13 +881,13 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad } // Case 2: - // The withdrawal amount is less than 32 ETH, we burn the difference to cover up the loss for inactivity - if (validatorInfo.withdrawalAmount < 32 ether) { - // @todo Adapt to Pectra - pufETHBurnAmount = PUFFER_VAULT.convertToSharesUp(32 ether - validatorInfo.withdrawalAmount); + // The withdrawal amount is less than 32 ETH * numBatches, we burn the difference to cover up the loss for inactivity + if (validatorInfo.withdrawalAmount < 32 ether * numBatches) { + pufETHBurnAmount = PUFFER_VAULT.convertToSharesUp(32 ether * numBatches - validatorInfo.withdrawalAmount); } + // Case 3: - // Withdrawal amount was >= 32 ether, we don't burn anything + // Withdrawal amount was >= 32 ETH * numBatches, we don't burn anything return pufETHBurnAmount; } @@ -840,17 +920,18 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad ); } - function _getVTBurnAmount(ProtocolStorage storage $, address node, StoppedValidatorInfo calldata validatorInfo) - internal - view - returns (uint256) - { + function _getVTBurnAmount( + ProtocolStorage storage $, + address node, + StoppedValidatorInfo calldata validatorInfo, + uint8 numBatches + ) internal view returns (uint256) { uint256 validatedEpochs = validatorInfo.endEpoch - validatorInfo.startEpoch; // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up - uint256 vtBurnAmount = validatedEpochs * 4444444444444445; + uint256 vtBurnAmount = validatedEpochs * 4444444444444445 * numBatches; - uint256 minimumVTAmount = $.minimumVtAmount; + uint256 minimumVTAmount = $.minimumVtAmount * numBatches; uint256 nodeVTBalance = $.nodeOperatorInfo[node].vtBalance; // If the VT burn amount is less than the minimum VT amount that means that the node operator exited early @@ -885,14 +966,5 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); } - function _deleteValidator(Validator storage validator) internal { - delete validator.node; - delete validator.bond; - delete validator.module; - delete validator.status; - delete validator.pubKey; - delete validator.numBatches; - } - function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } } diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 45bc7886..d6f68191 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -8,6 +8,7 @@ import { PufferModuleManager } from "../PufferModuleManager.sol"; import { PufferVaultV5 } from "../PufferVaultV5.sol"; import { IPufferOracleV2 } from "../interface/IPufferOracleV2.sol"; import { Status } from "../struct/Status.sol"; +import { WithdrawalType } from "../struct/WithdrawalType.sol"; import { Permit } from "../structs/Permit.sol"; import { ValidatorTicket } from "../ValidatorTicket.sol"; import { NodeInfo } from "../struct/NodeInfo.sol"; @@ -98,6 +99,12 @@ interface IPufferProtocol { */ error InvalidNumberOfBatches(); + /** + * @notice Thrown if the withdrawal amount is invalid + * @dev Signature "0xdb73cdf0" + */ + error InvalidWithdrawAmount(); + /** * @notice Emitted when the number of active validators changes * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" @@ -177,6 +184,29 @@ interface IPufferProtocol { uint256 vtBurnAmount ); + /** + * @notice Emitted when a validator is downsized + * @param pubKey is the validator public key + * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain + * @param moduleName is the staking Module + * @param pufETHBurnAmount The amount of pufETH burned from the Node Operator + * @param vtBurnAmount The amount of Validator Tickets burned from the Node Operator + * @param epoch The epoch of the downsize + * @param numBatchesBefore The number of batches before the downsize + * @param numBatchesAfter The number of batches after the downsize + * @dev Signature "0x708d62f89df6fdb944118762f267baa489a8512915584a6b271365c6baec6df4" + */ + event ValidatorDownsized( + bytes pubKey, + uint256 indexed pufferModuleIndex, + bytes32 indexed moduleName, + uint256 pufETHBurnAmount, + uint256 vtBurnAmount, + uint256 epoch, + uint256 numBatchesBefore, + uint256 numBatchesAfter + ); + /** * @notice Emitted when a consolidation is requested * @param moduleName is the module name @@ -248,14 +278,19 @@ interface IPufferProtocol { * @param moduleName The name of the module * @param indices The indices of the validators to withdraw * @param gweiAmounts The amounts of the validators to withdraw, in Gwei + * @param withdrawalType The type of withdrawal + * @param validatorAmountsSignatures The signatures of the guardians to validate the amount of the validators to withdraw * @dev The pubkeys should be active validators on the same module * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) - * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded - * to the caller from the EigenPod + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule */ - function requestWithdrawal(bytes32 moduleName, uint256[] calldata indices, uint64[] calldata gweiAmounts) - external - payable; + function requestWithdrawal( + bytes32 moduleName, + uint256[] calldata indices, + uint64[] calldata gweiAmounts, + WithdrawalType[] calldata withdrawalType, + bytes[][] calldata validatorAmountsSignatures + ) external payable; /** * @notice Batch settling of validator withdrawals diff --git a/mainnet-contracts/src/struct/StoppedValidatorInfo.sol b/mainnet-contracts/src/struct/StoppedValidatorInfo.sol index dd1091a1..6a3ece8b 100644 --- a/mainnet-contracts/src/struct/StoppedValidatorInfo.sol +++ b/mainnet-contracts/src/struct/StoppedValidatorInfo.sol @@ -19,4 +19,6 @@ struct StoppedValidatorInfo { uint256 pufferModuleIndex; /// @dev Amount of funds withdrawn upon validator stoppage. uint256 withdrawalAmount; + /// @dev Indicates whether the validator was downsized instead of exited + bool isDownsize; } diff --git a/mainnet-contracts/src/struct/WithdrawalType.sol b/mainnet-contracts/src/struct/WithdrawalType.sol new file mode 100644 index 00000000..1af0fcff --- /dev/null +++ b/mainnet-contracts/src/struct/WithdrawalType.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * @dev WithdrawalType + */ +enum WithdrawalType { + EXIT_VALIDATOR, + DOWNSIZE, + WITHDRAW_REWARDS +} From aeed0e2847ff50b319bf532b5410ae50fdb4bf32 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 6 Jun 2025 13:49:41 +0200 Subject: [PATCH 26/82] Adapted some comments and updated ValidatorKeyData --- mainnet-contracts/src/PufferProtocol.sol | 4 +++- .../src/interface/IPufferProtocol.sol | 17 ++++++++++++----- .../src/struct/ValidatorKeyData.sol | 3 --- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index ae677731..3904a478 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -426,7 +426,7 @@ contract PufferProtocol is numBatches: validator.numBatches }); - // However the burned part of the bond will be distributed between part of the bond returned and bond remaining (proportional to downsizing) + // However the burned part of the bond will be distributed between the bond returned and bond remaining (proportional to downsizing) // We burn the VT according to previous burn rate (before downsize) uint256 vtBurnAmount = @@ -436,11 +436,13 @@ contract PufferProtocol is burnAmounts.pufETH += burnAmount; burnAmounts.vt += vtBurnAmount; + // The bond to be returned is proportional to the num of batches we are downsizing (after burning) uint256 exitingBond = (validator.bond - burnAmount) * numDownsizeBatches / validator.numBatches; // We update the bondWithdrawals bondWithdrawals[i].pufETHAmount = exitingBond; bondWithdrawals[i].numBatches = numDownsizeBatches; + emit ValidatorDownsized({ pubKey: validator.pubKey, pufferModuleIndex: validatorInfos[i].pufferModuleIndex, diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index d6f68191..afc6fac5 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -281,6 +281,12 @@ interface IPufferProtocol { * @param withdrawalType The type of withdrawal * @param validatorAmountsSignatures The signatures of the guardians to validate the amount of the validators to withdraw * @dev The pubkeys should be active validators on the same module + * @dev There are 3 types of withdrawal: + * EXIT_VALIDATOR: The validator is fully exited. The gweiAmount needs to be 0 + * DOWNSIZE: The number of batches of the validator is reduced. The gweiAmount needs to be exactly a multiple of a batch size (32 ETH in gwei) + * And the validator should have more than the requested number of batches + * WITHDRAW_REWARDS: The amount cannot be higher than what the protocol provisioned for the validator and must be validated by the guardians via the `validatorAmountsSignatures` + * @dev The validatorAmountsSignatures is only needed when the withdrawal type is WITHDRAW_REWARDS * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule */ @@ -297,11 +303,12 @@ interface IPufferProtocol { * * @notice Settles a validator withdrawal * @dev This is one of the most important methods in the protocol - * It has multiple tasks: - * 1. Burn the pufETH from the node operator (if the withdrawal amount was lower than 32 ETH) - * 2. Burn the Validator Tickets from the node operator - * 3. Transfer withdrawal ETH from the PufferModule of the Validator to the PufferVault - * 4. Decrement the `lockedETHAmount` on the PufferOracle to reflect the new amount of locked ETH + * The withdrawals might be partial or total, and the validator might be downsized or fully exited + * It has multiple tasks: + * 1. Burn the pufETH from the node operator (if the withdrawal amount was lower than 32 ETH * numBatches or completely if the validator was slashed) + * 2. Burn the Validator Tickets from the node operator + * 3. Transfer withdrawal ETH from the PufferModule of the Validator to the PufferVault + * 4. Decrement the `lockedETHAmount` on the PufferOracle to reflect the new amount of locked ETH */ function batchHandleWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, diff --git a/mainnet-contracts/src/struct/ValidatorKeyData.sol b/mainnet-contracts/src/struct/ValidatorKeyData.sol index a62ebbdf..b7765484 100644 --- a/mainnet-contracts/src/struct/ValidatorKeyData.sol +++ b/mainnet-contracts/src/struct/ValidatorKeyData.sol @@ -8,8 +8,5 @@ struct ValidatorKeyData { bytes blsPubKey; bytes signature; bytes32 depositDataRoot; - bytes[] deprecated_blsEncryptedPrivKeyShares; - bytes deprecated_blsPubKeySet; - bytes deprecated_raveEvidence; uint8 numBatches; } From 717a38707fdfdfcb8ae011f0c17515526c201722 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 6 Jun 2025 14:41:02 +0200 Subject: [PATCH 27/82] Fixed oracle call --- mainnet-contracts/src/PufferProtocol.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 3904a478..5f154d3a 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -398,6 +398,7 @@ contract PufferProtocol is Withdrawals[] memory bondWithdrawals = new Withdrawals[](validatorInfos.length); uint256 numExitedBatches; + uint256 numExitedValidators; // We MUST NOT do the burning/oracle update/transferring ETH from the PufferModule -> PufferVault // because it affects pufETH exchange rate @@ -461,6 +462,7 @@ contract PufferProtocol is validator.numBatches -= numDownsizeBatches; } else { numExitedBatches += validator.numBatches; + numExitedValidators++; // Save the Node address for the bond transfer bondWithdrawals[i].node = validator.node; @@ -509,7 +511,7 @@ contract PufferProtocol is // Because we've calculated everything in the previous loop, we can do the burning PUFFER_VAULT.burn(burnAmounts.pufETH); // Deduct 32 ETH per batch from the `lockedETHAmount` on the PufferOracle - PUFFER_ORACLE.exitValidators(validatorInfos.length, numExitedBatches); + PUFFER_ORACLE.exitValidators(numExitedValidators, numExitedBatches); // In this loop, we transfer back the bonds, and do the accounting that affects the exchange rate for (uint256 i = 0; i < validatorInfos.length; ++i) { From d162236299c4f1795eba273f95a1292c5f6d3366 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 6 Jun 2025 17:36:02 +0200 Subject: [PATCH 28/82] Adapted tests and refactor batchHandleWithdrawals --- ...GenerateBLSKeysAndRegisterValidators.s.sol | 4 +- mainnet-contracts/src/PufferProtocol.sol | 187 ++++++++++-------- .../test/handlers/PufferProtocolHandler.sol | 4 +- .../test/mocks/MockPufferOracle.sol | 7 +- .../test/unit/PufferProtocol.t.sol | 70 ++++--- 5 files changed, 151 insertions(+), 121 deletions(-) diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index d110e974..21435994 100644 --- a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol +++ b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol @@ -102,9 +102,7 @@ contract GenerateBLSKeysAndRegisterValidators is Script { blsPubKey: stdJson.readBytes(registrationJson, ".bls_pub_key"), signature: stdJson.readBytes(registrationJson, ".signature"), depositDataRoot: stdJson.readBytes32(registrationJson, ".deposit_data_root"), - deprecated_blsEncryptedPrivKeyShares: new bytes[](3), - deprecated_blsPubKeySet: new bytes(48), - deprecated_raveEvidence: new bytes(0) + numBatches: 1 }); Permit memory pufETHPermit = _signPermit({ diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 5f154d3a..b915ce19 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -74,6 +74,8 @@ contract PufferProtocol is */ bytes32 internal constant _PUFFER_MODULE_0 = bytes32("PUFFER_MODULE_0"); + uint256 internal constant _BATCH_SIZE_GWEIS = 32 * 10 ** 9; + /** * @inheritdoc IPufferProtocol */ @@ -352,8 +354,6 @@ contract PufferProtocol is bytes[] memory pubkeys = new bytes[](indices.length); - uint256 batchSizeGweis = 32 ether / 1 gwei; - // validate pubkeys belong to that node and are active for (uint256 i = 0; i < indices.length; i++) { require($.validators[moduleName][indices[i]].node == msg.sender, InvalidValidator()); @@ -363,9 +363,9 @@ contract PufferProtocol is if (withdrawalType[i] == WithdrawalType.EXIT_VALIDATOR) { require(gweiAmount == 0, InvalidWithdrawAmount()); } else if (withdrawalType[i] == WithdrawalType.DOWNSIZE) { - uint256 batches = gweiAmount / batchSizeGweis; + uint256 batches = gweiAmount / _BATCH_SIZE_GWEIS; require( - batches > $.validators[moduleName][indices[i]].numBatches && batches * batchSizeGweis == gweiAmount, + batches > $.validators[moduleName][indices[i]].numBatches && gweiAmount % _BATCH_SIZE_GWEIS == 0, InvalidWithdrawAmount() ); } else if (withdrawalType[i] == WithdrawalType.WITHDRAW_REWARDS) { @@ -413,97 +413,24 @@ contract PufferProtocol is revert InvalidValidatorState(validator.status); } + // Save the Node address for the bond transfer + bondWithdrawals[i].node = validator.node; + if (validatorInfos[i].isDownsize) { uint8 numDownsizeBatches = uint8(validatorInfos[i].withdrawalAmount / 32 ether); numExitedBatches += numDownsizeBatches; - - // Save the Node address for the bond transfer - bondWithdrawals[i].node = validator.node; - - // We burn the bond according to previous burn rate (before downsize) - uint256 burnAmount = _getBondBurnAmount({ - validatorInfo: validatorInfos[i], - validatorBondAmount: validator.bond, - numBatches: validator.numBatches - }); - - // However the burned part of the bond will be distributed between the bond returned and bond remaining (proportional to downsizing) - - // We burn the VT according to previous burn rate (before downsize) - uint256 vtBurnAmount = - _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i], validator.numBatches); - - // We update the burnAmounts - burnAmounts.pufETH += burnAmount; - burnAmounts.vt += vtBurnAmount; - - // The bond to be returned is proportional to the num of batches we are downsizing (after burning) - uint256 exitingBond = (validator.bond - burnAmount) * numDownsizeBatches / validator.numBatches; - - // We update the bondWithdrawals - bondWithdrawals[i].pufETHAmount = exitingBond; bondWithdrawals[i].numBatches = numDownsizeBatches; - emit ValidatorDownsized({ - pubKey: validator.pubKey, - pufferModuleIndex: validatorInfos[i].pufferModuleIndex, - moduleName: validatorInfos[i].moduleName, - pufETHBurnAmount: burnAmount, - vtBurnAmount: vtBurnAmount, - epoch: validatorInfos[i].endEpoch, - numBatchesBefore: validator.numBatches, - numBatchesAfter: validator.numBatches - numDownsizeBatches - }); - - $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); - $.nodeOperatorInfo[validator.node].numBatches -= numDownsizeBatches; - - validator.bond -= uint96(exitingBond); - validator.numBatches -= numDownsizeBatches; + // We update the bondWithdrawals + bondWithdrawals[i].pufETHAmount = + _downsizeValidators($, validatorInfos, validator, numDownsizeBatches, i, burnAmounts); } else { numExitedBatches += validator.numBatches; numExitedValidators++; - - // Save the Node address for the bond transfer - bondWithdrawals[i].node = validator.node; - - uint96 bondAmount = validator.bond; - // Get the burnAmount for the withdrawal at the current exchange rate - uint256 burnAmount = _getBondBurnAmount({ - validatorInfo: validatorInfos[i], - validatorBondAmount: bondAmount, - numBatches: validator.numBatches - }); - uint256 vtBurnAmount = - _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i], validator.numBatches); - - // Update the burnAmounts - burnAmounts.pufETH += burnAmount; - burnAmounts.vt += vtBurnAmount; - - // Store the withdrawal amount for that node operator - // nosemgrep basic-arithmetic-underflow - bondWithdrawals[i].pufETHAmount = (bondAmount - burnAmount); bondWithdrawals[i].numBatches = validator.numBatches; - emit ValidatorExited({ - pubKey: validator.pubKey, - pufferModuleIndex: validatorInfos[i].pufferModuleIndex, - moduleName: validatorInfos[i].moduleName, - pufETHBurnAmount: burnAmount, - vtBurnAmount: vtBurnAmount - }); - // Decrease the number of registered validators for that module - _decreaseNumberOfRegisteredValidators($, validatorInfos[i].moduleName); - // Storage VT and the active validator count update for the Node Operator - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); - --$.nodeOperatorInfo[validator.node].activeValidatorCount; - $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; - - delete $.validators[validatorInfos[i].moduleName][ - validatorInfos[i].pufferModuleIndex - ]; + // We update the bondWithdrawals + bondWithdrawals[i].pufETHAmount = _exitValidator($, validatorInfos, validator, i, burnAmounts); } } @@ -970,5 +897,95 @@ contract PufferProtocol is emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); } + function _downsizeValidators( + ProtocolStorage storage $, + StoppedValidatorInfo[] calldata validatorInfos, + Validator storage validator, + uint8 numDownsizeBatches, + uint256 i, + BurnAmounts memory burnAmounts + ) internal returns (uint256 exitingBond) { + // We burn the bond according to previous burn rate (before downsize) + uint256 burnAmount = _getBondBurnAmount({ + validatorInfo: validatorInfos[i], + validatorBondAmount: validator.bond, + numBatches: validator.numBatches + }); + + // However the burned part of the bond will be distributed between the bond returned and bond remaining (proportional to downsizing) + + // We burn the VT according to previous burn rate (before downsize) + uint256 vtBurnAmount = _getVTBurnAmount($, validator.node, validatorInfos[i], validator.numBatches); + + // We update the burnAmounts + burnAmounts.pufETH += burnAmount; + burnAmounts.vt += vtBurnAmount; + + // The bond to be returned is proportional to the num of batches we are downsizing (after burning) + exitingBond = (validator.bond - burnAmount) * numDownsizeBatches / validator.numBatches; + + emit ValidatorDownsized({ + pubKey: validator.pubKey, + pufferModuleIndex: validatorInfos[i].pufferModuleIndex, + moduleName: validatorInfos[i].moduleName, + pufETHBurnAmount: burnAmount, + vtBurnAmount: vtBurnAmount, + epoch: validatorInfos[i].endEpoch, + numBatchesBefore: validator.numBatches, + numBatchesAfter: validator.numBatches - numDownsizeBatches + }); + + $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); + $.nodeOperatorInfo[validator.node].numBatches -= numDownsizeBatches; + + validator.bond -= uint96(exitingBond); + validator.numBatches -= numDownsizeBatches; + + return exitingBond; + } + + function _exitValidator( + ProtocolStorage storage $, + StoppedValidatorInfo[] calldata validatorInfos, + Validator storage validator, + uint256 i, + BurnAmounts memory burnAmounts + ) internal returns (uint256) { + uint96 bondAmount = validator.bond; + // Get the burnAmount for the withdrawal at the current exchange rate + uint256 burnAmount = _getBondBurnAmount({ + validatorInfo: validatorInfos[i], + validatorBondAmount: bondAmount, + numBatches: validator.numBatches + }); + uint256 vtBurnAmount = _getVTBurnAmount($, validator.node, validatorInfos[i], validator.numBatches); + + // Update the burnAmounts + burnAmounts.pufETH += burnAmount; + burnAmounts.vt += vtBurnAmount; + + emit ValidatorExited({ + pubKey: validator.pubKey, + pufferModuleIndex: validatorInfos[i].pufferModuleIndex, + moduleName: validatorInfos[i].moduleName, + pufETHBurnAmount: burnAmount, + vtBurnAmount: vtBurnAmount + }); + + // Decrease the number of registered validators for that module + _decreaseNumberOfRegisteredValidators($, validatorInfos[i].moduleName); + // Storage VT and the active validator count update for the Node Operator + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); + --$.nodeOperatorInfo[validator.node].activeValidatorCount; + $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; + + delete $.validators[validatorInfos[i].moduleName][ + validatorInfos[i].pufferModuleIndex + ]; + // nosemgrep basic-arithmetic-underflow + return bondAmount - burnAmount; + } + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } } diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index 99f53d9f..c948ac08 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -549,9 +549,7 @@ contract PufferProtocolHandler is Test { signature: mockValidatorSignature, withdrawalCredentials: withdrawalCredentials }), - deprecated_blsEncryptedPrivKeyShares: new bytes[](3), - deprecated_blsPubKeySet: new bytes(48), - deprecated_raveEvidence: new bytes(0) + numBatches: 1 }); return validatorData; diff --git a/mainnet-contracts/test/mocks/MockPufferOracle.sol b/mainnet-contracts/test/mocks/MockPufferOracle.sol index 7ccc8042..48447a05 100644 --- a/mainnet-contracts/test/mocks/MockPufferOracle.sol +++ b/mainnet-contracts/test/mocks/MockPufferOracle.sol @@ -29,8 +29,9 @@ contract MockPufferOracle is IPufferOracleV2 { return 99999; } - function provisionNode() external { } - function exitValidators(uint256) external { } + function provisionNode(uint256) external { } + + function exitValidators(uint256, uint256) external { } function getValidatorTicketPrice() external view returns (uint256 pricePerVT) { } @@ -39,4 +40,6 @@ contract MockPufferOracle is IPufferOracleV2 { function isOverBurstThreshold() external view returns (bool) { } function getNumberOfActiveValidators() external view returns (uint256) { } + + function getNumberOfActiveBatches() external view returns (uint256) { } } diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 40f723d7..ca1b4423 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -184,6 +184,7 @@ contract PufferProtocolTest is UnitTestHelper { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); pufferOracle.setTotalNumberOfValidators( + 5, 5, 99999999, _getGuardianEOASignatures( @@ -228,9 +229,7 @@ contract PufferProtocolTest is UnitTestHelper { blsPubKey: pubKey, // key length must be 48 byte signature: new bytes(0), depositDataRoot: bytes32(""), - deprecated_blsEncryptedPrivKeyShares: new bytes[](3), - deprecated_blsPubKeySet: new bytes(48), - deprecated_raveEvidence: new bytes(0) + numBatches: 1 }); vm.expectEmit(true, true, true, true); @@ -255,9 +254,7 @@ contract PufferProtocolTest is UnitTestHelper { blsPubKey: hex"aeaa", // invalid key signature: new bytes(0), depositDataRoot: bytes32(""), - deprecated_blsEncryptedPrivKeyShares: new bytes[](3), - deprecated_blsPubKeySet: new bytes(48), - deprecated_raveEvidence: new bytes(0) + numBatches: 1 }); vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); @@ -754,7 +751,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(16 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }); // Valid proof @@ -1061,7 +1059,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }) ); @@ -1086,7 +1085,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }) ); } @@ -1135,7 +1135,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }); StoppedValidatorInfo memory bobInfo = StoppedValidatorInfo({ @@ -1145,7 +1146,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }); StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](2); @@ -1201,7 +1203,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(35 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }); stopInfos[1] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, @@ -1210,7 +1213,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 31.9 ether, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }); stopInfos[2] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, @@ -1219,7 +1223,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 31 ether, startEpoch: 100, endEpoch: _getEpochNumber(34 days, 100), - wasSlashed: true + wasSlashed: true, + isDownsize: false }); stopInfos[3] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, @@ -1228,7 +1233,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 31.8 ether, startEpoch: 100, endEpoch: _getEpochNumber(48 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }); stopInfos[4] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, @@ -1237,7 +1243,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 31.5 ether, startEpoch: 100, endEpoch: _getEpochNumber(2 days, 100), - wasSlashed: true + wasSlashed: true, + isDownsize: false }); vm.expectEmit(true, true, true, true); @@ -1307,7 +1314,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }); StoppedValidatorInfo memory bobInfo = StoppedValidatorInfo({ @@ -1317,7 +1325,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }); vm.expectEmit(true, true, true, true); @@ -1403,7 +1412,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 29 ether, startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: true + wasSlashed: true, + isDownsize: false }); // Burns two bonds from Alice (she registered 2 validators, but only one got activated) @@ -1454,7 +1464,8 @@ contract PufferProtocolTest is UnitTestHelper { startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), withdrawalAmount: 29.5 ether, - wasSlashed: true + wasSlashed: true, + isDownsize: false }); // Burns one whole bond @@ -1504,7 +1515,8 @@ contract PufferProtocolTest is UnitTestHelper { startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), withdrawalAmount: 30 ether, - wasSlashed: true + wasSlashed: true, + isDownsize: false }); // Burns one whole bond @@ -1562,7 +1574,8 @@ contract PufferProtocolTest is UnitTestHelper { startEpoch: 100, endEpoch: _getEpochNumber(28 days, 100), withdrawalAmount: 31.9 ether, - wasSlashed: false + wasSlashed: false, + isDownsize: false }); // Burns one whole bond @@ -1612,7 +1625,8 @@ contract PufferProtocolTest is UnitTestHelper { startEpoch: 100, endEpoch: _getEpochNumber(15 days, 100), withdrawalAmount: 32.1 ether, - wasSlashed: false + wasSlashed: false, + isDownsize: false }); // Burns one whole bond @@ -1650,7 +1664,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(1 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }) ); @@ -1682,7 +1697,8 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, startEpoch: 100, endEpoch: _getEpochNumber(3 days, 100), - wasSlashed: false + wasSlashed: false, + isDownsize: false }) ); @@ -1845,9 +1861,7 @@ contract PufferProtocolTest is UnitTestHelper { signature: validatorSignature, withdrawalCredentials: withdrawalCredentials }), - deprecated_blsEncryptedPrivKeyShares: new bytes[](3), - deprecated_blsPubKeySet: new bytes(48), - deprecated_raveEvidence: new bytes(0) + numBatches: 1 }); return validatorData; From 5f3132e2cdb0d21c2edfd9c2c0426931527830d6 Mon Sep 17 00:00:00 2001 From: Eladio Date: Fri, 6 Jun 2025 17:39:20 +0200 Subject: [PATCH 29/82] Spell error --- mainnet-contracts/src/PufferModuleManager.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index 1ce4068c..87daa1b1 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -247,7 +247,7 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, * @notice Upgrades the given validators to consolidating (0x02) * @param moduleName The name of the module * @param pubkeys The pubkeys of the validators to upgrade - * @dev The funcion does not check that the pubkeys belong to the module + * @dev The function does not check that the pubkeys belong to the module * @dev Restricted to the DAO * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded From 8d365bdb8561c3dceecd6e3494578cbe3e9b6ad5 Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 9 Jun 2025 18:16:19 +0200 Subject: [PATCH 30/82] Set PufferProtocol as payable to fix provisionNode --- .../script/DeployPufferModuleImplementation.s.sol | 4 ++-- .../script/GenerateBLSKeysAndRegisterValidators.s.sol | 4 ++-- mainnet-contracts/src/PufferProtocol.sol | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol b/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol index ab1a0ce9..44297610 100644 --- a/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol +++ b/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol @@ -26,7 +26,7 @@ contract DeployPufferModuleImplementation is DeployerHelper { vm.startBroadcast(); PufferModule newImpl = new PufferModule({ - protocol: PufferProtocol(_getPufferProtocol()), + protocol: PufferProtocol(payable(_getPufferProtocol())), eigenPodManager: _getEigenPodManager(), delegationManager: IDelegationManager(_getDelegationManager()), moduleManager: PufferModuleManager(payable(_getPufferModuleManager())), @@ -51,7 +51,7 @@ contract DeployPufferModuleImplementation is DeployerHelper { vm.startPrank(_getPaymaster()); PufferModule newImpl = new PufferModule({ - protocol: PufferProtocol(_getPufferProtocol()), + protocol: PufferProtocol(payable(_getPufferProtocol())), eigenPodManager: _getEigenPodManager(), delegationManager: IDelegationManager(_getDelegationManager()), moduleManager: PufferModuleManager(payable(_getPufferModuleManager())), diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index 21435994..88483fa8 100644 --- a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol +++ b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol @@ -41,12 +41,12 @@ contract GenerateBLSKeysAndRegisterValidators is Script { if (block.chainid == 17000) { // Holesky protocolAddress = 0xE00c79408B9De5BaD2FDEbB1688997a68eC988CD; - pufferProtocol = PufferProtocol(protocolAddress); + pufferProtocol = PufferProtocol(payable(protocolAddress)); forkVersion = "0x01017000"; } else if (block.chainid == 1) { // Mainnet protocolAddress = 0xf7b6B32492c2e13799D921E84202450131bd238B; - pufferProtocol = PufferProtocol(protocolAddress); + pufferProtocol = PufferProtocol(payable(protocolAddress)); forkVersion = "0x00000000"; } diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index b915ce19..c86c98d3 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -21,7 +21,7 @@ import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IER import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; -import { InvalidAddress } from "./Errors.sol"; +import { InvalidAddress, Unauthorized } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; import { NoncesUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/NoncesUpgradeable.sol"; @@ -123,6 +123,10 @@ contract PufferProtocol is _disableInitializers(); } + receive() external payable { + require(msg.sender == address(PUFFER_VAULT), Unauthorized()); + } + /** * @notice Initializes the contract */ From d7ccb26bc5ce37ba7cdd31f5dd716995a28585a2 Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 9 Jun 2025 19:59:28 +0200 Subject: [PATCH 31/82] Removed placeholder value from Oracle --- mainnet-contracts/src/PufferOracleV2.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/mainnet-contracts/src/PufferOracleV2.sol b/mainnet-contracts/src/PufferOracleV2.sol index d745ff31..bee9e21d 100644 --- a/mainnet-contracts/src/PufferOracleV2.sol +++ b/mainnet-contracts/src/PufferOracleV2.sol @@ -64,7 +64,6 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { PUFFER_VAULT = vault; _totalNumberOfValidators = 927122; // Oracle will be updated with the correct value _epochNumber = 268828; // Oracle will be updated with the correct value - _numberOfActiveBatches = 927122; // Oracle will be updated with the correct value _setMintPrice(0.01 ether); } From 28790a04e36e5eb2ca0fc7c5717e4a5701e61f54 Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 9 Jun 2025 20:10:37 +0200 Subject: [PATCH 32/82] Fixed failing tests + writing new ones --- .../test/unit/PufferModuleManager.t.sol | 24 ++++++------- .../test/unit/PufferProtocol.t.sol | 34 ++++++++++++++++++- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/mainnet-contracts/test/unit/PufferModuleManager.t.sol b/mainnet-contracts/test/unit/PufferModuleManager.t.sol index 2037d606..e2e64023 100644 --- a/mainnet-contracts/test/unit/PufferModuleManager.t.sol +++ b/mainnet-contracts/test/unit/PufferModuleManager.t.sol @@ -352,7 +352,7 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); - // Verify we get the fee back + pufferModuleManager.requestWithdrawal{ value: EXIT_FEE }(MOCK_MODULE, pubkeys, gweiAmounts); vm.stopPrank(); } @@ -369,13 +369,13 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); - // Verify we get the fee back + pufferModuleManager.requestWithdrawal{ value: 2 * EXIT_FEE }(MOCK_MODULE, pubkeys, gweiAmounts); vm.stopPrank(); } function test_requestWithdrawalExcessFee() public { - _createPufferModule(MOCK_MODULE); + address moduleAddress = _createPufferModule(MOCK_MODULE); bytes[] memory pubkeys = new bytes[](1); pubkeys[0] = bytes("0x1234"); @@ -383,24 +383,24 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.startPrank(validatorExitor); - uint256 initialBalance = validatorExitor.balance; + uint256 initialBalance = moduleAddress.balance; vm.expectEmit(true, true, true, true); emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); pufferModuleManager.requestWithdrawal{ value: 1 ether }(MOCK_MODULE, pubkeys, gweiAmounts); - // Calculate expected balance: initial - gas costs - uint256 expectedBalance = initialBalance - EXIT_FEE; + // Calculate expected balance: initial + amount sent - fee + uint256 expectedBalance = initialBalance + 1 ether - EXIT_FEE; // Verify the balance change accounting for gas - assertEq(validatorExitor.balance, expectedBalance, "Should get the fee back minus gas costs"); + assertEq(moduleAddress.balance, expectedBalance, "Module should get the fee back minus gas costs"); vm.stopPrank(); } function test_requestWithdrawalExcessFee2() public { - _createPufferModule(MOCK_MODULE); + address moduleAddress = _createPufferModule(MOCK_MODULE); bytes[] memory pubkeys = new bytes[](2); pubkeys[0] = bytes("0x1234"); @@ -409,18 +409,18 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.startPrank(validatorExitor); - uint256 initialBalance = validatorExitor.balance; + uint256 initialBalance = moduleAddress.balance; vm.expectEmit(true, true, true, true); emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); pufferModuleManager.requestWithdrawal{ value: 1 ether }(MOCK_MODULE, pubkeys, gweiAmounts); - // Calculate expected balance: initial - gas costs - uint256 expectedBalance = initialBalance - 2 * EXIT_FEE; + // Calculate expected balance: initial + amount sent - fee + uint256 expectedBalance = initialBalance + 1 ether - 2 * EXIT_FEE; // Verify the balance change accounting for gas - assertEq(validatorExitor.balance, expectedBalance, "Should get the fee back minus gas costs"); + assertEq(moduleAddress.balance, expectedBalance, "Module should get the fee back minus gas costs"); vm.stopPrank(); } diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index ca1b4423..4ffa7c63 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -11,7 +11,6 @@ import { Validator } from "../../src/struct/Validator.sol"; import { PufferProtocol } from "../../src/PufferProtocol.sol"; import { PufferModule } from "../../src/PufferModule.sol"; import { ROLE_ID_DAO, ROLE_ID_OPERATIONS_PAYMASTER, ROLE_ID_OPERATIONS_MULTISIG } from "../../script/Roles.sol"; -import { Unauthorized } from "../../src/Errors.sol"; import { LibGuardianMessages } from "../../src/LibGuardianMessages.sol"; import { Permit } from "../../src/structs/Permit.sol"; import { ModuleLimit } from "../../src/struct/ProtocolStorage.sol"; @@ -239,6 +238,39 @@ contract PufferProtocolTest is UnitTestHelper { ); } + // Try registering with an invalid number of batches + function test_register_invalid_num_batches() public { + uint256 vtPrice = pufferOracle.getValidatorTicketPrice() * 30; + + bytes memory pubKey = _getPubKey(bytes32("something")); + + bytes[] memory newSetOfPubKeys = new bytes[](3); + + // we have 3 guardians in TestHelper.sol + newSetOfPubKeys[0] = bytes("key1"); + newSetOfPubKeys[0] = bytes("key2"); + newSetOfPubKeys[0] = bytes("key3"); + + ValidatorKeyData memory validatorData = ValidatorKeyData({ + blsPubKey: pubKey, // key length must be 48 byte + signature: new bytes(0), + depositDataRoot: bytes32(""), + numBatches: 0 + }); + + vm.expectRevert(IPufferProtocol.InvalidNumberOfBatches.selector); + pufferProtocol.registerValidatorKey{ value: vtPrice }( + validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit + ); + + validatorData.numBatches = 65; + + vm.expectRevert(IPufferProtocol.InvalidNumberOfBatches.selector); + pufferProtocol.registerValidatorKey{ value: vtPrice }( + validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit + ); + } + // Try registering with invalid BLS key length function test_register_invalid_bls_key() public { uint256 smoothingCommitment = pufferOracle.getValidatorTicketPrice(); From a76adecf9d9e948a5b6442c99f98e7bb2025491f Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:11:21 +0000 Subject: [PATCH 33/82] forge fmt --- mainnet-contracts/test/unit/PufferProtocol.t.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 4ffa7c63..b3dd68db 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -259,16 +259,12 @@ contract PufferProtocolTest is UnitTestHelper { }); vm.expectRevert(IPufferProtocol.InvalidNumberOfBatches.selector); - pufferProtocol.registerValidatorKey{ value: vtPrice }( - validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit - ); + pufferProtocol.registerValidatorKey{ value: vtPrice }(validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit); validatorData.numBatches = 65; vm.expectRevert(IPufferProtocol.InvalidNumberOfBatches.selector); - pufferProtocol.registerValidatorKey{ value: vtPrice }( - validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit - ); + pufferProtocol.registerValidatorKey{ value: vtPrice }(validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit); } // Try registering with invalid BLS key length From 7883612b6c2ee115c344a2d2ef0a144e46649a0d Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 10 Jun 2025 18:13:08 +0200 Subject: [PATCH 34/82] Added signature check to downsize, and changed burn. Restored old oracle --- mainnet-contracts/src/PufferOracleV2.sol | 31 +++-------------- mainnet-contracts/src/PufferProtocol.sol | 34 ++++++++++++------- .../src/interface/IPufferOracleV2.sol | 20 ++--------- .../src/interface/IPufferProtocol.sol | 2 +- .../test/mocks/MockPufferOracle.sol | 6 ++-- .../test/unit/PufferProtocol.t.sol | 1 - 6 files changed, 31 insertions(+), 63 deletions(-) diff --git a/mainnet-contracts/src/PufferOracleV2.sol b/mainnet-contracts/src/PufferOracleV2.sol index bee9e21d..a68e32b1 100644 --- a/mainnet-contracts/src/PufferOracleV2.sol +++ b/mainnet-contracts/src/PufferOracleV2.sol @@ -51,12 +51,6 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { */ uint256 internal _validatorTicketPrice; - /** - * @dev Number of active batches - * Slot 4 - */ - uint256 internal _numberOfActiveBatches; - constructor(IGuardianModule guardianModule, address payable vault, address accessManager) AccessManaged(accessManager) { @@ -71,13 +65,10 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { * @notice Exits the validator from the Beacon chain * @dev Restricted to PufferProtocol contract */ - function exitValidators(uint256 numberOfExits, uint256 numberOfBatchesExited) public restricted { + function exitValidators(uint256 numberOfExits) public restricted { // nosemgrep basic-arithmetic-underflow _numberOfActivePufferValidators -= numberOfExits; - // nosemgrep basic-arithmetic-underflow - _numberOfActiveBatches -= numberOfBatchesExited; emit NumberOfActiveValidators(_numberOfActivePufferValidators); - emit NumberOfActiveBatches(_numberOfActiveBatches); } /** @@ -86,13 +77,11 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { * The PufferVault balance is decreased by the same amount * @dev Restricted to PufferProtocol contract */ - function provisionNode(uint256 numberOfBatches) external restricted { + function provisionNode() external restricted { unchecked { ++_numberOfActivePufferValidators; - _numberOfActiveBatches += numberOfBatches; } emit NumberOfActiveValidators(_numberOfActivePufferValidators); - emit NumberOfActiveBatches(_numberOfActiveBatches); } /** @@ -107,13 +96,9 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { /** * @notice Updates the total number of validators * @param newTotalNumberOfValidators The new number of validators - * @param newNumActiveBatches The new number of active batches - * @param epochNumber The epoch number of the update - * @param guardianEOASignatures The guardian EOA signatures */ function setTotalNumberOfValidators( uint256 newTotalNumberOfValidators, - uint256 newNumActiveBatches, uint256 epochNumber, bytes[] calldata guardianEOASignatures ) external restricted { @@ -123,7 +108,6 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { GUARDIAN_MODULE.validateTotalNumberOfValidators(newTotalNumberOfValidators, epochNumber, guardianEOASignatures); emit TotalNumberOfValidatorsUpdated(_totalNumberOfValidators, newTotalNumberOfValidators, epochNumber); _totalNumberOfValidators = newTotalNumberOfValidators; - _numberOfActiveBatches = newNumActiveBatches; _epochNumber = epochNumber; } @@ -131,7 +115,7 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { * @inheritdoc IPufferOracle */ function getLockedEthAmount() external view returns (uint256) { - return _numberOfActiveBatches * 32 ether; + return _numberOfActivePufferValidators * 32 ether; } /** @@ -141,18 +125,11 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { return _totalNumberOfValidators; } - /** - * @inheritdoc IPufferOracleV2 - */ - function getNumberOfActiveBatches() external view returns (uint256) { - return _numberOfActiveBatches; - } - /** * @inheritdoc IPufferOracle */ function isOverBurstThreshold() external view returns (bool) { - return (((_numberOfActivePufferValidators * 100) / _totalNumberOfValidators) > _BURST_THRESHOLD); + return ((_numberOfActivePufferValidators * 100 / _totalNumberOfValidators) > _BURST_THRESHOLD); } /** diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index c86c98d3..e7e36ab7 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -366,13 +366,17 @@ contract PufferProtocol is if (withdrawalType[i] == WithdrawalType.EXIT_VALIDATOR) { require(gweiAmount == 0, InvalidWithdrawAmount()); - } else if (withdrawalType[i] == WithdrawalType.DOWNSIZE) { - uint256 batches = gweiAmount / _BATCH_SIZE_GWEIS; - require( - batches > $.validators[moduleName][indices[i]].numBatches && gweiAmount % _BATCH_SIZE_GWEIS == 0, - InvalidWithdrawAmount() - ); - } else if (withdrawalType[i] == WithdrawalType.WITHDRAW_REWARDS) { + } else { + + if (withdrawalType[i] == WithdrawalType.DOWNSIZE) { + uint256 batches = gweiAmount / _BATCH_SIZE_GWEIS; + require( + batches > $.validators[moduleName][indices[i]].numBatches && gweiAmount % _BATCH_SIZE_GWEIS == 0, + InvalidWithdrawAmount() + ); + } + + // If downsize or rewards withdrawal, backend needs to validate the amount bytes32 messageHash = keccak256(abi.encode(msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(msg.sender))); @@ -402,7 +406,6 @@ contract PufferProtocol is Withdrawals[] memory bondWithdrawals = new Withdrawals[](validatorInfos.length); uint256 numExitedBatches; - uint256 numExitedValidators; // We MUST NOT do the burning/oracle update/transferring ETH from the PufferModule -> PufferVault // because it affects pufETH exchange rate @@ -430,7 +433,6 @@ contract PufferProtocol is _downsizeValidators($, validatorInfos, validator, numDownsizeBatches, i, burnAmounts); } else { numExitedBatches += validator.numBatches; - numExitedValidators++; bondWithdrawals[i].numBatches = validator.numBatches; // We update the bondWithdrawals @@ -441,8 +443,9 @@ contract PufferProtocol is VALIDATOR_TICKET.burn(burnAmounts.vt); // Because we've calculated everything in the previous loop, we can do the burning PUFFER_VAULT.burn(burnAmounts.pufETH); + // Deduct 32 ETH per batch from the `lockedETHAmount` on the PufferOracle - PUFFER_ORACLE.exitValidators(numExitedValidators, numExitedBatches); + PUFFER_ORACLE.exitValidators(numExitedBatches); // In this loop, we transfer back the bonds, and do the accounting that affects the exchange rate for (uint256 i = 0; i < validatorInfos.length; ++i) { @@ -848,7 +851,9 @@ contract PufferProtocol is emit SuccessfullyProvisioned(validatorPubKey, index, moduleName); // Increase lockedETH on Puffer Oracle - PUFFER_ORACLE.provisionNode(numBatches); + for (uint256 i = 0; i < numBatches; i++) { + PUFFER_ORACLE.provisionNode(); + } BEACON_DEPOSIT_CONTRACT.deposit{ value: numBatches * 32 ether }( validatorPubKey, module.getWithdrawalCredentials(), validatorSignature, depositDataRoot @@ -925,8 +930,11 @@ contract PufferProtocol is burnAmounts.pufETH += burnAmount; burnAmounts.vt += vtBurnAmount; - // The bond to be returned is proportional to the num of batches we are downsizing (after burning) - exitingBond = (validator.bond - burnAmount) * numDownsizeBatches / validator.numBatches; + // The bond to be returned is proportional to the num of batches we are downsizing minus the burned amount + exitingBond = validator.bond * numDownsizeBatches / validator.numBatches; + + require(exitingBond >= burnAmount, InvalidWithdrawAmount()); + exitingBond -= burnAmount; emit ValidatorDownsized({ pubKey: validator.pubKey, diff --git a/mainnet-contracts/src/interface/IPufferOracleV2.sol b/mainnet-contracts/src/interface/IPufferOracleV2.sol index b86b3835..0923f87a 100644 --- a/mainnet-contracts/src/interface/IPufferOracleV2.sol +++ b/mainnet-contracts/src/interface/IPufferOracleV2.sol @@ -17,12 +17,6 @@ interface IPufferOracleV2 is IPufferOracle { event NumberOfActiveValidators(uint256 numberOfActivePufferValidators); - /** - * @notice Emitted when the number of active batches is updated - * @param numberOfActiveBatches is the number of active batches - */ - event NumberOfActiveBatches(uint256 numberOfActiveBatches); - /** * @notice Emitted when the total number of validators is updated * @param oldNumberOfValidators is the old number of validators @@ -42,28 +36,20 @@ interface IPufferOracleV2 is IPufferOracle { */ function getNumberOfActiveValidators() external view returns (uint256); - /** - * @notice Returns the number of active batches of 32 ETH staked by the validators on Ethereum - */ - function getNumberOfActiveBatches() external view returns (uint256); - /** * @notice Exits `validatorNumber` validators, decreasing the `lockedETHAmount` by validatorNumber * 32 ETH. * It is called when when the validator exits the system in the `batchHandleWithdrawals` on the PufferProtocol. * In the same transaction, we are transferring full withdrawal ETH from the PufferModule to the Vault - * Decrementing the `lockedETHAmount` by 32 ETH per batch and we burn the Node Operator's pufETH (bond) if we need to cover up the loss. - * @param validatorNumber is the number of validators to exit - * @param numberOfBatchesExited is the number of batches exited + * Decrementing the `lockedETHAmount` by 32 ETH and we burn the Node Operator's pufETH (bond) if we need to cover up the loss. * @dev Restricted to PufferProtocol contract */ - function exitValidators(uint256 validatorNumber, uint256 numberOfBatchesExited) external; + function exitValidators(uint256 validatorNumber) external; /** * @notice Increases the `lockedETHAmount` on the PufferOracle by 32 ETH to account for a new deposit. * It is called when the Beacon chain receives a new deposit from the PufferProtocol. * The PufferVault's balance will simultaneously decrease by 32 ETH as the deposit is made. - * @param numberOfBatches is the number of batches to provision * @dev Restricted to PufferProtocol contract */ - function provisionNode(uint256 numberOfBatches) external; + function provisionNode() external; } diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index afc6fac5..4517b4e7 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -286,7 +286,7 @@ interface IPufferProtocol { * DOWNSIZE: The number of batches of the validator is reduced. The gweiAmount needs to be exactly a multiple of a batch size (32 ETH in gwei) * And the validator should have more than the requested number of batches * WITHDRAW_REWARDS: The amount cannot be higher than what the protocol provisioned for the validator and must be validated by the guardians via the `validatorAmountsSignatures` - * @dev The validatorAmountsSignatures is only needed when the withdrawal type is WITHDRAW_REWARDS + * @dev The validatorAmountsSignatures is only needed when the withdrawal type is DOWNSIZE orWITHDRAW_REWARDS * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule */ diff --git a/mainnet-contracts/test/mocks/MockPufferOracle.sol b/mainnet-contracts/test/mocks/MockPufferOracle.sol index 48447a05..4d4fb393 100644 --- a/mainnet-contracts/test/mocks/MockPufferOracle.sol +++ b/mainnet-contracts/test/mocks/MockPufferOracle.sol @@ -29,9 +29,9 @@ contract MockPufferOracle is IPufferOracleV2 { return 99999; } - function provisionNode(uint256) external { } + function provisionNode() external { } - function exitValidators(uint256, uint256) external { } + function exitValidators(uint256) external { } function getValidatorTicketPrice() external view returns (uint256 pricePerVT) { } @@ -40,6 +40,4 @@ contract MockPufferOracle is IPufferOracleV2 { function isOverBurstThreshold() external view returns (bool) { } function getNumberOfActiveValidators() external view returns (uint256) { } - - function getNumberOfActiveBatches() external view returns (uint256) { } } diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index b3dd68db..6f0007b7 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -183,7 +183,6 @@ contract PufferProtocolTest is UnitTestHelper { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); pufferOracle.setTotalNumberOfValidators( - 5, 5, 99999999, _getGuardianEOASignatures( From 141628f76a360047398be779500ea10c16478a35 Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:14:59 +0000 Subject: [PATCH 35/82] forge fmt --- mainnet-contracts/src/PufferProtocol.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index e7e36ab7..911c1052 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -367,7 +367,6 @@ contract PufferProtocol is if (withdrawalType[i] == WithdrawalType.EXIT_VALIDATOR) { require(gweiAmount == 0, InvalidWithdrawAmount()); } else { - if (withdrawalType[i] == WithdrawalType.DOWNSIZE) { uint256 batches = gweiAmount / _BATCH_SIZE_GWEIS; require( From 40b352cd868bba2dd5540791c00076058ebe31bf Mon Sep 17 00:00:00 2001 From: Eladio Date: Wed, 11 Jun 2025 12:23:50 +0200 Subject: [PATCH 36/82] Added numBatches to ValidatorKeyRegistered event --- mainnet-contracts/src/PufferProtocol.sol | 2 +- mainnet-contracts/src/interface/IPufferProtocol.sol | 3 ++- mainnet-contracts/test/handlers/PufferProtocolHandler.sol | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 911c1052..cf1994c3 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -740,7 +740,7 @@ contract PufferProtocol is ++$.moduleLimits[moduleName].numberOfRegisteredValidators; } emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); - emit ValidatorKeyRegistered(data.blsPubKey, pufferModuleIndex, moduleName); + emit ValidatorKeyRegistered(data.blsPubKey, pufferModuleIndex, moduleName, data.numBatches); } function _setValidatorLimitPerModule(bytes32 moduleName, uint128 limit) internal { diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 4517b4e7..5ea39c15 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -164,9 +164,10 @@ interface IPufferProtocol { * @param pubKey is the validator public key * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain * @param moduleName is the staking Module + * @param numBatches is the number of batches the validator has * @dev Signature "0x6b9febc68231d6c196b22b02f442fa6dc3148ee90b6e83d5b978c11833587159" */ - event ValidatorKeyRegistered(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName); + event ValidatorKeyRegistered(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint8 numBatches); /** * @notice Emitted when the Validator exited and stopped validating diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index c948ac08..0318bf99 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -582,7 +582,7 @@ contract PufferProtocolHandler is Test { uint256 bond = 1 ether; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName); + emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName,1); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( validatorKeyData, moduleName, emptyPermit, emptyPermit ); From c6cbc18d2f80e07af75cc45e6e79f7c5282e533a Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:24:42 +0000 Subject: [PATCH 37/82] forge fmt --- mainnet-contracts/src/interface/IPufferProtocol.sol | 4 +++- mainnet-contracts/test/handlers/PufferProtocolHandler.sol | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 5ea39c15..6142a0e4 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -167,7 +167,9 @@ interface IPufferProtocol { * @param numBatches is the number of batches the validator has * @dev Signature "0x6b9febc68231d6c196b22b02f442fa6dc3148ee90b6e83d5b978c11833587159" */ - event ValidatorKeyRegistered(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint8 numBatches); + event ValidatorKeyRegistered( + bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint8 numBatches + ); /** * @notice Emitted when the Validator exited and stopped validating diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index 0318bf99..be21ad52 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -582,7 +582,7 @@ contract PufferProtocolHandler is Test { uint256 bond = 1 ether; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName,1); + emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( validatorKeyData, moduleName, emptyPermit, emptyPermit ); From efb94401932c20fb9e45efac15f639a3a6367364 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Thu, 12 Jun 2025 14:06:03 +0200 Subject: [PATCH 38/82] fix validator exits --- mainnet-contracts/src/PufferProtocol.sol | 172 ++++++++---------- .../src/interface/IPufferProtocol.sol | 19 +- .../test/unit/PufferProtocol.t.sol | 79 ++++---- 3 files changed, 120 insertions(+), 150 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 2cf32df1..43b6eab4 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -21,7 +21,7 @@ import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IER import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; -import { InvalidAddress, Unauthorized } from "./Errors.sol"; +import { InvalidAddress } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; import { NoncesUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/NoncesUpgradeable.sol"; @@ -508,14 +508,9 @@ contract PufferProtocol is BurnAmounts memory burnAmounts; Withdrawals[] memory bondWithdrawals = new Withdrawals[](validatorInfos.length); + // 1 batch = 32 ETH uint256 numExitedBatches; - //@todo triggers when validator exits fully or it is downsized - - // We MUST NOT do the burning/oracle update/transferring ETH from the PufferModule -> PufferVault - // because it affects pufETH exchange rate - - // First, we do the calculations // slither-disable-start calls-loop for (uint256 i = 0; i < validatorInfos.length; ++i) { Validator storage validator = @@ -527,20 +522,9 @@ contract PufferProtocol is // Save the Node address for the bond transfer bondWithdrawals[i].node = validator.node; + uint256 bondBurnAmount; - // Not all validators have the number of batches populated, for the validators that are registered a while ago, we assume 1 batch - bondWithdrawals[i].numBatches = validator.numBatches > 0 ? validator.numBatches : 1; - // Do the accounting for the total number of batches exited - numExitedBatches += bondWithdrawals[i].numBatches; - - uint96 bondAmount = validator.bond; - // Get the burnAmount for the withdrawal at the current exchange rate - uint256 burnAmount = _getBondBurnAmount({ - validatorInfo: validatorInfos[i], - validatorBondAmount: bondAmount, - numBatches: validator.numBatches - }); - uint256 vtBurnAmount = _getVTBurnAmount($, validator.node, validatorInfos[i]); + uint256 vtBurnAmount = _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i]); // We need to scope the variables to avoid stack too deep errors { @@ -550,37 +534,33 @@ contract PufferProtocol is _useVTOrValidationTime($, validator, vtBurnAmount, epochValidated, vtConsumptionSignature); } - // Update the burnAmounts - burnAmounts.pufETH += burnAmount; + if (validatorInfos[i].isDownsize) { + // We update the bondWithdrawals + (bondWithdrawals[i].pufETHAmount, bondWithdrawals[i].numBatches) = + _downsizeValidators($, validatorInfos[i], validator); - // Store the withdrawal amount for that node operator - // nosemgrep basic-arithmetic-underflow - bondWithdrawals[i].pufETHAmount = (bondAmount - burnAmount); - - emit ValidatorExited({ - pubKey: validator.pubKey, - pufferModuleIndex: validatorInfos[i].pufferModuleIndex, - moduleName: validatorInfos[i].moduleName, - pufETHBurnAmount: burnAmount, - vtBurnAmount: vtBurnAmount - }); + numExitedBatches += bondWithdrawals[i].numBatches; + } else { + // Full validator exit + numExitedBatches += validator.numBatches; + bondWithdrawals[i].numBatches = validator.numBatches > 0 ? validator.numBatches : 1; - // Decrease the number of registered validators for that module - _decreaseNumberOfRegisteredValidators($, validatorInfos[i].moduleName); - // Storage VT and the active validator count update for the Node Operator - // nosemgrep basic-arithmetic-underflow - --$.nodeOperatorInfo[validator.node].activeValidatorCount; + // We update the bondWithdrawals + (bondBurnAmount, bondWithdrawals[i].pufETHAmount, bondWithdrawals[i].numBatches) = + _exitValidator($, validatorInfos[i], validator); + } - delete validator.node; - delete validator.bond; - delete validator.module; - delete validator.status; - delete validator.pubKey; + // Update the burnAmounts + burnAmounts.pufETH += bondBurnAmount; } - VALIDATOR_TICKET.burn(burnAmounts.vt); - // Because we've calculated everything in the previous loop, we can do the burning - PUFFER_VAULT.burn(burnAmounts.pufETH); + if (burnAmounts.vt > 0) { + VALIDATOR_TICKET.burn(burnAmounts.vt); + } + if (burnAmounts.pufETH > 0) { + // Because we've calculated everything in the previous loop, we can do the burning + PUFFER_VAULT.burn(burnAmounts.pufETH); + } // Deduct 32 ETH per batch from the `lockedETHAmount` on the PufferOracle PUFFER_ORACLE.exitValidators(numExitedBatches); @@ -999,6 +979,12 @@ contract PufferProtocol is // nosemgrep basic-arithmetic-underflow $.nodeOperatorInfo[validator.node].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); + emit ValidationTimeConsumed({ + node: validator.node, + consumedAmount: 0, + deprecated_burntVTs: vtBurnAmount + }); + return burnedAmount; } @@ -1064,6 +1050,12 @@ contract PufferProtocol is $.nodeOperatorInfo[node].totalEpochsValidated = totalEpochsValidated; $.nodeOperatorInfo[node].validationTime -= amountToConsume; + emit ValidationTimeConsumed({ + node: node, + consumedAmount: amountToConsume, + deprecated_burntVTs: deprecated_burntVTs + }); + address weth = PUFFER_VAULT.asset(); // WETH is a contract that has a fallback function that accepts ETH, and never reverts @@ -1086,8 +1078,6 @@ contract PufferProtocol is uint256 minimumVTAmount = $.minimumVtAmount; uint256 nodeVTBalance = $.nodeOperatorInfo[node].deprecated_vtBalance; - //@todo might be buggy - // If the VT burn amount is less than the minimum VT amount that means that the node operator exited early // If we don't penalize it, the node operator can exit early and re-register with the same VTs. // By doing that, they can lower the APY for the pufETH holders @@ -1122,95 +1112,87 @@ contract PufferProtocol is function _downsizeValidators( ProtocolStorage storage $, - StoppedValidatorInfo[] calldata validatorInfos, - Validator storage validator, - uint256 numDownsizeBatches, - uint256 i, - BurnAmounts memory burnAmounts - ) internal returns (uint256 exitingBond) { + StoppedValidatorInfo calldata validatorInfo, + Validator storage validator + ) internal returns (uint256 exitingBond, uint256 exitedBatches) { + exitedBatches = validatorInfo.withdrawalAmount / 32 ether; + + uint256 numBatchesBefore = validator.numBatches; + // We burn the bond according to previous burn rate (before downsize) uint256 burnAmount = _getBondBurnAmount({ - validatorInfo: validatorInfos[i], + validatorInfo: validatorInfo, validatorBondAmount: validator.bond, - numBatches: validator.numBatches + numBatches: numBatchesBefore }); // However the burned part of the bond will be distributed between the bond returned and bond remaining (proportional to downsizing) - // We burn the VT according to previous burn rate (before downsize) - uint256 vtBurnAmount = _getVTBurnAmount($, validator.node, validatorInfos[i]); - - // We update the burnAmounts - burnAmounts.pufETH += burnAmount; - burnAmounts.vt += vtBurnAmount; - // The bond to be returned is proportional to the num of batches we are downsizing minus the burned amount - exitingBond = validator.bond * numDownsizeBatches / validator.numBatches; + exitingBond = validator.bond * exitedBatches / validator.numBatches; require(exitingBond >= burnAmount, InvalidWithdrawAmount()); exitingBond -= burnAmount; emit ValidatorDownsized({ pubKey: validator.pubKey, - pufferModuleIndex: validatorInfos[i].pufferModuleIndex, - moduleName: validatorInfos[i].moduleName, + pufferModuleIndex: validatorInfo.pufferModuleIndex, + moduleName: validatorInfo.moduleName, pufETHBurnAmount: burnAmount, - vtBurnAmount: vtBurnAmount, - epoch: validatorInfos[i].totalEpochsValidated, - numBatchesBefore: validator.numBatches, - numBatchesAfter: validator.numBatches - numDownsizeBatches + epoch: validatorInfo.totalEpochsValidated, + numBatchesBefore: numBatchesBefore, + numBatchesAfter: validator.numBatches - exitedBatches }); - $.nodeOperatorInfo[validator.node].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); - $.nodeOperatorInfo[validator.node].numBatches -= SafeCast.toUint8(numDownsizeBatches); + $.nodeOperatorInfo[validator.node].numBatches -= SafeCast.toUint8(exitedBatches); - validator.bond -= uint96(exitingBond); - validator.numBatches -= SafeCast.toUint8(numDownsizeBatches); + validator.bond -= SafeCast.toUint96(exitingBond); + validator.numBatches -= SafeCast.toUint8(exitedBatches); - return exitingBond; + return (exitingBond, exitedBatches); } function _exitValidator( ProtocolStorage storage $, - StoppedValidatorInfo[] calldata validatorInfos, - Validator storage validator, - uint256 i, - BurnAmounts memory burnAmounts - ) internal returns (uint256) { + StoppedValidatorInfo calldata validatorInfo, + Validator storage validator + ) internal returns (uint256 bondBurnAmount, uint256 bondReturnAmount, uint256 exitedBatches) { uint96 bondAmount = validator.bond; + uint256 numBatches = validator.numBatches; + // Get the burnAmount for the withdrawal at the current exchange rate uint256 burnAmount = _getBondBurnAmount({ - validatorInfo: validatorInfos[i], + validatorInfo: validatorInfo, validatorBondAmount: bondAmount, numBatches: validator.numBatches }); - uint256 vtBurnAmount = _getVTBurnAmount($, validator.node, validatorInfos[i]); - - // Update the burnAmounts - burnAmounts.pufETH += burnAmount; - burnAmounts.vt += vtBurnAmount; emit ValidatorExited({ pubKey: validator.pubKey, - pufferModuleIndex: validatorInfos[i].pufferModuleIndex, - moduleName: validatorInfos[i].moduleName, - pufETHBurnAmount: burnAmount, - vtBurnAmount: vtBurnAmount + pufferModuleIndex: validatorInfo.pufferModuleIndex, + moduleName: validatorInfo.moduleName, + pufETHBurnAmount: burnAmount + }); + + bondBurnAmount = _getBondBurnAmount({ + validatorInfo: validatorInfo, + validatorBondAmount: bondAmount, + numBatches: validator.numBatches }); // Decrease the number of registered validators for that module - _decreaseNumberOfRegisteredValidators($, validatorInfos[i].moduleName); + _decreaseNumberOfRegisteredValidators($, validatorInfo.moduleName); + // Storage VT and the active validator count update for the Node Operator // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[validator.node].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); --$.nodeOperatorInfo[validator.node].activeValidatorCount; $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; - delete $.validators[validatorInfos[i].moduleName][ - validatorInfos[i].pufferModuleIndex + delete $.validators[validatorInfo.moduleName][ + validatorInfo.pufferModuleIndex ]; // nosemgrep basic-arithmetic-underflow - return bondAmount - burnAmount; + return (burnAmount, bondAmount - burnAmount, numBatches); } function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index ffb857a5..ada6318e 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -192,11 +192,7 @@ interface IPufferProtocol { * @dev Signature "0xf435da9e3aeccc40d39fece7829f9941965ceee00d31fa7a89d608a273ea906e" */ event ValidatorExited( - bytes pubKey, - uint256 indexed pufferModuleIndex, - bytes32 indexed moduleName, - uint256 pufETHBurnAmount, - uint256 vtBurnAmount + bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint256 pufETHBurnAmount ); /** @@ -205,23 +201,30 @@ interface IPufferProtocol { * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain * @param moduleName is the staking Module * @param pufETHBurnAmount The amount of pufETH burned from the Node Operator - * @param vtBurnAmount The amount of Validator Tickets burned from the Node Operator * @param epoch The epoch of the downsize * @param numBatchesBefore The number of batches before the downsize * @param numBatchesAfter The number of batches after the downsize - * @dev Signature "0x708d62f89df6fdb944118762f267baa489a8512915584a6b271365c6baec6df4" + * @dev Signature "0x75afd977bd493b29a8e699e6b7a9ab85df6b62f4ba5664e370bd5cb0b0e2b776" */ event ValidatorDownsized( bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint256 pufETHBurnAmount, - uint256 vtBurnAmount, uint256 epoch, uint256 numBatchesBefore, uint256 numBatchesAfter ); + /** + * @notice Emitted when validation time is consumed + * @param node is the node operator address + * @param consumedAmount is the amount of validation time that was consumed + * @param deprecated_burntVTs is the amount of VT that was burnt + * @dev Signature "0x4b16b7334c6437660b5530a3a5893e7a10fa5424e5c0d67806687147553544ef" + */ + event ValidationTimeConsumed(address indexed node, uint256 consumedAmount, uint256 deprecated_burntVTs); + /** * @notice Emitted when a consolidation is requested * @param moduleName is the module name diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 0a07f774..8a7a3691 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -932,9 +932,11 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH + emit IPufferProtocol.ValidationTimeConsumed( + alice, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); + vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0); _executeFullWithdrawal( StoppedValidatorInfo({ module: NoRestakingModule, @@ -1051,12 +1053,18 @@ contract PufferProtocolTest is UnitTestHelper { stopInfos[1] = bobInfo; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, epochsValidated * BURN_RATE_PER_EPOCH + emit IPufferProtocol.ValidationTimeConsumed( + alice, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, epochsValidated * BURN_RATE_PER_EPOCH + vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0); + vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidationTimeConsumed( + bob, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); + vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0); + pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); @@ -1141,44 +1149,30 @@ contract PufferProtocolTest is UnitTestHelper { }); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 35 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH - ); + emit IPufferProtocol.ValidationTimeConsumed(alice, 0, 35 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0); + vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidationTimeConsumed(bob, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("bob")), - 1, - PUFFER_MODULE_0, - pufferVault.convertToSharesUp(0.1 ether), - 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH + _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.1 ether) ); vm.expectEmit(true, true, true, true); - + emit IPufferProtocol.ValidationTimeConsumed(charlie, 0, 34 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("charlie")), - 2, - PUFFER_MODULE_0, - pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 2).bond, - 34 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH + _getPubKey(bytes32("charlie")), 2, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 2).bond ); // got slashed vm.expectEmit(true, true, true, true); - + emit IPufferProtocol.ValidationTimeConsumed(dianna, 0, 48 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("dianna")), - 3, - PUFFER_MODULE_0, - pufferVault.convertToSharesUp(0.2 ether), - 48 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH + _getPubKey(bytes32("dianna")), 3, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.2 ether) ); vm.expectEmit(true, true, true, true); - + emit IPufferProtocol.ValidationTimeConsumed(eve, 0, 2 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("eve")), - 4, - PUFFER_MODULE_0, - pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond, - 2.00000000000000025 ether // because of rounding we take a little more (28 days of VT) + _getPubKey(bytes32("eve")), 4, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond ); // got slashed pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); @@ -1369,14 +1363,12 @@ contract PufferProtocolTest is UnitTestHelper { }); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH - ); // 10 days of VT + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0); // 10 days of VT + emit IPufferProtocol.ValidationTimeConsumed(alice, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); _executeFullWithdrawal(aliceInfo); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH - ); // 10 days of VT + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0); // 10 days of VT + emit IPufferProtocol.ValidationTimeConsumed(bob, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); _executeFullWithdrawal(bobInfo); assertApproxEqAbs( @@ -1996,14 +1988,7 @@ contract PufferProtocolTest is UnitTestHelper { return amount * 1 ether; } - function _getVTBurnAmount(uint256 startEpoch, uint256 endEpoch) internal pure returns (uint256) { - uint256 validatedEpochs = endEpoch - startEpoch; - // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day - // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up - return validatedEpochs * 4444444444444445; - } - - function test_getNodeInfo() public { + function test_getNodeInfo() public view { // Test non-existent node NodeInfo memory nodeInfo = pufferProtocol.getNodeInfo(address(0x123)); assertEq(nodeInfo.activeValidatorCount, 0); From 9c42d1964809ae2d21dc26b3a89ec3d750dc3bb6 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 13 Jun 2025 10:57:52 +0200 Subject: [PATCH 39/82] update the test --- mainnet-contracts/src/PufferProtocol.sol | 64 +++++++++---------- .../test/unit/PufferProtocol.t.sol | 40 ++++-------- 2 files changed, 45 insertions(+), 59 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 43b6eab4..44f5afc4 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -197,14 +197,17 @@ contract PufferProtocol is ProtocolStorage storage $ = _getPufferProtocolStorage(); - _settleVTAccounting({ + uint256 burnAmount = _useVTOrValidationTime({ $: $, - node: node, + nodeOperator: node, totalEpochsValidated: totalEpochsValidated, - vtConsumptionSignature: vtConsumptionSignature, - deprecated_burntVTs: 0 + vtConsumptionSignature: vtConsumptionSignature }); + if (burnAmount > 0) { + VALIDATOR_TICKET.burn(burnAmount); + } + $.nodeOperatorInfo[node].validationTime += SafeCast.toUint96(msg.value); emit ValidationTimeDeposited({ node: node, ethAmount: msg.value }); } @@ -524,14 +527,12 @@ contract PufferProtocol is bondWithdrawals[i].node = validator.node; uint256 bondBurnAmount; - uint256 vtBurnAmount = _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i]); - // We need to scope the variables to avoid stack too deep errors { uint256 epochValidated = validatorInfos[i].totalEpochsValidated; bytes[] calldata vtConsumptionSignature = validatorInfos[i].vtConsumptionSignature; burnAmounts.vt += - _useVTOrValidationTime($, validator, vtBurnAmount, epochValidated, vtConsumptionSignature); + _useVTOrValidationTime($, bondWithdrawals[i].node, epochValidated, vtConsumptionSignature); } if (validatorInfos[i].isDownsize) { @@ -963,51 +964,48 @@ contract PufferProtocol is function _useVTOrValidationTime( ProtocolStorage storage $, - Validator storage validator, - uint256 vtBurnAmount, + address nodeOperator, uint256 totalEpochsValidated, bytes[] calldata vtConsumptionSignature - ) internal returns (uint256 burnedAmount) { + ) internal returns (uint256 vtAmountToBurn) { // Burn the VT first, then fallback to ETH from the node operator - uint256 nodeVTBalance = $.nodeOperatorInfo[validator.node].deprecated_vtBalance; + uint256 nodeVTBalance = $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance; + + uint256 vtBurnAmount = _getVTBurnAmount($, nodeOperator, totalEpochsValidated); // If the node operator has VT, we burn it first if (nodeVTBalance > 0) { if (nodeVTBalance >= vtBurnAmount) { // Burn the VT first, and update the node operator VT balance - burnedAmount = vtBurnAmount; + vtAmountToBurn = vtBurnAmount; // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[validator.node].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); + $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); - emit ValidationTimeConsumed({ - node: validator.node, - consumedAmount: 0, - deprecated_burntVTs: vtBurnAmount - }); + emit ValidationTimeConsumed({ node: nodeOperator, consumedAmount: 0, deprecated_burntVTs: vtBurnAmount }); - return burnedAmount; + return vtAmountToBurn; } // If the node operator has less VT than the amount to burn, we burn all of it, and we use the validation time - burnedAmount = nodeVTBalance; + vtAmountToBurn = nodeVTBalance; // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[validator.node].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); + $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); _settleVTAccounting({ $: $, - node: validator.node, + node: nodeOperator, totalEpochsValidated: totalEpochsValidated, vtConsumptionSignature: vtConsumptionSignature, deprecated_burntVTs: nodeVTBalance }); - return burnedAmount; + return vtAmountToBurn; } // If the node operator has no VT, we use the validation time _settleVTAccounting({ $: $, - node: validator.node, + node: nodeOperator, totalEpochsValidated: totalEpochsValidated, vtConsumptionSignature: vtConsumptionSignature, deprecated_burntVTs: 0 @@ -1040,37 +1038,37 @@ contract PufferProtocol is uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[node].totalEpochsValidated; - uint256 validatorTicketsBurnt = deprecated_burntVTs * 225 / 1 ether; // 1 VT = 1 DAY = 225 Epochs + // convert burned validator tickets to epochs + uint256 epochsBurntFromDeprecatedVT = deprecated_burntVTs * 225 / 1 ether; // 1 VT = 1 DAY. 1 DAY = 225 Epochs - uint256 amountToConsume = - (totalEpochsValidated - previousTotalEpochsValidated - validatorTicketsBurnt) * meanPrice; + uint256 validationTimeToConsume = + (totalEpochsValidated - previousTotalEpochsValidated - epochsBurntFromDeprecatedVT) * meanPrice; // Update the current epoch VT price for the node operator $.nodeOperatorInfo[node].epochPrice = epochCurrentPrice; $.nodeOperatorInfo[node].totalEpochsValidated = totalEpochsValidated; - $.nodeOperatorInfo[node].validationTime -= amountToConsume; + $.nodeOperatorInfo[node].validationTime -= validationTimeToConsume; emit ValidationTimeConsumed({ node: node, - consumedAmount: amountToConsume, + consumedAmount: validationTimeToConsume, deprecated_burntVTs: deprecated_burntVTs }); address weth = PUFFER_VAULT.asset(); // WETH is a contract that has a fallback function that accepts ETH, and never reverts - weth.call{ value: amountToConsume }(""); + weth.call{ value: validationTimeToConsume }(""); // Transfer WETH to the Revenue Distributor, it will be slow released to the PufferVault - ERC20(weth).transfer(PUFFER_REVENUE_DISTRIBUTOR, amountToConsume); + ERC20(weth).transfer(PUFFER_REVENUE_DISTRIBUTOR, validationTimeToConsume); } - function _getVTBurnAmount(ProtocolStorage storage $, address node, StoppedValidatorInfo calldata validatorInfo) + function _getVTBurnAmount(ProtocolStorage storage $, address node, uint256 validatedEpochs) internal view returns (uint256) { - uint256 validatedEpochs = validatorInfo.totalEpochsValidated; // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up uint256 vtBurnAmount = validatedEpochs * 4444444444444445; diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 8a7a3691..521a45e6 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -533,21 +533,25 @@ contract PufferProtocolTest is UnitTestHelper { // Provision a newly registered validator pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); - // 30 Days later, Alice wants to top-up more VT - vm.warp(block.timestamp + 10 days); - - // reduced by 100000000000 wei - pufferOracle.setMintPrice(9703921568628); - - // alice validated for 10 days * 225 epochs = 2250 epochs with 1 validator - // uint256 vtBurnAmount = validatedEpochs * 4444444444444445 - uint256 validatedEpochs = 2250; + // alice validated for 20 days * 225 epochs = 4500 epochs with 1 validator + uint256 validatedEpochs = 4500; bytes[] memory vtConsumptionSignatures = _getGuardianSignaturesForRegistration(alice, validatedEpochs); + // We deposit 10 VT for alice (legacy VT) + deal(address(validatorTicket), address(this), 10 ether); + validatorTicket.approve(address(pufferProtocol), 10 ether); + emptyPermit.amount = 10 ether; + pufferProtocol.depositValidatorTickets(emptyPermit, alice); + vm.startPrank(alice); + + // We then deposit validation time for Alice, it should burn 10 legacy VTs, and 10 of the validation time + vm.expectEmit(true, true, true, true); + emit IPufferProtocol.ValidationTimeConsumed( + alice, 10 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 10 ether + ); // 10 Legacy VTs got burned pufferProtocol.depositValidationTime{ value: 1 ether }(alice, validatedEpochs, vtConsumptionSignatures); - vm.stopPrank(); } function testRevert_invalidETHPayment() external { @@ -604,16 +608,11 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(validator.bond, pufferVault.balanceOf(address(pufferProtocol)), "alice bond is in the protocol"); - vm.warp(startTimestamp); - pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Didn't claim the bond yet assertEq(pufferVault.balanceOf(alice), 0, "alice has zero pufETH"); - // 15 days later (+16 is because 1 day is the start offset) - vm.warp(startTimestamp + 16 days); - StoppedValidatorInfo memory validatorInfo = StoppedValidatorInfo({ module: NoRestakingModule, moduleName: PUFFER_MODULE_0, @@ -634,9 +633,6 @@ contract PufferProtocolTest is UnitTestHelper { assertApproxEqAbs( pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1.5 ether, 1, "assets owned by alice" ); - - // Alice doesn't withdraw her VT's right away - vm.warp(startTimestamp + 50 days); } // Alice deposits VT for herself @@ -1479,8 +1475,6 @@ contract PufferProtocolTest is UnitTestHelper { uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); assertEq(exchangeRateBefore, 1 ether, "shares before provisioning, 1:1"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.deal(NoRestakingModule, 200 ether); @@ -1532,8 +1526,6 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(address(pufferVault).balance, 1003 ether, "1003 ETH in the vault"); assertEq(exchangeRateBefore, 1 ether, "shares before provisioning, 1:1"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // We provision one validator @@ -1601,8 +1593,6 @@ contract PufferProtocolTest is UnitTestHelper { uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); assertEq(exchangeRateBefore, 1 ether, "shares before provisioning"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.deal(NoRestakingModule, 200 ether); @@ -1654,8 +1644,6 @@ contract PufferProtocolTest is UnitTestHelper { uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); assertEq(exchangeRateBefore, 1 ether, "shares before provisioning"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.deal(NoRestakingModule, 200 ether); From 5a9bd38616e68257f1b003f03f00a581da8b9f2e Mon Sep 17 00:00:00 2001 From: Benjamin Date: Fri, 13 Jun 2025 11:04:31 +0200 Subject: [PATCH 40/82] codespell --- mainnet-contracts/src/PufferProtocol.sol | 2 +- mainnet-contracts/test/unit/PufferProtocol.t.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 44f5afc4..67f3cbab 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -286,7 +286,7 @@ contract PufferProtocol is uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); // The node operator must deposit 1.5 ETH or more + minimum validation time for ~30 days - // At the moment thats roughly 30 days * 225 (there is rougly 225 epochs per day) + // At the moment that's roughly 30 days * 225 (there is roughly 225 epochs per day) uint256 minimumETHRequired = _VALIDATOR_BOND + (_MINIMUM_EPOCHS_VALIDATION * epochCurrentPrice); require(msg.value >= minimumETHRequired, InvalidETHAmount()); diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 521a45e6..fad769d6 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -37,7 +37,7 @@ contract PufferProtocolTest is UnitTestHelper { */ uint256 internal constant MINIMUM_EPOCHS_VALIDATION = 6750; - // Eth has rougly 225 epochs per day + // Eth has 225 epochs per day uint256 internal constant EPOCHS_PER_DAY = 225; // 1 VT is burned per 225 epochs From 1fca25b3326d4ca87cd121baf1b6a64ba5ca18cb Mon Sep 17 00:00:00 2001 From: eladio Date: Mon, 23 Jun 2025 09:35:06 +0200 Subject: [PATCH 41/82] Improved IProtocol natspec and naming. Slight optimization in protocol --- mainnet-contracts/src/PufferProtocol.sol | 8 ++--- .../src/interface/IPufferProtocol.sol | 31 +++++++++++++------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 67f3cbab..aa53983d 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -239,9 +239,7 @@ contract PufferProtocol is } /** - * @notice New function that allows the transaction sender (node operator) to withdraw WETH to a recipient (use this instead of `withdrawValidatorTickets`) - * The Validation time can be withdrawn if there are no active or pending validators - * The WETH is sent to the recipient + * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ function withdrawValidationTime(uint96 amount, address recipient) external restricted { @@ -971,10 +969,10 @@ contract PufferProtocol is // Burn the VT first, then fallback to ETH from the node operator uint256 nodeVTBalance = $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance; - uint256 vtBurnAmount = _getVTBurnAmount($, nodeOperator, totalEpochsValidated); - // If the node operator has VT, we burn it first if (nodeVTBalance > 0) { + + uint256 vtBurnAmount = _getVTBurnAmount($, nodeOperator, totalEpochsValidated); if (nodeVTBalance >= vtBurnAmount) { // Burn the VT first, and update the node operator VT balance vtAmountToBurn = vtBurnAmount; diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index ada6318e..a681d6f4 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -155,9 +155,9 @@ interface IPufferProtocol { /** * @notice Emitted when Validation Time is withdrawn from the protocol - * @dev Signature "0xba152c9819ee6cbe5243df48eb44ae038608f08bc7e9e9042bfc32f996257781" + * @dev Signature "0xd19b9bc208843da6deef01aa6dedd607204c4f8b6d02f79b60e326a8c6e2b6e8" */ - event ValidationTimeWithdrawn(address indexed node, address indexed recipient, uint256 amount); + event ValidationTimeWithdrawn(address indexed node, address indexed recipient, uint256 ethAmount); /** * @notice Emitted when the guardians decide to skip validator provisioning for `moduleName` @@ -177,7 +177,7 @@ interface IPufferProtocol { * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain * @param moduleName is the staking Module * @param numBatches is the number of batches the validator has - * @dev Signature "0x6b9febc68231d6c196b22b02f442fa6dc3148ee90b6e83d5b978c11833587159" + * @dev Signature "0xd97b45553982eba642947754e3448d2142408b73d3e4be6b760a89066eb6c00a" */ event ValidatorKeyRegistered( bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint8 numBatches @@ -189,7 +189,7 @@ interface IPufferProtocol { * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain * @param moduleName is the staking Module * @param pufETHBurnAmount The amount of pufETH burned from the Node Operator - * @dev Signature "0xf435da9e3aeccc40d39fece7829f9941965ceee00d31fa7a89d608a273ea906e" + * @dev Signature "0x0ee12bdc2aff5d233a9a1ade9fa115fc2a8dd82c1a30dd0a46b5e4763b887289" */ event ValidatorExited( bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint256 pufETHBurnAmount @@ -283,6 +283,13 @@ interface IPufferProtocol { external payable; + /** + * @notice New function that allows the transaction sender (node operator) to withdraw WETH to a recipient (use this instead of `withdrawValidatorTickets`) + * The Validation time can be withdrawn if there are no active or pending validators + * The WETH is sent to the recipient + */ + function withdrawValidationTime(uint96 amount, address recipient) external; + /** * @notice Withdraws the `amount` of Validator Tickers from the `msg.sender` to the `recipient` * DEPRECATED - This method is deprecated and will be removed in the future upgrade @@ -296,7 +303,7 @@ interface IPufferProtocol { * @param srcIndices The indices of the validators to consolidate from * @param targetIndices The indices of the validators to consolidate to * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) - * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule * to the caller from the EigenPod */ function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) @@ -332,13 +339,12 @@ interface IPufferProtocol { /** * @notice Batch settling of validator withdrawals - * * @notice Settles a validator withdrawal * @dev This is one of the most important methods in the protocol * The withdrawals might be partial or total, and the validator might be downsized or fully exited * It has multiple tasks: * 1. Burn the pufETH from the node operator (if the withdrawal amount was lower than 32 ETH * numBatches or completely if the validator was slashed) - * 2. Burn the Validator Tickets from the node operator + * 2. Burn the Validator Tickets from the node operator (deprecated) and transfer consumed validation time (as WETH) to the PUFFER_REVENUE_DISTRIBUTOR * 3. Transfer withdrawal ETH from the PufferModule of the Validator to the PufferVault * 4. Decrement the `lockedETHAmount` on the PufferOracle to reflect the new amount of locked ETH */ @@ -349,6 +355,8 @@ interface IPufferProtocol { /** * @notice Skips the next validator for `moduleName` + * @param moduleName The name of the module + * @param guardianEOASignatures The signatures of the guardians to validate the skipping of provisioning * @dev Restricted to Guardians */ function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external; @@ -406,6 +414,8 @@ interface IPufferProtocol { /** * @notice Provisions the next node that is in line for provisioning + * @param validatorSignature The signature of the validator to provision + * @param depositRootHash The deposit root hash of the validator * @dev You can check who is next for provisioning by calling `getNextValidatorToProvision` method */ function provisionNode(bytes calldata validatorSignature, bytes32 depositRootHash) external; @@ -446,11 +456,13 @@ interface IPufferProtocol { * @dev There is a queue per moduleName and it is FIFO * @param data The validator key data * @param moduleName The name of the module + * @param totalEpochsValidated The total number of epochs validated by the validator + * @param vtConsumptionSignature The signature of the guardians to validate the number of epochs validated */ function registerValidatorKey( ValidatorKeyData calldata data, bytes32 moduleName, - uint256 vtConsumptionAmount, + uint256 totalEpochsValidated, bytes[] calldata vtConsumptionSignature ) external payable; @@ -485,8 +497,7 @@ interface IPufferProtocol { function getWithdrawalCredentials(address module) external view returns (bytes memory); /** - * @notice Returns the minimum amount of Validator Tokens to run a validator - * Returns the minimum amount of Epochs a validator needs to run + * @notice Returns the minimum amount of Epochs a validator needs to run */ function getMinimumVtAmount() external view returns (uint256); From 82822595df005b37cdeb9d4b84cdf9a43116cd9d Mon Sep 17 00:00:00 2001 From: eladio Date: Tue, 24 Jun 2025 13:51:50 +0200 Subject: [PATCH 42/82] Added comments and small refactor --- mainnet-contracts/src/PufferProtocol.sol | 27 +++++++++++-------- .../src/interface/IPufferProtocol.sol | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index aa53983d..d3115437 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -98,6 +98,7 @@ contract PufferProtocol is /** * @inheritdoc IPufferProtocol + * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ ValidatorTicket public immutable override VALIDATOR_TICKET; @@ -163,6 +164,7 @@ contract PufferProtocol is /** * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function depositValidatorTickets(Permit calldata permit, address node) external restricted { if (node == address(0)) { @@ -215,6 +217,7 @@ contract PufferProtocol is /** * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function withdrawValidatorTickets(uint96 amount, address recipient) external restricted { ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -268,7 +271,7 @@ contract PufferProtocol is } /** - * @notice Registers a validator key and consumes the ETH for the validation time for the other active validators. + * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ function registerValidatorKey( @@ -775,6 +778,7 @@ contract PufferProtocol is /** * @inheritdoc IPufferProtocol + * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function getValidatorTicketsBalance(address owner) public view returns (uint256) { ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -960,6 +964,16 @@ contract PufferProtocol is ); } + /** + * @dev Internal function to return the deprecated validator tickets burn amount + * and/or consume the validation time from the node operator + * @dev The deprecated vt balance is reduced here but the actual VT is not burned here (for efficiency) + * @param $ The protocol storage + * @param nodeOperator The node operator address + * @param totalEpochsValidated The total number of epochs validated by the node operator + * @param vtConsumptionSignature The signature of the guardians to validate the number of epochs validated + * @return vtAmountToBurn The amount of VT to burn + */ function _useVTOrValidationTime( ProtocolStorage storage $, address nodeOperator, @@ -989,15 +1003,6 @@ contract PufferProtocol is // nosemgrep basic-arithmetic-underflow $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); - _settleVTAccounting({ - $: $, - node: nodeOperator, - totalEpochsValidated: totalEpochsValidated, - vtConsumptionSignature: vtConsumptionSignature, - deprecated_burntVTs: nodeVTBalance - }); - - return vtAmountToBurn; } // If the node operator has no VT, we use the validation time @@ -1006,7 +1011,7 @@ contract PufferProtocol is node: nodeOperator, totalEpochsValidated: totalEpochsValidated, vtConsumptionSignature: vtConsumptionSignature, - deprecated_burntVTs: 0 + deprecated_burntVTs: nodeVTBalance }); } diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index a681d6f4..02f08926 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -452,7 +452,7 @@ interface IPufferProtocol { function createPufferModule(bytes32 moduleName) external returns (address); /** - * @notice Registers a new validator key in a `moduleName` queue with a permit + * @notice Registers a validator key and consumes the ETH for the validation time for the other active validators. * @dev There is a queue per moduleName and it is FIFO * @param data The validator key data * @param moduleName The name of the module From 95811edd505caba643126e27ce8e1a4bce2183a5 Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:52:39 +0000 Subject: [PATCH 43/82] forge fmt --- mainnet-contracts/src/PufferProtocol.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index d3115437..4eb029ca 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -985,7 +985,6 @@ contract PufferProtocol is // If the node operator has VT, we burn it first if (nodeVTBalance > 0) { - uint256 vtBurnAmount = _getVTBurnAmount($, nodeOperator, totalEpochsValidated); if (nodeVTBalance >= vtBurnAmount) { // Burn the VT first, and update the node operator VT balance @@ -1002,7 +1001,6 @@ contract PufferProtocol is vtAmountToBurn = nodeVTBalance; // nosemgrep basic-arithmetic-underflow $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); - } // If the node operator has no VT, we use the validation time From e8cc1a94a6d244d379001ad5a2730e2810286401 Mon Sep 17 00:00:00 2001 From: eladio Date: Tue, 24 Jun 2025 14:16:46 +0200 Subject: [PATCH 44/82] Removed duplicated code --- mainnet-contracts/src/PufferProtocol.sol | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 4eb029ca..372d6477 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -1159,8 +1159,8 @@ contract PufferProtocol is uint96 bondAmount = validator.bond; uint256 numBatches = validator.numBatches; - // Get the burnAmount for the withdrawal at the current exchange rate - uint256 burnAmount = _getBondBurnAmount({ + // Get the bondBurnAmount for the withdrawal at the current exchange rate + bondBurnAmount = _getBondBurnAmount({ validatorInfo: validatorInfo, validatorBondAmount: bondAmount, numBatches: validator.numBatches @@ -1170,13 +1170,7 @@ contract PufferProtocol is pubKey: validator.pubKey, pufferModuleIndex: validatorInfo.pufferModuleIndex, moduleName: validatorInfo.moduleName, - pufETHBurnAmount: burnAmount - }); - - bondBurnAmount = _getBondBurnAmount({ - validatorInfo: validatorInfo, - validatorBondAmount: bondAmount, - numBatches: validator.numBatches + pufETHBurnAmount: bondBurnAmount }); // Decrease the number of registered validators for that module @@ -1191,7 +1185,7 @@ contract PufferProtocol is validatorInfo.pufferModuleIndex ]; // nosemgrep basic-arithmetic-underflow - return (burnAmount, bondAmount - burnAmount, numBatches); + return (bondBurnAmount, bondAmount - bondBurnAmount, numBatches); } function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } From c1cdf1eda996d0d702288bba3218d97e336f872b Mon Sep 17 00:00:00 2001 From: eladio Date: Tue, 24 Jun 2025 14:25:36 +0200 Subject: [PATCH 45/82] Fixed wrong comment --- mainnet-contracts/src/PufferProtocol.sol | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 372d6477..273a4975 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -1125,11 +1125,10 @@ contract PufferProtocol is numBatches: numBatchesBefore }); - // However the burned part of the bond will be distributed between the bond returned and bond remaining (proportional to downsizing) - - // The bond to be returned is proportional to the num of batches we are downsizing minus the burned amount exitingBond = validator.bond * exitedBatches / validator.numBatches; + // The burned amount is subtracted from the exiting bond, so the remaining bond is kept in full + // The backend must prevent any downsize that would result in a burned amount greater than the exiting bond require(exitingBond >= burnAmount, InvalidWithdrawAmount()); exitingBond -= burnAmount; From 991d55849771d672edba717ed807621eef7e8dbc Mon Sep 17 00:00:00 2001 From: eladio Date: Tue, 24 Jun 2025 18:04:32 +0200 Subject: [PATCH 46/82] Added numBatches param to relevant events --- mainnet-contracts/src/PufferProtocol.sol | 5 ++- .../src/interface/IPufferProtocol.sol | 16 ++++++-- .../test/unit/PufferProtocol.t.sol | 38 ++++++++++--------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 273a4975..f01878e1 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -952,7 +952,7 @@ contract PufferProtocol is // Transfer 32 ETH to this contract for each batch PUFFER_VAULT.transferETH(address(this), numBatches * 32 ether); - emit SuccessfullyProvisioned(validatorPubKey, index, moduleName); + emit SuccessfullyProvisioned(validatorPubKey, index, moduleName, numBatches); // Increase lockedETH on Puffer Oracle for (uint256 i = 0; i < numBatches; ++i) { @@ -1169,7 +1169,8 @@ contract PufferProtocol is pubKey: validator.pubKey, pufferModuleIndex: validatorInfo.pufferModuleIndex, moduleName: validatorInfo.moduleName, - pufETHBurnAmount: bondBurnAmount + pufETHBurnAmount: bondBurnAmount, + numBatches: numBatches }); // Decrease the number of registered validators for that module diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 02f08926..43fbadf4 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -189,10 +189,15 @@ interface IPufferProtocol { * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain * @param moduleName is the staking Module * @param pufETHBurnAmount The amount of pufETH burned from the Node Operator - * @dev Signature "0x0ee12bdc2aff5d233a9a1ade9fa115fc2a8dd82c1a30dd0a46b5e4763b887289" + * @param numBatches is the number of batches the validator had + * @dev Signature "0xf435da9e3aeccc40d39fece7829f9941965ceee00d31fa7a89d608a273ea906e" */ event ValidatorExited( - bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint256 pufETHBurnAmount + bytes pubKey, + uint256 indexed pufferModuleIndex, + bytes32 indexed moduleName, + uint256 pufETHBurnAmount, + uint256 numBatches ); /** @@ -239,9 +244,12 @@ interface IPufferProtocol { * @param pubKey is the validator public key * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain * @param moduleName is the staking Module - * @dev Signature "0x96cbbd073e24b0a7d0cab7dc347c239e52be23c1b44ce240b3b929821fed19a4" + * @param numBatches is the number of batches the validator has + * @dev Signature "0xfed1ead36b4481c77b26f25acade13754ce94663e2515f15507b2cfbade3ed8d" */ - event SuccessfullyProvisioned(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName); + event SuccessfullyProvisioned( + bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint256 numBatches + ); /** * @notice Returns validator information diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index fad769d6..3cc93d1e 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -176,7 +176,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(idx, 1, "idx should be 1"); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0); + emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); moduleSelectionIndex = pufferProtocol.getModuleSelectIndex(); assertEq(moduleSelectionIndex, 1, "module idx changed"); @@ -334,12 +334,12 @@ contract PufferProtocolTest is UnitTestHelper { // 1. provision zero key vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(zeroPubKey, 0, PUFFER_MODULE_0); + emit IPufferProtocol.SuccessfullyProvisioned(zeroPubKey, 0, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Provision Bob that is not zero pubKey vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(bobPubKey, 1, PUFFER_MODULE_0); + emit IPufferProtocol.SuccessfullyProvisioned(bobPubKey, 1, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); Validator memory bobValidator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 1); @@ -348,7 +348,7 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); - emit IPufferProtocol.SuccessfullyProvisioned(zeroPubKey, 3, PUFFER_MODULE_0); + emit IPufferProtocol.SuccessfullyProvisioned(zeroPubKey, 3, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Get validators @@ -397,7 +397,7 @@ contract PufferProtocolTest is UnitTestHelper { // Provision Bob that is not zero pubKey vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 0, PUFFER_MODULE_0); + emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 0, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -407,7 +407,7 @@ contract PufferProtocolTest is UnitTestHelper { assertTrue(nextId == 0, "module id"); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("benjamin")), 0, EIGEN_DA); + emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("benjamin")), 0, EIGEN_DA, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -447,7 +447,7 @@ contract PufferProtocolTest is UnitTestHelper { ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("alice")), 1, PUFFER_MODULE_0); + emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("alice")), 1, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } @@ -932,7 +932,7 @@ contract PufferProtocolTest is UnitTestHelper { alice, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0); + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); _executeFullWithdrawal( StoppedValidatorInfo({ module: NoRestakingModule, @@ -1053,13 +1053,13 @@ contract PufferProtocolTest is UnitTestHelper { alice, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0); + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidationTimeConsumed( bob, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0); + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); @@ -1147,28 +1147,32 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidationTimeConsumed(alice, 0, 35 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0); + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidationTimeConsumed(bob, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.1 ether) + _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.1 ether), 1 ); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidationTimeConsumed(charlie, 0, 34 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("charlie")), 2, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 2).bond + _getPubKey(bytes32("charlie")), + 2, + PUFFER_MODULE_0, + pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 2).bond, + 1 ); // got slashed vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidationTimeConsumed(dianna, 0, 48 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("dianna")), 3, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.2 ether) + _getPubKey(bytes32("dianna")), 3, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.2 ether), 1 ); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidationTimeConsumed(eve, 0, 2 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("eve")), 4, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond + _getPubKey(bytes32("eve")), 4, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond, 1 ); // got slashed pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); @@ -1359,11 +1363,11 @@ contract PufferProtocolTest is UnitTestHelper { }); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0); // 10 days of VT + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); // 10 days of VT emit IPufferProtocol.ValidationTimeConsumed(alice, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); _executeFullWithdrawal(aliceInfo); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0); // 10 days of VT + emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); // 10 days of VT emit IPufferProtocol.ValidationTimeConsumed(bob, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); _executeFullWithdrawal(bobInfo); From 1f36dca379e2dbadf89da688fd9094f500919eca Mon Sep 17 00:00:00 2001 From: eladio Date: Thu, 26 Jun 2025 17:20:05 +0200 Subject: [PATCH 47/82] FIxed param bug in _getVTBurnAmount, added checks to _useVTOrValidationTime and improved natspec --- mainnet-contracts/src/PufferProtocol.sol | 26 +++++++++++++++++-- .../src/interface/IPufferProtocol.sol | 6 +++++ .../src/struct/StoppedValidatorInfo.sol | 4 +-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index f01878e1..78b11f3e 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -386,7 +386,6 @@ contract PufferProtocol is function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable - virtual restricted { if (srcIndices.length == 0) { @@ -980,12 +979,20 @@ contract PufferProtocol is uint256 totalEpochsValidated, bytes[] calldata vtConsumptionSignature ) internal returns (uint256 vtAmountToBurn) { + uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[nodeOperator].totalEpochsValidated; + + if (previousTotalEpochsValidated == totalEpochsValidated) { + return 0; + } + require(previousTotalEpochsValidated < totalEpochsValidated, InvalidTotalEpochsValidated()); + // Burn the VT first, then fallback to ETH from the node operator uint256 nodeVTBalance = $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance; // If the node operator has VT, we burn it first if (nodeVTBalance > 0) { - uint256 vtBurnAmount = _getVTBurnAmount($, nodeOperator, totalEpochsValidated); + uint256 vtBurnAmount = + _getVTBurnAmount($, nodeOperator, totalEpochsValidated - previousTotalEpochsValidated); if (nodeVTBalance >= vtBurnAmount) { // Burn the VT first, and update the node operator VT balance vtAmountToBurn = vtBurnAmount; @@ -1013,6 +1020,14 @@ contract PufferProtocol is }); } + /** + * @dev Internal function to settle the VT accounting for a node operator + * @param $ The protocol storage + * @param node The node operator address + * @param totalEpochsValidated The total number of epochs validated by the node operator + * @param vtConsumptionSignature The signature of the guardians to validate the number of epochs validated + * @param deprecated_burntVTs The amount of VT to burn (to be deducted from validation time consumption) + */ function _settleVTAccounting( ProtocolStorage storage $, address node, @@ -1065,6 +1080,13 @@ contract PufferProtocol is ERC20(weth).transfer(PUFFER_REVENUE_DISTRIBUTOR, validationTimeToConsume); } + /** + * @dev Internal function to get the amount of VT to burn during a number of epochs + * @param $ The protocol storage + * @param node The node operator address + * @param validatedEpochs The number of epochs validated by the node operator (not necessarily the total epochs) + * @return vtBurnAmount The amount of VT to burn + */ function _getVTBurnAmount(ProtocolStorage storage $, address node, uint256 validatedEpochs) internal view diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 43fbadf4..d4fcea0b 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -105,6 +105,12 @@ interface IPufferProtocol { */ error InvalidWithdrawAmount(); + /** + * @notice Thrown when the total epochs validated is invalid + * @dev Signature "0x1af51909" + */ + error InvalidTotalEpochsValidated(); + /** * @notice Emitted when the number of active validators changes * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" diff --git a/mainnet-contracts/src/struct/StoppedValidatorInfo.sol b/mainnet-contracts/src/struct/StoppedValidatorInfo.sol index cf24ef5d..5766936e 100644 --- a/mainnet-contracts/src/struct/StoppedValidatorInfo.sol +++ b/mainnet-contracts/src/struct/StoppedValidatorInfo.sol @@ -15,9 +15,9 @@ struct StoppedValidatorInfo { uint256 pufferModuleIndex; /// @dev Amount of funds withdrawn upon validator stoppage. uint256 withdrawalAmount; - /// @dev Total number of epochs validated by the validator. + /// @dev Total number of epochs validated by the node operator. uint256 totalEpochsValidated; - /// @dev Signature of the guardian module that consumed the validator tickets. + /// @dev The signature of the guardians to validate the number of epochs validated. bytes[] vtConsumptionSignature; /// @dev Indicates whether the validator was downsized instead of exited bool isDownsize; From 079872a3e683ec58b9b836af20e25c755606c056 Mon Sep 17 00:00:00 2001 From: eladio Date: Fri, 27 Jun 2025 17:51:04 +0200 Subject: [PATCH 48/82] Removed min amount from _getVTBurnAmount --- mainnet-contracts/src/PufferProtocol.sol | 30 ++----------------- .../src/interface/IPufferProtocol.sol | 1 + 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 78b11f3e..4f37a314 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -991,8 +991,7 @@ contract PufferProtocol is // If the node operator has VT, we burn it first if (nodeVTBalance > 0) { - uint256 vtBurnAmount = - _getVTBurnAmount($, nodeOperator, totalEpochsValidated - previousTotalEpochsValidated); + uint256 vtBurnAmount = _getVTBurnAmount(totalEpochsValidated - previousTotalEpochsValidated); if (nodeVTBalance >= vtBurnAmount) { // Burn the VT first, and update the node operator VT balance vtAmountToBurn = vtBurnAmount; @@ -1082,36 +1081,13 @@ contract PufferProtocol is /** * @dev Internal function to get the amount of VT to burn during a number of epochs - * @param $ The protocol storage - * @param node The node operator address * @param validatedEpochs The number of epochs validated by the node operator (not necessarily the total epochs) * @return vtBurnAmount The amount of VT to burn */ - function _getVTBurnAmount(ProtocolStorage storage $, address node, uint256 validatedEpochs) - internal - view - returns (uint256) - { + function _getVTBurnAmount(uint256 validatedEpochs) internal pure returns (uint256) { // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up - uint256 vtBurnAmount = validatedEpochs * 4444444444444445; - - uint256 minimumVTAmount = $.minimumVtAmount; - uint256 nodeVTBalance = $.nodeOperatorInfo[node].deprecated_vtBalance; - - // If the VT burn amount is less than the minimum VT amount that means that the node operator exited early - // If we don't penalize it, the node operator can exit early and re-register with the same VTs. - // By doing that, they can lower the APY for the pufETH holders - if (minimumVTAmount > vtBurnAmount) { - // Case when the node operator registered the validator but afterwards the DAO increases the minimum VT amount - if (nodeVTBalance < minimumVTAmount) { - return nodeVTBalance; - } - - return minimumVTAmount; - } - - return vtBurnAmount; + return validatedEpochs * 4444444444444445; } function _callPermit(address token, Permit calldata permitData) internal { diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index d4fcea0b..fe4335b8 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -361,6 +361,7 @@ interface IPufferProtocol { * 2. Burn the Validator Tickets from the node operator (deprecated) and transfer consumed validation time (as WETH) to the PUFFER_REVENUE_DISTRIBUTOR * 3. Transfer withdrawal ETH from the PufferModule of the Validator to the PufferVault * 4. Decrement the `lockedETHAmount` on the PufferOracle to reflect the new amount of locked ETH + * @dev If a node operator exits early, will be penalized by the protocol by increasing the totalEpochsValidated so the VT consumption is higher than the actual amount of epochs validated */ function batchHandleWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, From 09690cea58d3cd5b37f759137c506bbb391ad465 Mon Sep 17 00:00:00 2001 From: eladio Date: Tue, 1 Jul 2025 18:05:22 +0200 Subject: [PATCH 49/82] Reworked signatures (tests not adapted) --- mainnet-contracts/src/GuardianModule.sol | 41 ++++- mainnet-contracts/src/LibGuardianMessages.sol | 14 +- .../src/ProtocolSignatureNonces.sol | 100 +++++++++++ mainnet-contracts/src/PufferProtocol.sol | 155 ++++++++++++------ .../src/interface/IGuardianModule.sol | 18 +- .../src/interface/IPufferProtocol.sol | 31 +++- mainnet-contracts/src/struct/Signatures.sol | 10 ++ 7 files changed, 298 insertions(+), 71 deletions(-) create mode 100644 mainnet-contracts/src/ProtocolSignatureNonces.sol create mode 100644 mainnet-contracts/src/struct/Signatures.sol diff --git a/mainnet-contracts/src/GuardianModule.sol b/mainnet-contracts/src/GuardianModule.sol index c25f4878..c0233a97 100644 --- a/mainnet-contracts/src/GuardianModule.sol +++ b/mainnet-contracts/src/GuardianModule.sol @@ -177,11 +177,12 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule */ - function validateBatchWithdrawals(StoppedValidatorInfo[] calldata validatorInfos, bytes[] calldata eoaSignatures) - external - view - { - bytes32 signedMessageHash = LibGuardianMessages._getHandleBatchWithdrawalMessage(validatorInfos); + function validateBatchWithdrawals( + StoppedValidatorInfo[] calldata validatorInfos, + bytes[] calldata eoaSignatures, + uint256 deadline + ) external view { + bytes32 signedMessageHash = LibGuardianMessages._getHandleBatchWithdrawalMessage(validatorInfos, deadline); // Check the signatures bool validSignatures = @@ -213,6 +214,36 @@ contract GuardianModule is AccessManaged, IGuardianModule { } } + /** + * @inheritdoc IGuardianModule + */ + function validateWithdrawalRequest(bytes[] calldata eoaSignatures, bytes32 messageHash) external view { + // Recreate the message hash + bytes32 signedMessageHash = LibGuardianMessages._getAnyHashedMessage(messageHash); + + bool validSignatures = + validateGuardiansEOASignatures({ eoaSignatures: eoaSignatures, signedMessageHash: signedMessageHash }); + + if (!validSignatures) { + revert Unauthorized(); + } + } + + /** + * @inheritdoc IGuardianModule + */ + function validateTotalEpochsValidated(bytes[] calldata eoaSignatures, bytes32 messageHash) external view { + // Recreate the message hash + bytes32 signedMessageHash = LibGuardianMessages._getAnyHashedMessage(messageHash); + + bool validSignatures = + validateGuardiansEOASignatures({ eoaSignatures: eoaSignatures, signedMessageHash: signedMessageHash }); + + if (!validSignatures) { + revert Unauthorized(); + } + } + /** * @inheritdoc IGuardianModule */ diff --git a/mainnet-contracts/src/LibGuardianMessages.sol b/mainnet-contracts/src/LibGuardianMessages.sol index 59874181..76112907 100644 --- a/mainnet-contracts/src/LibGuardianMessages.sol +++ b/mainnet-contracts/src/LibGuardianMessages.sol @@ -47,14 +47,15 @@ library LibGuardianMessages { /** * @notice Returns the message to be signed for handling the batch withdrawal * @param validatorInfos is an array of validator information + * @param deadline is the deadline for the signature * @return the message to be signed */ - function _getHandleBatchWithdrawalMessage(StoppedValidatorInfo[] memory validatorInfos) + function _getHandleBatchWithdrawalMessage(StoppedValidatorInfo[] memory validatorInfos, uint256 deadline) internal pure returns (bytes32) { - return keccak256(abi.encode(validatorInfos)).toEthSignedMessageHash(); + return keccak256(abi.encode(validatorInfos, deadline)).toEthSignedMessageHash(); } /** @@ -85,5 +86,14 @@ library LibGuardianMessages { { return keccak256(abi.encode(moduleName, root, blockNumber)).toEthSignedMessageHash(); } + + /** + * @notice Returns the message to be signed for any message + * @param hashedMessage is the hashed message to be signed + * @return the message to be signed + */ + function _getAnyHashedMessage(bytes32 hashedMessage) internal pure returns (bytes32) { + return hashedMessage.toEthSignedMessageHash(); + } } /* solhint-disable func-named-parameters */ diff --git a/mainnet-contracts/src/ProtocolSignatureNonces.sol b/mainnet-contracts/src/ProtocolSignatureNonces.sol new file mode 100644 index 00000000..987f46ea --- /dev/null +++ b/mainnet-contracts/src/ProtocolSignatureNonces.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * @title ProtocolSignatureNonces + * @author Puffer Finance + * @custom:security-contact security@puffer.fi + * @dev Abstract contract for managing protocol signatures with selector-based nonces and deadline support. + * + * This contract implements a selector-based nonce system to prevent DOS attacks through nonce manipulation. + * Each function can have its own nonce space using a unique selector, preventing cross-function nonce conflicts. + * + * Key security features: + * - Selector-based nonces prevent DOS attacks between different operations + * - Deadline support for signature expiration (recommended implementation) + * - Nonce validation to ensure proper signature ordering + */ +abstract contract ProtocolSignatureNonces { + /** + * @dev The nonce used for an `account` is not the expected current nonce. + * @param selector The function selector that determines the nonce space + * @param account The account whose nonce was invalid + * @param currentNonce The current expected nonce for the account + */ + error InvalidAccountNonce(bytes32 selector, address account, uint256 currentNonce); + + struct ProtocolSignatureNoncesStorage { + /** + * @dev Mapping from function selector to account to nonce value. + * This creates separate nonce spaces for different operations, + * preventing cross-function nonce manipulation attacks. + */ + mapping(bytes32 selector => mapping(address account => uint256)) _nonces; + } + + // keccak256(abi.encode(uint256(keccak256("ProtocolSignatureNoncesStorageLocation")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ProtocolSignatureNoncesStorageLocation = + 0xbaa308cee87141dd88d1ecc2d7cbf7f5fef8a56b897e48c821339feb34e04200; + + /** + * @dev Returns the storage pointer for nonces. + * @return $ The storage pointer to ProtocolSignatureNoncesStorage + */ + function _getProtocolSignatureNoncesStorage() private pure returns (ProtocolSignatureNoncesStorage storage $) { + assembly { + $.slot := ProtocolSignatureNoncesStorageLocation + } + } + + /** + * @dev Returns the next unused nonce for an address in a specific function context. + * @param selector The function selector that determines the nonce space + * @param owner The address to get the nonce for + * @return The current nonce value for the owner in the specified function context + */ + function nonces(bytes32 selector, address owner) public view virtual returns (uint256) { + ProtocolSignatureNoncesStorage storage $ = _getProtocolSignatureNoncesStorage(); + return $._nonces[selector][owner]; + } + + /** + * @dev Consumes a nonce for a specific function context. + * Returns the current value and increments nonce. + * @param selector The function selector that determines the nonce space + * @param owner The address whose nonce to consume + * @return The current nonce value before incrementing + * + * @dev This function increments the nonce atomically, ensuring + * that each nonce can only be used once per function context. + * The nonce cannot be decremented or reset, preventing replay attacks. + */ + function _useNonce(bytes32 selector, address owner) internal virtual returns (uint256) { + ProtocolSignatureNoncesStorage storage $ = _getProtocolSignatureNoncesStorage(); + // For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be + // decremented or reset. This guarantees that the nonce never overflows. + unchecked { + // It is important to do x++ and not ++x here. + return $._nonces[selector][owner]++; + } + } + + /** + * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. + * @param selector The function selector that determines the nonce space + * @param owner The address whose nonce to validate and consume + * @param nonce The expected nonce value + * + * @dev This function validates that the provided nonce matches the expected + * current nonce before consuming it. This prevents replay attacks and + * ensures proper signature ordering. + * + * @dev Reverts with InvalidAccountNonce if the nonce doesn't match. + */ + function _useCheckedNonce(bytes32 selector, address owner, uint256 nonce) internal virtual { + uint256 current = _useNonce(selector, owner); + if (nonce != current) { + revert InvalidAccountNonce(selector, owner, current); + } + } +} diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 4f37a314..71766e08 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -24,7 +24,8 @@ import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; -import { NoncesUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/NoncesUpgradeable.sol"; +import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; +import { EpochsValidatedSignature } from "./struct/Signatures.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; /** @@ -39,7 +40,7 @@ contract PufferProtocol is AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolStorage, - NoncesUpgradeable + ProtocolSignatureNonces { /** * @dev Helper struct for the full withdrawals accounting @@ -189,29 +190,30 @@ contract PufferProtocol is * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ - function depositValidationTime(address node, uint256 totalEpochsValidated, bytes[] calldata vtConsumptionSignature) + function depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external payable restricted { - require(node != address(0), InvalidAddress()); + if (block.timestamp > epochsValidatedSignature.deadline) { + revert DeadlineExceeded(); + } + + require(epochsValidatedSignature.nodeOperator != address(0), InvalidAddress()); require(msg.value > 0, InvalidETHAmount()); ProtocolStorage storage $ = _getPufferProtocolStorage(); - uint256 burnAmount = _useVTOrValidationTime({ - $: $, - nodeOperator: node, - totalEpochsValidated: totalEpochsValidated, - vtConsumptionSignature: vtConsumptionSignature - }); + epochsValidatedSignature.functionSelector = IPufferProtocol.depositValidationTime.selector; + + uint256 burnAmount = _useVTOrValidationTime({ $: $, epochsValidatedSignature: epochsValidatedSignature }); if (burnAmount > 0) { VALIDATOR_TICKET.burn(burnAmount); } - $.nodeOperatorInfo[node].validationTime += SafeCast.toUint96(msg.value); - emit ValidationTimeDeposited({ node: node, ethAmount: msg.value }); + $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].validationTime += SafeCast.toUint96(msg.value); + emit ValidationTimeDeposited({ node: epochsValidatedSignature.nodeOperator, ethAmount: msg.value }); } /** @@ -278,8 +280,13 @@ contract PufferProtocol is ValidatorKeyData calldata data, bytes32 moduleName, uint256 totalEpochsValidated, - bytes[] calldata vtConsumptionSignature + bytes[] calldata vtConsumptionSignature, + uint256 deadline ) external payable restricted { + if (block.timestamp > deadline) { + revert DeadlineExceeded(); + } + ProtocolStorage storage $ = _getPufferProtocolStorage(); _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); @@ -296,9 +303,13 @@ contract PufferProtocol is _settleVTAccounting({ $: $, - node: msg.sender, - totalEpochsValidated: totalEpochsValidated, - vtConsumptionSignature: vtConsumptionSignature, + epochsValidatedSignature: EpochsValidatedSignature({ + nodeOperator: msg.sender, + totalEpochsValidated: totalEpochsValidated, + functionSelector: IPufferProtocol.registerValidatorKey.selector, + deadline: deadline, + signatures: vtConsumptionSignature + }), deprecated_burntVTs: 0 }); @@ -432,11 +443,17 @@ contract PufferProtocol is uint256[] calldata indices, uint64[] calldata gweiAmounts, WithdrawalType[] calldata withdrawalType, - bytes[][] calldata validatorAmountsSignatures + bytes[][] calldata validatorAmountsSignatures, + uint256 deadline ) external payable restricted { + if (block.timestamp > deadline) { + revert DeadlineExceeded(); + } + ProtocolStorage storage $ = _getPufferProtocolStorage(); bytes[] memory pubkeys = new bytes[](indices.length); + bytes32 functionSelector = IPufferProtocol.requestWithdrawal.selector; // validate pubkeys belong to that node and are active for (uint256 i = 0; i < indices.length; ++i) { @@ -455,12 +472,15 @@ contract PufferProtocol is } // If downsize or rewards withdrawal, backend needs to validate the amount - bytes32 messageHash = - keccak256(abi.encode(msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(msg.sender))); + bytes32 messageHash = keccak256( + abi.encode( + msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(functionSelector, msg.sender), deadline + ) + ); - GUARDIAN_MODULE.validateGuardiansEOASignatures({ + GUARDIAN_MODULE.validateWithdrawalRequest({ eoaSignatures: validatorAmountsSignatures[i], - signedMessageHash: messageHash + messageHash: messageHash }); } } @@ -502,9 +522,14 @@ contract PufferProtocol is */ function batchHandleWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, - bytes[] calldata guardianEOASignatures + bytes[] calldata guardianEOASignatures, + uint256 deadline ) external restricted { - GUARDIAN_MODULE.validateBatchWithdrawals(validatorInfos, guardianEOASignatures); + if (block.timestamp > deadline) { + revert DeadlineExceeded(); + } + + GUARDIAN_MODULE.validateBatchWithdrawals(validatorInfos, guardianEOASignatures, deadline); ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -514,6 +539,8 @@ contract PufferProtocol is // 1 batch = 32 ETH uint256 numExitedBatches; + bytes32 functionSelector = IPufferProtocol.batchHandleWithdrawals.selector; + // slither-disable-start calls-loop for (uint256 i = 0; i < validatorInfos.length; ++i) { Validator storage validator = @@ -530,9 +557,16 @@ contract PufferProtocol is // We need to scope the variables to avoid stack too deep errors { uint256 epochValidated = validatorInfos[i].totalEpochsValidated; - bytes[] calldata vtConsumptionSignature = validatorInfos[i].vtConsumptionSignature; - burnAmounts.vt += - _useVTOrValidationTime($, bondWithdrawals[i].node, epochValidated, vtConsumptionSignature); + burnAmounts.vt += _useVTOrValidationTime( + $, + EpochsValidatedSignature({ + nodeOperator: bondWithdrawals[i].node, + totalEpochsValidated: epochValidated, + functionSelector: functionSelector, + deadline: deadline, + signatures: validatorInfos[i].vtConsumptionSignature + }) + ); } if (validatorInfos[i].isDownsize) { @@ -968,30 +1002,35 @@ contract PufferProtocol is * and/or consume the validation time from the node operator * @dev The deprecated vt balance is reduced here but the actual VT is not burned here (for efficiency) * @param $ The protocol storage - * @param nodeOperator The node operator address - * @param totalEpochsValidated The total number of epochs validated by the node operator - * @param vtConsumptionSignature The signature of the guardians to validate the number of epochs validated + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Identifier of the function that initiated this flow + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated * @return vtAmountToBurn The amount of VT to burn */ - function _useVTOrValidationTime( - ProtocolStorage storage $, - address nodeOperator, - uint256 totalEpochsValidated, - bytes[] calldata vtConsumptionSignature - ) internal returns (uint256 vtAmountToBurn) { + function _useVTOrValidationTime(ProtocolStorage storage $, EpochsValidatedSignature memory epochsValidatedSignature) + internal + returns (uint256 vtAmountToBurn) + { + address nodeOperator = epochsValidatedSignature.nodeOperator; uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[nodeOperator].totalEpochsValidated; - if (previousTotalEpochsValidated == totalEpochsValidated) { + if (previousTotalEpochsValidated == epochsValidatedSignature.totalEpochsValidated) { return 0; } - require(previousTotalEpochsValidated < totalEpochsValidated, InvalidTotalEpochsValidated()); + require( + previousTotalEpochsValidated < epochsValidatedSignature.totalEpochsValidated, InvalidTotalEpochsValidated() + ); // Burn the VT first, then fallback to ETH from the node operator uint256 nodeVTBalance = $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance; // If the node operator has VT, we burn it first if (nodeVTBalance > 0) { - uint256 vtBurnAmount = _getVTBurnAmount(totalEpochsValidated - previousTotalEpochsValidated); + uint256 vtBurnAmount = + _getVTBurnAmount(epochsValidatedSignature.totalEpochsValidated - previousTotalEpochsValidated); if (nodeVTBalance >= vtBurnAmount) { // Burn the VT first, and update the node operator VT balance vtAmountToBurn = vtBurnAmount; @@ -1012,9 +1051,7 @@ contract PufferProtocol is // If the node operator has no VT, we use the validation time _settleVTAccounting({ $: $, - node: nodeOperator, - totalEpochsValidated: totalEpochsValidated, - vtConsumptionSignature: vtConsumptionSignature, + epochsValidatedSignature: epochsValidatedSignature, deprecated_burntVTs: nodeVTBalance }); } @@ -1022,29 +1059,38 @@ contract PufferProtocol is /** * @dev Internal function to settle the VT accounting for a node operator * @param $ The protocol storage - * @param node The node operator address - * @param totalEpochsValidated The total number of epochs validated by the node operator - * @param vtConsumptionSignature The signature of the guardians to validate the number of epochs validated + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Identifier of the function that initiated this flow + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated * @param deprecated_burntVTs The amount of VT to burn (to be deducted from validation time consumption) */ function _settleVTAccounting( ProtocolStorage storage $, - address node, - uint256 totalEpochsValidated, - bytes[] calldata vtConsumptionSignature, + EpochsValidatedSignature memory epochsValidatedSignature, uint256 deprecated_burntVTs ) internal { + address node = epochsValidatedSignature.nodeOperator; // There is nothing to settle if this is the first validator for the node operator if ($.nodeOperatorInfo[node].activeValidatorCount + $.nodeOperatorInfo[node].pendingValidatorCount == 0) { return; } // We have no way of getting the present consumed amount for the other validators on-chain, so we use Puffer Backend service to get that amount and a signature from the service - bytes32 messageHash = keccak256(abi.encode(node, totalEpochsValidated, _useNonce(node))); + bytes32 messageHash = keccak256( + abi.encode( + node, + epochsValidatedSignature.totalEpochsValidated, + _useNonce(epochsValidatedSignature.functionSelector, node), + epochsValidatedSignature.deadline + ) + ); - GUARDIAN_MODULE.validateGuardiansEOASignatures({ - eoaSignatures: vtConsumptionSignature, - signedMessageHash: messageHash + GUARDIAN_MODULE.validateTotalEpochsValidated({ + eoaSignatures: epochsValidatedSignature.signatures, + messageHash: messageHash }); uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); @@ -1056,12 +1102,13 @@ contract PufferProtocol is // convert burned validator tickets to epochs uint256 epochsBurntFromDeprecatedVT = deprecated_burntVTs * 225 / 1 ether; // 1 VT = 1 DAY. 1 DAY = 225 Epochs - uint256 validationTimeToConsume = - (totalEpochsValidated - previousTotalEpochsValidated - epochsBurntFromDeprecatedVT) * meanPrice; + uint256 validationTimeToConsume = ( + epochsValidatedSignature.totalEpochsValidated - previousTotalEpochsValidated - epochsBurntFromDeprecatedVT + ) * meanPrice; // Update the current epoch VT price for the node operator $.nodeOperatorInfo[node].epochPrice = epochCurrentPrice; - $.nodeOperatorInfo[node].totalEpochsValidated = totalEpochsValidated; + $.nodeOperatorInfo[node].totalEpochsValidated = epochsValidatedSignature.totalEpochsValidated; $.nodeOperatorInfo[node].validationTime -= validationTimeToConsume; emit ValidationTimeConsumed({ diff --git a/mainnet-contracts/src/interface/IGuardianModule.sol b/mainnet-contracts/src/interface/IGuardianModule.sol index 2a435b72..d68d1f5d 100644 --- a/mainnet-contracts/src/interface/IGuardianModule.sol +++ b/mainnet-contracts/src/interface/IGuardianModule.sol @@ -126,10 +126,12 @@ interface IGuardianModule { * The order of the signatures MUST the same as the order of the validators in the validator module * @param validatorInfos The information of the stopped validators * @param guardianEOASignatures The guardian EOA signatures + * @param deadline The deadline for the signature */ function validateBatchWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, - bytes[] calldata guardianEOASignatures + bytes[] calldata guardianEOASignatures, + uint256 deadline ) external; /** @@ -163,6 +165,20 @@ interface IGuardianModule { external view; + /** + * @notice Validates the withdrawal request + * @param eoaSignatures The guardian EOA signatures + * @param messageHash The message hash + */ + function validateWithdrawalRequest(bytes[] calldata eoaSignatures, bytes32 messageHash) external view; + + /** + * @notice Validates the total epochs validated + * @param eoaSignatures The guardian EOA signatures + * @param messageHash The message hash + */ + function validateTotalEpochsValidated(bytes[] calldata eoaSignatures, bytes32 messageHash) external view; + /** * @notice Returns the threshold value for guardian signatures * @dev The threshold value is the minimum number of guardian signatures required for a transaction to be considered valid diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index fe4335b8..918dbab1 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -14,6 +14,7 @@ import { ValidatorTicket } from "../ValidatorTicket.sol"; import { NodeInfo } from "../struct/NodeInfo.sol"; import { ModuleLimit } from "../struct/ProtocolStorage.sol"; import { StoppedValidatorInfo } from "../struct/StoppedValidatorInfo.sol"; +import { EpochsValidatedSignature } from "../struct/Signatures.sol"; import { IBeaconDepositContract } from "../interface/IBeaconDepositContract.sol"; /** @@ -111,6 +112,12 @@ interface IPufferProtocol { */ error InvalidTotalEpochsValidated(); + /** + * @notice Thrown when the deadline is exceeded + * @dev Signature "0xddff8620" + */ + error DeadlineExceeded(); + /** * @notice Emitted when the number of active validators changes * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" @@ -289,13 +296,14 @@ interface IPufferProtocol { /** * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`). * Deposits Validation Time for the `node`. Validation Time is in native ETH. - * @param node is the node operator address - * @param totalEpochsValidated is the total number of epochs validated by that node operator - * @param vtConsumptionSignature is the signature from the guardians over the total number of epochs validated + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Can be left empty, it will be used to prevent replay attacks + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated */ - function depositValidationTime(address node, uint256 totalEpochsValidated, bytes[] calldata vtConsumptionSignature) - external - payable; + function depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external payable; /** * @notice New function that allows the transaction sender (node operator) to withdraw WETH to a recipient (use this instead of `withdrawValidatorTickets`) @@ -333,6 +341,7 @@ interface IPufferProtocol { * @param gweiAmounts The amounts of the validators to withdraw, in Gwei * @param withdrawalType The type of withdrawal * @param validatorAmountsSignatures The signatures of the guardians to validate the amount of the validators to withdraw + * @param deadline The deadline for the signatures * @dev The pubkeys should be active validators on the same module * @dev There are 3 types of withdrawal: * EXIT_VALIDATOR: The validator is fully exited. The gweiAmount needs to be 0 @@ -348,7 +357,8 @@ interface IPufferProtocol { uint256[] calldata indices, uint64[] calldata gweiAmounts, WithdrawalType[] calldata withdrawalType, - bytes[][] calldata validatorAmountsSignatures + bytes[][] calldata validatorAmountsSignatures, + uint256 deadline ) external payable; /** @@ -365,7 +375,8 @@ interface IPufferProtocol { */ function batchHandleWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, - bytes[] calldata guardianEOASignatures + bytes[] calldata guardianEOASignatures, + uint256 deadline ) external; /** @@ -473,12 +484,14 @@ interface IPufferProtocol { * @param moduleName The name of the module * @param totalEpochsValidated The total number of epochs validated by the validator * @param vtConsumptionSignature The signature of the guardians to validate the number of epochs validated + * @param deadline The deadline for the signature */ function registerValidatorKey( ValidatorKeyData calldata data, bytes32 moduleName, uint256 totalEpochsValidated, - bytes[] calldata vtConsumptionSignature + bytes[] calldata vtConsumptionSignature, + uint256 deadline ) external payable; /** diff --git a/mainnet-contracts/src/struct/Signatures.sol b/mainnet-contracts/src/struct/Signatures.sol new file mode 100644 index 00000000..fe343b07 --- /dev/null +++ b/mainnet-contracts/src/struct/Signatures.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +struct EpochsValidatedSignature { + bytes32 functionSelector; + uint256 totalEpochsValidated; + address nodeOperator; + uint256 deadline; + bytes[] signatures; +} From 9b62be57f424c32ab7eee6aa8a72c8c01c2a2033 Mon Sep 17 00:00:00 2001 From: eladio Date: Tue, 1 Jul 2025 18:16:41 +0200 Subject: [PATCH 50/82] Adapted one script and test handler --- .../script/GenerateBLSKeysAndRegisterValidators.s.sol | 6 +++++- mainnet-contracts/test/handlers/PufferProtocolHandler.sol | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index 0462fcc6..0d11cb53 100644 --- a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol +++ b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol @@ -37,6 +37,8 @@ contract GenerateBLSKeysAndRegisterValidators is Script { bytes32 private constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + uint256 private constant SIGNATURE_VALIDITY_PERIOD = 1 days; // TODO: Check this value with team + function setUp() public { if (block.chainid == 17000) { // Holesky @@ -105,7 +107,9 @@ contract GenerateBLSKeysAndRegisterValidators is Script { numBatches: 1 }); - IPufferProtocol(protocolAddress).registerValidatorKey(validatorData, moduleName, 0, new bytes[](0)); + IPufferProtocol(protocolAddress).registerValidatorKey( + validatorData, moduleName, 0, new bytes[](0), block.timestamp + SIGNATURE_VALIDITY_PERIOD + ); registeredPubKeys.push(validatorData.blsPubKey); } diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index ec9c533e..10cab22d 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -584,7 +584,7 @@ contract PufferProtocolHandler is Test { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( - validatorKeyData, moduleName, 0, new bytes[](0) + validatorKeyData, moduleName, 0, new bytes[](0), block.timestamp + 1 days ); return (smoothingCommitment + bond); From 6b187f72df3ec72b5f9eb4488a80ad0d335043a3 Mon Sep 17 00:00:00 2001 From: eladio Date: Wed, 2 Jul 2025 13:38:01 +0200 Subject: [PATCH 51/82] Adapted tests to new func sigs --- .../test/unit/PufferProtocol.t.sol | 80 ++++++++++++++----- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 3cc93d1e..92d823cc 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -23,6 +23,7 @@ import { Permit } from "../../src/structs/Permit.sol"; import { ModuleLimit } from "../../src/struct/ProtocolStorage.sol"; import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol"; import { NodeInfo } from "../../src/struct/NodeInfo.sol"; +import { EpochsValidatedSignature } from "../../src/struct/Signatures.sol"; contract PufferProtocolTest is UnitTestHelper { using ECDSA for bytes32; @@ -196,7 +197,7 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); vm.expectRevert(IPufferProtocol.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( - validatorKeyData, bytes32("imaginary module"), 0, new bytes[](0) + validatorKeyData, bytes32("imaginary module"), 0, new bytes[](0), block.timestamp + 1 days ); } @@ -206,7 +207,9 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); uint256 amount = 5.11 ether; - pufferProtocol.registerValidatorKey{ value: amount }(validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0)); + pufferProtocol.registerValidatorKey{ value: amount }( + validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); assertEq( address(pufferProtocol).balance, @@ -244,12 +247,16 @@ contract PufferProtocolTest is UnitTestHelper { }); vm.expectRevert(IPufferProtocol.InvalidNumberOfBatches.selector); - pufferProtocol.registerValidatorKey{ value: vtPrice }(validatorData, PUFFER_MODULE_0, 0, new bytes[](0)); + pufferProtocol.registerValidatorKey{ value: vtPrice }( + validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); validatorData.numBatches = 65; vm.expectRevert(IPufferProtocol.InvalidNumberOfBatches.selector); - pufferProtocol.registerValidatorKey{ value: vtPrice }(validatorData, PUFFER_MODULE_0, 0, new bytes[](0)); + pufferProtocol.registerValidatorKey{ value: vtPrice }( + validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); } // Try registering with invalid BLS key length @@ -272,7 +279,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( - validatorData, PUFFER_MODULE_0, 0, new bytes[](0) + validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); } @@ -480,7 +487,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectRevert(); pufferProtocol.registerValidatorKey{ value: type(uint256).max }( - validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0) + validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); } @@ -499,7 +506,9 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); - pufferProtocol.registerValidatorKey{ value: amount }(data, PUFFER_MODULE_0, 0, new bytes[](0)); + pufferProtocol.registerValidatorKey{ value: amount }( + data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); assertApproxEqAbs( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), BOND, 1, "1 pufETH after" @@ -522,7 +531,9 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); - pufferProtocol.registerValidatorKey{ value: amount }(data, PUFFER_MODULE_0, 0, new bytes[](0)); + pufferProtocol.registerValidatorKey{ value: amount }( + data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); vm.stopPrank(); assertApproxEqAbs( @@ -551,7 +562,15 @@ contract PufferProtocolTest is UnitTestHelper { emit IPufferProtocol.ValidationTimeConsumed( alice, 10 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 10 ether ); // 10 Legacy VTs got burned - pufferProtocol.depositValidationTime{ value: 1 ether }(alice, validatedEpochs, vtConsumptionSignatures); + pufferProtocol.depositValidationTime{ value: 1 ether }( + EpochsValidatedSignature({ + nodeOperator: alice, + totalEpochsValidated: validatedEpochs, + functionSelector: IPufferProtocol.depositValidationTime.selector, + deadline: block.timestamp + 1 days, + signatures: vtConsumptionSignatures + }) + ); } function testRevert_invalidETHPayment() external { @@ -562,7 +581,9 @@ contract PufferProtocolTest is UnitTestHelper { // Underpay VT vm.expectRevert(); - pufferProtocol.registerValidatorKey{ value: 0.1 ether }(data, PUFFER_MODULE_0, 0, new bytes[](0)); + pufferProtocol.registerValidatorKey{ value: 0.1 ether }( + data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); } function test_validator_limit_per_module() external { @@ -579,7 +600,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectRevert(IPufferProtocol.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + BOND) }( - validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0) + validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); } @@ -1061,7 +1082,9 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); - pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); + pufferProtocol.batchHandleWithdrawals( + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + ); assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); @@ -1174,7 +1197,9 @@ contract PufferProtocolTest is UnitTestHelper { emit IPufferProtocol.ValidatorExited( _getPubKey(bytes32("eve")), 4, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond, 1 ); // got slashed - pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); + pufferProtocol.batchHandleWithdrawals( + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + ); assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); @@ -1234,7 +1259,9 @@ contract PufferProtocolTest is UnitTestHelper { // Exchange rate remained unchanged, 1 wei diff (rounding) assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); - pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); + pufferProtocol.batchHandleWithdrawals( + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + ); // Validation time is unchanged assertEq( @@ -1302,7 +1329,9 @@ contract PufferProtocolTest is UnitTestHelper { // Exchange rate remained unchanged, 1 wei diff (rounding) assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); - pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); + pufferProtocol.batchHandleWithdrawals( + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + ); // Nothing is changed, we didn't deposit revenue assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); @@ -1408,7 +1437,8 @@ contract PufferProtocolTest is UnitTestHelper { vm.stopPrank(); // this contract has the PAYMASTER role, so we need to stop the prank pufferProtocol.batchHandleWithdrawals({ validatorInfos: stopInfos, - guardianEOASignatures: _getHandleBatchWithdrawalMessage(stopInfos) + guardianEOASignatures: _getHandleBatchWithdrawalMessage(stopInfos), + deadline: block.timestamp + 1 days }); } @@ -1762,7 +1792,9 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidationTimeDeposited({ node: address(this), ethAmount: 7.5 ether }); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); - pufferProtocol.registerValidatorKey{ value: 9 ether }(data, PUFFER_MODULE_0, 0, new bytes[](0)); + pufferProtocol.registerValidatorKey{ value: 9 ether }( + data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); // Protocol holds 7.5 ETHER assertEq(address(pufferProtocol).balance, 7.5 ether, "7.5 ETH in the protocol"); @@ -1822,7 +1854,7 @@ contract PufferProtocolTest is UnitTestHelper { view returns (bytes[] memory) { - uint256 nonce = pufferProtocol.nonces(node); + uint256 nonce = pufferProtocol.nonces(IPufferProtocol.registerValidatorKey.selector, node); bytes32 digest = keccak256(abi.encode(node, validatedEpochsTotal, nonce)); @@ -1848,7 +1880,7 @@ contract PufferProtocolTest is UnitTestHelper { view returns (bytes[] memory) { - bytes32 digest = LibGuardianMessages._getHandleBatchWithdrawalMessage(validatorInfos); + bytes32 digest = LibGuardianMessages._getHandleBatchWithdrawalMessage(validatorInfos, block.timestamp + 1 days); (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SK, digest); bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. @@ -1951,7 +1983,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: amount }( - validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures + validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures, block.timestamp + 1 days ); } @@ -2011,7 +2043,9 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory data = _getMockValidatorKeyData(invalidPubKey, PUFFER_MODULE_0); vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); - pufferProtocol.registerValidatorKey{ value: 3 ether }(data, PUFFER_MODULE_0, 0, new bytes[](0)); + pufferProtocol.registerValidatorKey{ value: 3 ether }( + data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); } function test_changeMinimumVTAmount_invalid_amount() public { @@ -2040,7 +2074,9 @@ contract PufferProtocolTest is UnitTestHelper { // Panic Error is expected panic: arithmetic underflow or overflow (0x11) vm.expectRevert(bytes("panic: arithmetic underflow or overflow (0x11)")); - pufferProtocol.batchHandleWithdrawals(validatorInfos, _getHandleBatchWithdrawalMessage(validatorInfos)); + pufferProtocol.batchHandleWithdrawals( + validatorInfos, _getHandleBatchWithdrawalMessage(validatorInfos), block.timestamp + 1 days + ); } function test_useVTOrValidationTime_edge_cases() public { From 899b73eab108d29422ddb4e762fcfc11b80048e6 Mon Sep 17 00:00:00 2001 From: eladio Date: Wed, 2 Jul 2025 13:38:22 +0200 Subject: [PATCH 52/82] Refactors to fix stack too deep --- mainnet-contracts/src/PufferProtocol.sol | 37 ++++++++++++++---------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 71766e08..ea9111fe 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -92,6 +92,13 @@ contract PufferProtocol is */ uint256 internal constant _32_ETH_GWEI = 32 * 10 ** 9; + bytes32 internal constant _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY = IPufferProtocol.registerValidatorKey.selector; + bytes32 internal constant _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME = + IPufferProtocol.depositValidationTime.selector; + bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = IPufferProtocol.requestWithdrawal.selector; + bytes32 internal constant _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS = + IPufferProtocol.batchHandleWithdrawals.selector; + /** * @inheritdoc IPufferProtocol */ @@ -204,7 +211,7 @@ contract PufferProtocol is ProtocolStorage storage $ = _getPufferProtocolStorage(); - epochsValidatedSignature.functionSelector = IPufferProtocol.depositValidationTime.selector; + epochsValidatedSignature.functionSelector = _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME; uint256 burnAmount = _useVTOrValidationTime({ $: $, epochsValidatedSignature: epochsValidatedSignature }); @@ -301,19 +308,21 @@ contract PufferProtocol is emit ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - _VALIDATOR_BOND) }); + uint8 numBatches = data.numBatches; + _settleVTAccounting({ $: $, epochsValidatedSignature: EpochsValidatedSignature({ nodeOperator: msg.sender, totalEpochsValidated: totalEpochsValidated, - functionSelector: IPufferProtocol.registerValidatorKey.selector, + functionSelector: _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY, deadline: deadline, signatures: vtConsumptionSignature }), deprecated_burntVTs: 0 }); - uint256 bondAmountEth = _VALIDATOR_BOND * data.numBatches; + uint256 bondAmountEth = _VALIDATOR_BOND * numBatches; // The bond is converted to pufETH at the current exchange rate uint256 pufETHBondAmount = PUFFER_VAULT.depositETH{ value: bondAmountEth }(address(this)); @@ -327,7 +336,7 @@ contract PufferProtocol is module: address($.modules[moduleName]), bond: uint96(pufETHBondAmount), node: msg.sender, - numBatches: data.numBatches + numBatches: numBatches }); // Increment indices for this module and number of validators registered @@ -347,7 +356,7 @@ contract PufferProtocol is pubKey: data.blsPubKey, pufferModuleIndex: pufferModuleIndex, moduleName: moduleName, - numBatches: data.numBatches + numBatches: numBatches }); } @@ -453,12 +462,12 @@ contract PufferProtocol is ProtocolStorage storage $ = _getPufferProtocolStorage(); bytes[] memory pubkeys = new bytes[](indices.length); - bytes32 functionSelector = IPufferProtocol.requestWithdrawal.selector; // validate pubkeys belong to that node and are active for (uint256 i = 0; i < indices.length; ++i) { - require($.validators[moduleName][indices[i]].node == msg.sender, InvalidValidator()); - pubkeys[i] = $.validators[moduleName][indices[i]].pubKey; + Validator memory validator = $.validators[moduleName][indices[i]]; + require(validator.node == msg.sender, InvalidValidator()); + pubkeys[i] = validator.pubKey; if (withdrawalType[i] == WithdrawalType.EXIT_VALIDATOR) { require(gweiAmounts[i] == 0, InvalidWithdrawAmount()); @@ -466,15 +475,14 @@ contract PufferProtocol is if (withdrawalType[i] == WithdrawalType.DOWNSIZE) { uint256 batches = gweiAmounts[i] / _32_ETH_GWEI; require( - batches > $.validators[moduleName][indices[i]].numBatches && gweiAmounts[i] % _32_ETH_GWEI == 0, - InvalidWithdrawAmount() + batches > validator.numBatches && gweiAmounts[i] % _32_ETH_GWEI == 0, InvalidWithdrawAmount() ); } // If downsize or rewards withdrawal, backend needs to validate the amount bytes32 messageHash = keccak256( abi.encode( - msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(functionSelector, msg.sender), deadline + msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(_FUNCTION_SELECTOR_REQUEST_WITHDRAWAL, msg.sender), deadline ) ); @@ -539,8 +547,6 @@ contract PufferProtocol is // 1 batch = 32 ETH uint256 numExitedBatches; - bytes32 functionSelector = IPufferProtocol.batchHandleWithdrawals.selector; - // slither-disable-start calls-loop for (uint256 i = 0; i < validatorInfos.length; ++i) { Validator storage validator = @@ -557,14 +563,15 @@ contract PufferProtocol is // We need to scope the variables to avoid stack too deep errors { uint256 epochValidated = validatorInfos[i].totalEpochsValidated; + bytes[] memory vtConsumptionSignature = validatorInfos[i].vtConsumptionSignature; burnAmounts.vt += _useVTOrValidationTime( $, EpochsValidatedSignature({ nodeOperator: bondWithdrawals[i].node, totalEpochsValidated: epochValidated, - functionSelector: functionSelector, + functionSelector: _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS, deadline: deadline, - signatures: validatorInfos[i].vtConsumptionSignature + signatures: vtConsumptionSignature }) ); } From f392b1bd25b24909c75c9cb299026f19de5e0fa4 Mon Sep 17 00:00:00 2001 From: eladio Date: Wed, 2 Jul 2025 13:38:42 +0200 Subject: [PATCH 53/82] fmt --- mainnet-contracts/src/PufferProtocol.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index ea9111fe..ff1664c1 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -482,7 +482,11 @@ contract PufferProtocol is // If downsize or rewards withdrawal, backend needs to validate the amount bytes32 messageHash = keccak256( abi.encode( - msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(_FUNCTION_SELECTOR_REQUEST_WITHDRAWAL, msg.sender), deadline + msg.sender, + pubkeys[i], + gweiAmounts[i], + _useNonce(_FUNCTION_SELECTOR_REQUEST_WITHDRAWAL, msg.sender), + deadline ) ); From a71569ffa768d5063366ea2be7c88f51d41cdbc9 Mon Sep 17 00:00:00 2001 From: eladio Date: Wed, 2 Jul 2025 18:32:44 +0200 Subject: [PATCH 54/82] Added min and max for depositVT. Added numBatches when needed --- mainnet-contracts/src/PufferProtocol.sol | 43 +++++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index ff1664c1..608e2bf0 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -72,10 +72,22 @@ contract PufferProtocol is uint256 internal constant _VALIDATOR_BOND = 1.5 ether; /** - * @dev Minimum validation time in epochs + * @dev Minimum validation time in epochs (per batch number) * Roughly: 30 days * 225 epochs per day = 6750 epochs */ - uint256 internal constant _MINIMUM_EPOCHS_VALIDATION = 6750; + uint256 internal constant _MINIMUM_EPOCHS_VALIDATION_REGISTRATION = 6750; + + /** + * @dev Minimum validation time in epochs (per batch number) + * Roughly: 5 days * 225 epochs per day = 1125 epochs + */ + uint256 internal constant _MINIMUM_EPOCHS_VALIDATION_DEPOSIT = 1125; + + /** + * @dev Maximum validation time in epochs (per batch number) + * Roughly: 180 days * 225 epochs per day = 40500 epochs + */ + uint256 internal constant _MAXIMUM_EPOCHS_VALIDATION_DEPOSIT = 40500; /** * @dev Number of epochs per day @@ -207,9 +219,14 @@ contract PufferProtocol is } require(epochsValidatedSignature.nodeOperator != address(0), InvalidAddress()); - require(msg.value > 0, InvalidETHAmount()); - ProtocolStorage storage $ = _getPufferProtocolStorage(); + uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); + uint8 operatorNumBatches = $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].numBatches; + require( + msg.value >= operatorNumBatches * _MINIMUM_EPOCHS_VALIDATION_DEPOSIT * epochCurrentPrice + && msg.value <= operatorNumBatches * _MAXIMUM_EPOCHS_VALIDATION_DEPOSIT * epochCurrentPrice, + InvalidETHAmount() + ); epochsValidatedSignature.functionSelector = _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME; @@ -299,16 +316,17 @@ contract PufferProtocol is _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); + uint8 numBatches = data.numBatches; + uint256 bondAmountEth = _VALIDATOR_BOND * numBatches; - // The node operator must deposit 1.5 ETH or more + minimum validation time for ~30 days + // The node operator must deposit 1.5 ETH (per batch) or more + minimum validation time for ~30 days // At the moment that's roughly 30 days * 225 (there is roughly 225 epochs per day) - uint256 minimumETHRequired = _VALIDATOR_BOND + (_MINIMUM_EPOCHS_VALIDATION * epochCurrentPrice); + uint256 minimumETHRequired = + bondAmountEth + (numBatches * _MINIMUM_EPOCHS_VALIDATION_REGISTRATION * epochCurrentPrice); require(msg.value >= minimumETHRequired, InvalidETHAmount()); - emit ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - _VALIDATOR_BOND) }); - - uint8 numBatches = data.numBatches; + emit ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - bondAmountEth) }); _settleVTAccounting({ $: $, @@ -322,8 +340,6 @@ contract PufferProtocol is deprecated_burntVTs: 0 }); - uint256 bondAmountEth = _VALIDATOR_BOND * numBatches; - // The bond is converted to pufETH at the current exchange rate uint256 pufETHBondAmount = PUFFER_VAULT.depositETH{ value: bondAmountEth }(address(this)); @@ -342,7 +358,7 @@ contract PufferProtocol is // Increment indices for this module and number of validators registered unchecked { $.nodeOperatorInfo[msg.sender].epochPrice = epochCurrentPrice; - $.nodeOperatorInfo[msg.sender].validationTime += (msg.value - _VALIDATOR_BOND); + $.nodeOperatorInfo[msg.sender].validationTime += (msg.value - bondAmountEth); ++$.nodeOperatorInfo[msg.sender].pendingValidatorCount; ++$.pendingValidatorIndices[moduleName]; ++$.moduleLimits[moduleName].numberOfRegisteredValidators; @@ -634,7 +650,8 @@ contract PufferProtocol is uint256 vtPricePerEpoch = PUFFER_ORACLE.getValidatorTicketPrice(); - $.nodeOperatorInfo[node].validationTime -= ($.vtPenaltyEpochs * vtPricePerEpoch); + $.nodeOperatorInfo[node].validationTime -= + ($.vtPenaltyEpochs * vtPricePerEpoch * $.validators[moduleName][skippedIndex].numBatches); --$.nodeOperatorInfo[node].pendingValidatorCount; // Change the status of that validator From 86afa4bb2115c3e785e44faa6599dc9f25cbc0be Mon Sep 17 00:00:00 2001 From: eladio Date: Thu, 3 Jul 2025 12:14:53 +0200 Subject: [PATCH 55/82] Removed unused function --- .../src/ProtocolSignatureNonces.sol | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/mainnet-contracts/src/ProtocolSignatureNonces.sol b/mainnet-contracts/src/ProtocolSignatureNonces.sol index 987f46ea..401f3740 100644 --- a/mainnet-contracts/src/ProtocolSignatureNonces.sol +++ b/mainnet-contracts/src/ProtocolSignatureNonces.sol @@ -78,23 +78,4 @@ abstract contract ProtocolSignatureNonces { return $._nonces[selector][owner]++; } } - - /** - * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. - * @param selector The function selector that determines the nonce space - * @param owner The address whose nonce to validate and consume - * @param nonce The expected nonce value - * - * @dev This function validates that the provided nonce matches the expected - * current nonce before consuming it. This prevents replay attacks and - * ensures proper signature ordering. - * - * @dev Reverts with InvalidAccountNonce if the nonce doesn't match. - */ - function _useCheckedNonce(bytes32 selector, address owner, uint256 nonce) internal virtual { - uint256 current = _useNonce(selector, owner); - if (nonce != current) { - revert InvalidAccountNonce(selector, owner, current); - } - } } From 202c7f115b4a9a930bb2938abe3f3609d87b5cdc Mon Sep 17 00:00:00 2001 From: eladio Date: Mon, 7 Jul 2025 12:09:57 +0200 Subject: [PATCH 56/82] Created PufferLogic contract and adapted existing ones (WIP) --- mainnet-contracts/script/DeployPuffer.s.sol | 5 +- mainnet-contracts/src/ProtocolConstants.sol | 152 +++++++++++++++ mainnet-contracts/src/PufferProtocol.sol | 173 ++++++------------ mainnet-contracts/src/PufferProtocolLogic.sol | 59 ++++++ .../src/interface/IPufferProtocol.sol | 99 +--------- .../src/interface/IPufferProtocolLogic.sol | 7 + .../src/struct/ProtocolStorage.sol | 6 + .../test/unit/PufferProtocol.t.sol | 29 +-- 8 files changed, 307 insertions(+), 223 deletions(-) create mode 100644 mainnet-contracts/src/ProtocolConstants.sol create mode 100644 mainnet-contracts/src/PufferProtocolLogic.sol create mode 100644 mainnet-contracts/src/interface/IPufferProtocolLogic.sol diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index c534db05..13cd6785 100644 --- a/mainnet-contracts/script/DeployPuffer.s.sol +++ b/mainnet-contracts/script/DeployPuffer.s.sol @@ -30,6 +30,7 @@ import { RewardsCoordinatorMock } from "../test/mocks/RewardsCoordinatorMock.sol import { EigenAllocationManagerMock } from "../test/mocks/EigenAllocationManagerMock.sol"; import { RestakingOperatorController } from "../src/RestakingOperatorController.sol"; import { RestakingOperatorController } from "../src/RestakingOperatorController.sol"; +import { PufferProtocolLogic } from "../src/PufferProtocolLogic.sol"; /** * @title DeployPuffer * @author Puffer Finance @@ -181,8 +182,10 @@ contract DeployPuffer is BaseScript { address(moduleManager), abi.encodeCall(moduleManager.initialize, (address(accessManager))) ); + PufferProtocolLogic pufferProtocolLogic = new PufferProtocolLogic(); + // Initialize the Pool - pufferProtocol.initialize({ accessManager: address(accessManager) }); + pufferProtocol.initialize({ accessManager: address(accessManager), pufferProtocolLogic: address(pufferProtocolLogic) }); vm.label(address(accessManager), "AccessManager"); vm.label(address(operationsCoordinator), "OperationsCoordinator"); diff --git a/mainnet-contracts/src/ProtocolConstants.sol b/mainnet-contracts/src/ProtocolConstants.sol new file mode 100644 index 00000000..12b39f2e --- /dev/null +++ b/mainnet-contracts/src/ProtocolConstants.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; +import { Status } from "./struct/Status.sol"; + +abstract contract ProtocolConstants { + /** + * @notice Thrown when the deposit state that is provided doesn't match the one on Beacon deposit contract + */ + error InvalidDepositRootHash(); + + /** + * @notice Thrown when the node operator tries to withdraw VTs from the PufferProtocol but has active/pending validators + * @dev Signature "0x22242546" + */ + error ActiveOrPendingValidatorsExist(); + + /** + * @notice Thrown on the module creation if the module already exists + * @dev Signature "0x2157f2d7" + */ + error ModuleAlreadyExists(); + + /** + * @notice Thrown when the new validators tires to register to a module, but the validator limit for that module is already reached + * @dev Signature "0xb75c5781" + */ + error ValidatorLimitForModuleReached(); + + /** + * @notice Thrown when the BLS public key is not valid + * @dev Signature "0x7eef7967" + */ + error InvalidBLSPubKey(); + + /** + * @notice Thrown when validator is not in a valid state + * @dev Signature "0x3001591c" + */ + error InvalidValidatorState(Status status); + + /** + * @notice Thrown if the sender did not send enough ETH in the transaction + * @dev Signature "0x242b035c" + */ + error InvalidETHAmount(); + + /** + * @notice Thrown if the sender tries to register validator with invalid VT amount + * @dev Signature "0x95c01f62" + */ + error InvalidVTAmount(); + + /** + * @notice Thrown if the ETH transfer from the PufferModule to the PufferVault fails + * @dev Signature "0x625a40e6" + */ + error Failed(); + + /** + * @notice Thrown if the validator is not valid + * @dev Signature "0x682a6e7c" + */ + error InvalidValidator(); + + /** + * @notice Thrown if the input array length mismatch + * @dev Signature "0x43714afd" + */ + error InputArrayLengthMismatch(); + + /** + * @notice Thrown if the input array length is zero + * @dev Signature "0x796cc525" + */ + error InputArrayLengthZero(); + + /** + * @notice Thrown if the number of batches is 0 or greater than 64 + * @dev Signature "0x4ea54df9" + */ + error InvalidNumberOfBatches(); + + /** + * @notice Thrown if the withdrawal amount is invalid + * @dev Signature "0xdb73cdf0" + */ + error InvalidWithdrawAmount(); + + /** + * @notice Thrown when the total epochs validated is invalid + * @dev Signature "0x1af51909" + */ + error InvalidTotalEpochsValidated(); + + /** + * @notice Thrown when the deadline is exceeded + * @dev Signature "0xddff8620" + */ + error DeadlineExceeded(); + + /** + * @dev BLS public keys are 48 bytes long + */ + uint256 internal constant _BLS_PUB_KEY_LENGTH = 48; + + /** + * @dev ETH Amount required to be deposited as a bond + */ + uint256 internal constant _VALIDATOR_BOND = 1.5 ether; + + /** + * @dev Minimum validation time in epochs (per batch number) + * Roughly: 30 days * 225 epochs per day = 6750 epochs + */ + uint256 internal constant _MINIMUM_EPOCHS_VALIDATION_REGISTRATION = 6750; + + /** + * @dev Minimum validation time in epochs (per batch number) + * Roughly: 5 days * 225 epochs per day = 1125 epochs + */ + uint256 internal constant _MINIMUM_EPOCHS_VALIDATION_DEPOSIT = 1125; + + /** + * @dev Maximum validation time in epochs (per batch number) + * Roughly: 180 days * 225 epochs per day = 40500 epochs + */ + uint256 internal constant _MAXIMUM_EPOCHS_VALIDATION_DEPOSIT = 40500; + + /** + * @dev Number of epochs per day + */ + uint256 internal constant _EPOCHS_PER_DAY = 225; + + /** + * @dev Default "PUFFER_MODULE_0" module + */ + bytes32 internal constant _PUFFER_MODULE_0 = bytes32("PUFFER_MODULE_0"); + + /** + * @dev 32 ETH in Gwei + */ + uint256 internal constant _32_ETH_GWEI = 32 * 10 ** 9; + + bytes32 internal constant _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY = IPufferProtocol.registerValidatorKey.selector; + bytes32 internal constant _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME = + IPufferProtocol.depositValidationTime.selector; + bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = IPufferProtocol.requestWithdrawal.selector; + bytes32 internal constant _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS = + IPufferProtocol.batchHandleWithdrawals.selector; +} \ No newline at end of file diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 608e2bf0..4f49099d 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -27,6 +27,8 @@ import { PufferModule } from "./PufferModule.sol"; import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { ProtocolConstants } from "./ProtocolConstants.sol"; +import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; /** * @title PufferProtocol @@ -40,7 +42,8 @@ contract PufferProtocol is AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolStorage, - ProtocolSignatureNonces + ProtocolSignatureNonces, + ProtocolConstants { /** * @dev Helper struct for the full withdrawals accounting @@ -61,55 +64,7 @@ contract PufferProtocol is uint256 numBatches; } - /** - * @dev BLS public keys are 48 bytes long - */ - uint256 internal constant _BLS_PUB_KEY_LENGTH = 48; - - /** - * @dev ETH Amount required to be deposited as a bond - */ - uint256 internal constant _VALIDATOR_BOND = 1.5 ether; - - /** - * @dev Minimum validation time in epochs (per batch number) - * Roughly: 30 days * 225 epochs per day = 6750 epochs - */ - uint256 internal constant _MINIMUM_EPOCHS_VALIDATION_REGISTRATION = 6750; - - /** - * @dev Minimum validation time in epochs (per batch number) - * Roughly: 5 days * 225 epochs per day = 1125 epochs - */ - uint256 internal constant _MINIMUM_EPOCHS_VALIDATION_DEPOSIT = 1125; - /** - * @dev Maximum validation time in epochs (per batch number) - * Roughly: 180 days * 225 epochs per day = 40500 epochs - */ - uint256 internal constant _MAXIMUM_EPOCHS_VALIDATION_DEPOSIT = 40500; - - /** - * @dev Number of epochs per day - */ - uint256 internal constant _EPOCHS_PER_DAY = 225; - - /** - * @dev Default "PUFFER_MODULE_0" module - */ - bytes32 internal constant _PUFFER_MODULE_0 = bytes32("PUFFER_MODULE_0"); - - /** - * @dev 32 ETH in Gwei - */ - uint256 internal constant _32_ETH_GWEI = 32 * 10 ** 9; - - bytes32 internal constant _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY = IPufferProtocol.registerValidatorKey.selector; - bytes32 internal constant _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME = - IPufferProtocol.depositValidationTime.selector; - bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = IPufferProtocol.requestWithdrawal.selector; - bytes32 internal constant _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS = - IPufferProtocol.batchHandleWithdrawals.selector; /** * @inheritdoc IPufferProtocol @@ -171,7 +126,7 @@ contract PufferProtocol is /** * @notice Initializes the contract */ - function initialize(address accessManager) external initializer { + function initialize(address accessManager, address pufferProtocolLogic) external initializer { if (address(accessManager) == address(0)) { revert InvalidAddress(); } @@ -179,6 +134,7 @@ contract PufferProtocol is _createPufferModule(_PUFFER_MODULE_0); _changeMinimumVTAmount(30 * _EPOCHS_PER_DAY); // 30 days worth of ETH is the minimum VT amount _setVTPenalty(10 * _EPOCHS_PER_DAY); // 10 days worth of ETH is the VT penalty + _setPufferProtocolLogic(pufferProtocolLogic); } /** @@ -424,39 +380,11 @@ contract PufferProtocol is payable restricted { - if (srcIndices.length == 0) { - revert InputArrayLengthZero(); + bytes memory callData = abi.encodeWithSelector(IPufferProtocolLogic._requestConsolidation.selector, moduleName, srcIndices, targetIndices); + (bool success,) = address(this).delegatecall(callData); + if (!success) { + revert Failed(); } - if (srcIndices.length != targetIndices.length) { - revert InputArrayLengthMismatch(); - } - - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - bytes[] memory srcPubkeys = new bytes[](srcIndices.length); - bytes[] memory targetPubkeys = new bytes[](targetIndices.length); - Validator storage validatorSrc; - Validator storage validatorTarget; - for (uint256 i = 0; i < srcPubkeys.length; i++) { - require(srcIndices[i] != targetIndices[i], InvalidValidator()); - validatorSrc = $.validators[moduleName][srcIndices[i]]; - require(validatorSrc.node == msg.sender && validatorSrc.status == Status.ACTIVE, InvalidValidator()); - srcPubkeys[i] = validatorSrc.pubKey; - validatorTarget = $.validators[moduleName][targetIndices[i]]; - require(validatorTarget.node == msg.sender && validatorTarget.status == Status.ACTIVE, InvalidValidator()); - targetPubkeys[i] = validatorTarget.pubKey; - - // Update accounting - validatorTarget.bond += validatorSrc.bond; - validatorTarget.numBatches += validatorSrc.numBatches; - - delete $.validators[moduleName][srcIndices[i]]; - // Node info needs no update since all stays in the same node operator - } - - $.modules[moduleName].requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); - - emit ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); } /** @@ -703,6 +631,13 @@ contract PufferProtocol is _setVTPenalty(newPenaltyAmount); } + /** + * @dev Restricted to the DAO + */ + function setPufferProtocolLogic(address newPufferProtocolLogic) external restricted { + _setPufferProtocolLogic(newPufferProtocolLogic); + } + /** * @inheritdoc IPufferProtocol */ @@ -873,39 +808,6 @@ contract PufferProtocol is */ function revertIfPaused() external restricted { } - function _storeValidatorInformation( - ProtocolStorage storage $, - ValidatorKeyData calldata data, - uint256 pufETHAmount, - bytes32 moduleName, - uint256 vtAmount - ) internal { - uint256 pufferModuleIndex = $.pendingValidatorIndices[moduleName]; - - address moduleAddress = address($.modules[moduleName]); - - // No need for SafeCast - $.validators[moduleName][pufferModuleIndex] = Validator({ - pubKey: data.blsPubKey, - status: Status.PENDING, - module: moduleAddress, - bond: uint96(pufETHAmount), - node: msg.sender, - numBatches: data.numBatches - }); - - $.nodeOperatorInfo[msg.sender].deprecated_vtBalance += SafeCast.toUint96(vtAmount); - - // Increment indices for this module and number of validators registered - unchecked { - ++$.nodeOperatorInfo[msg.sender].pendingValidatorCount; - ++$.pendingValidatorIndices[moduleName]; - ++$.moduleLimits[moduleName].numberOfRegisteredValidators; - } - emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); - emit ValidatorKeyRegistered(data.blsPubKey, pufferModuleIndex, moduleName, data.numBatches); - } - function _setValidatorLimitPerModule(bytes32 moduleName, uint128 limit) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); if (limit < $.moduleLimits[moduleName].numberOfRegisteredValidators) { @@ -1261,5 +1163,46 @@ contract PufferProtocol is return (bondBurnAmount, bondAmount - bondBurnAmount, numBatches); } + function _delegatecall(address _implementation) internal { + // Use assembly for delegatecall to forward msg.sender, msg.value, and calldata + assembly { + // Copy calldata from msg.data (starts at 0x04) + let ptr := mload(0x40) // Get next free memory pointer + calldatacopy(ptr, 0, calldatasize()) // Copy all calldata + + // Perform delegatecall + let success := delegatecall( + gas(), // Forward all available gas + _implementation, // Address of the logic contract + ptr, // Pointer to the calldata + calldatasize(), // Size of the calldata + 0, // Output offset (we'll copy output later) + 0 // Output size (we'll copy output later) + ) + + // Get the size of the returned data + let returndata_size := returndatasize() + // Allocate memory for the return data + let returndata_ptr := mload(0x40) // Get another free memory pointer + // Copy the returned data to memory + returndatacopy(returndata_ptr, 0, returndata_size) + + // Revert or return based on delegatecall success + switch success + case 0 { + revert(returndata_ptr, returndata_size) // Revert with error message + } + default { + return(returndata_ptr, returndata_size) // Return data + } + } + } + + function _setPufferProtocolLogic(address newPufferProtocolLogic) internal { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + emit PufferProtocolLogicSet($.pufferProtocolLogic, newPufferProtocolLogic); + $.pufferProtocolLogic = newPufferProtocolLogic; + } + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } } diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol new file mode 100644 index 00000000..d3eaea10 --- /dev/null +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { PufferProtocolStorage } from "./PufferProtocolStorage.sol"; +import { ProtocolStorage } from "./struct/ProtocolStorage.sol"; +import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; +import { Validator } from "./struct/Validator.sol"; +import { Status } from "./struct/Validator.sol"; +import { ProtocolConstants } from "./ProtocolConstants.sol"; +import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; + +contract PufferProtocolLogic is + PufferProtocolStorage, + ProtocolSignatureNonces, + ProtocolConstants +{ + + /** + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ + function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) + external + payable + { + if (srcIndices.length == 0) { + revert InputArrayLengthZero(); + } + if (srcIndices.length != targetIndices.length) { + revert InputArrayLengthMismatch(); + } + + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + bytes[] memory srcPubkeys = new bytes[](srcIndices.length); + bytes[] memory targetPubkeys = new bytes[](targetIndices.length); + Validator storage validatorSrc; + Validator storage validatorTarget; + for (uint256 i = 0; i < srcPubkeys.length; i++) { + require(srcIndices[i] != targetIndices[i], InvalidValidator()); + validatorSrc = $.validators[moduleName][srcIndices[i]]; + require(validatorSrc.node == msg.sender && validatorSrc.status == Status.ACTIVE, InvalidValidator()); + srcPubkeys[i] = validatorSrc.pubKey; + validatorTarget = $.validators[moduleName][targetIndices[i]]; + require(validatorTarget.node == msg.sender && validatorTarget.status == Status.ACTIVE, InvalidValidator()); + targetPubkeys[i] = validatorTarget.pubKey; + + // Update accounting + validatorTarget.bond += validatorSrc.bond; + validatorTarget.numBatches += validatorSrc.numBatches; + + delete $.validators[moduleName][srcIndices[i]]; + // Node info needs no update since all stays in the same node operator + } + + $.modules[moduleName].requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); + + emit IPufferProtocol.ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); + } +} \ No newline at end of file diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 918dbab1..2535009e 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -23,100 +23,7 @@ import { IBeaconDepositContract } from "../interface/IBeaconDepositContract.sol" * @custom:security-contact security@puffer.fi */ interface IPufferProtocol { - /** - * @notice Thrown when the deposit state that is provided doesn't match the one on Beacon deposit contract - */ - error InvalidDepositRootHash(); - /** - * @notice Thrown when the node operator tries to withdraw VTs from the PufferProtocol but has active/pending validators - * @dev Signature "0x22242546" - */ - error ActiveOrPendingValidatorsExist(); - - /** - * @notice Thrown on the module creation if the module already exists - * @dev Signature "0x2157f2d7" - */ - error ModuleAlreadyExists(); - - /** - * @notice Thrown when the new validators tires to register to a module, but the validator limit for that module is already reached - * @dev Signature "0xb75c5781" - */ - error ValidatorLimitForModuleReached(); - - /** - * @notice Thrown when the BLS public key is not valid - * @dev Signature "0x7eef7967" - */ - error InvalidBLSPubKey(); - - /** - * @notice Thrown when validator is not in a valid state - * @dev Signature "0x3001591c" - */ - error InvalidValidatorState(Status status); - - /** - * @notice Thrown if the sender did not send enough ETH in the transaction - * @dev Signature "0x242b035c" - */ - error InvalidETHAmount(); - - /** - * @notice Thrown if the sender tries to register validator with invalid VT amount - * @dev Signature "0x95c01f62" - */ - error InvalidVTAmount(); - - /** - * @notice Thrown if the ETH transfer from the PufferModule to the PufferVault fails - * @dev Signature "0x625a40e6" - */ - error Failed(); - - /** - * @notice Thrown if the validator is not valid - * @dev Signature "0x682a6e7c" - */ - error InvalidValidator(); - - /** - * @notice Thrown if the input array length mismatch - * @dev Signature "0x43714afd" - */ - error InputArrayLengthMismatch(); - - /** - * @notice Thrown if the input array length is zero - * @dev Signature "0x796cc525" - */ - error InputArrayLengthZero(); - - /** - * @notice Thrown if the number of batches is 0 or greater than 64 - * @dev Signature "0x4ea54df9" - */ - error InvalidNumberOfBatches(); - - /** - * @notice Thrown if the withdrawal amount is invalid - * @dev Signature "0xdb73cdf0" - */ - error InvalidWithdrawAmount(); - - /** - * @notice Thrown when the total epochs validated is invalid - * @dev Signature "0x1af51909" - */ - error InvalidTotalEpochsValidated(); - - /** - * @notice Thrown when the deadline is exceeded - * @dev Signature "0xddff8620" - */ - error DeadlineExceeded(); /** * @notice Emitted when the number of active validators changes @@ -264,6 +171,12 @@ interface IPufferProtocol { bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint256 numBatches ); + /** + * @notice Emitted when the PufferProtocolLogic is set + * @dev Signature "0xe271f36954242c619ce9d0f727a7d3b5f4db04666752aaeb20bca6d52098792a" + */ + event PufferProtocolLogicSet(address oldPufferProtocolLogic, address newPufferProtocolLogic); + /** * @notice Returns validator information * @param moduleName is the staking Module diff --git a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol new file mode 100644 index 00000000..8fabd74a --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + + +interface IPufferProtocolLogic { + function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable; +} \ No newline at end of file diff --git a/mainnet-contracts/src/struct/ProtocolStorage.sol b/mainnet-contracts/src/struct/ProtocolStorage.sol index 97815ca8..a30cf4cd 100644 --- a/mainnet-contracts/src/struct/ProtocolStorage.sol +++ b/mainnet-contracts/src/struct/ProtocolStorage.sol @@ -66,6 +66,12 @@ struct ProtocolStorage { * Slot 9 */ uint256 vtPenaltyEpochs; + + /** + * @dev Address of the PufferProtocolLogic contract + * Slot 10 + */ + address pufferProtocolLogic; } struct ModuleLimit { diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 92d823cc..3125648e 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -9,6 +9,7 @@ import { ValidatorKeyData } from "../../src/struct/ValidatorKeyData.sol"; import { Status } from "../../src/struct/Status.sol"; import { Validator } from "../../src/struct/Validator.sol"; import { PufferProtocol } from "../../src/PufferProtocol.sol"; +import { ProtocolConstants } from "../../src/ProtocolConstants.sol"; import { PufferModule } from "../../src/PufferModule.sol"; import { PufferRevenueDepositor } from "../../src/PufferRevenueDepositor.sol"; import { @@ -186,7 +187,7 @@ contract PufferProtocolTest is UnitTestHelper { // Create an existing module should revert function test_create_existing_module_fails() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.ModuleAlreadyExists.selector); + vm.expectRevert(ProtocolConstants.ModuleAlreadyExists.selector); pufferProtocol.createPufferModule(PUFFER_MODULE_0); } @@ -195,7 +196,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 smoothingCommitment = pufferOracle.getValidatorTicketPrice() * 30; bytes memory pubKey = _getPubKey(bytes32("charlie")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - vm.expectRevert(IPufferProtocol.ValidatorLimitForModuleReached.selector); + vm.expectRevert(ProtocolConstants.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( validatorKeyData, bytes32("imaginary module"), 0, new bytes[](0), block.timestamp + 1 days ); @@ -246,14 +247,14 @@ contract PufferProtocolTest is UnitTestHelper { numBatches: 0 }); - vm.expectRevert(IPufferProtocol.InvalidNumberOfBatches.selector); + vm.expectRevert(ProtocolConstants.InvalidNumberOfBatches.selector); pufferProtocol.registerValidatorKey{ value: vtPrice }( validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); validatorData.numBatches = 65; - vm.expectRevert(IPufferProtocol.InvalidNumberOfBatches.selector); + vm.expectRevert(ProtocolConstants.InvalidNumberOfBatches.selector); pufferProtocol.registerValidatorKey{ value: vtPrice }( validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -277,7 +278,7 @@ contract PufferProtocolTest is UnitTestHelper { numBatches: 1 }); - vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); + vm.expectRevert(ProtocolConstants.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -298,7 +299,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory validatorSignature = _validatorSignature(); - vm.expectRevert(IPufferProtocol.InvalidDepositRootHash.selector); + vm.expectRevert(ProtocolConstants.InvalidDepositRootHash.selector); pufferProtocol.provisionNode(validatorSignature, bytes32("badDepositRoot")); // "depositRoot" is hardcoded in the mock // now it works @@ -598,7 +599,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory pubKey = _getPubKey(bytes32("bob")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - vm.expectRevert(IPufferProtocol.ValidatorLimitForModuleReached.selector); + vm.expectRevert(ProtocolConstants.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + BOND) }( validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -812,7 +813,7 @@ contract PufferProtocolTest is UnitTestHelper { // Register Validator key registers validator with 30 VTs _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); - vm.expectRevert(IPufferProtocol.ActiveOrPendingValidatorsExist.selector); + vm.expectRevert(ProtocolConstants.ActiveOrPendingValidatorsExist.selector); pufferProtocol.withdrawValidatorTickets(30 ether, alice); } @@ -861,13 +862,13 @@ contract PufferProtocolTest is UnitTestHelper { function test_setVTPenalty_bigger_than_minimum_VT_amount() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); + vm.expectRevert(ProtocolConstants.InvalidVTAmount.selector); pufferProtocol.setVTPenalty(50 * EPOCHS_PER_DAY); } function test_changeMinimumVTAmount_lower_than_penalty() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); + vm.expectRevert(ProtocolConstants.InvalidVTAmount.selector); pufferProtocol.changeMinimumVTAmount(9 * EPOCHS_PER_DAY); } @@ -987,7 +988,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes[] memory vtConsumptionSignature = _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY); // We've removed the validator data, meaning the validator status is 0 (UNINITIALIZED) - vm.expectRevert(abi.encodeWithSelector(IPufferProtocol.InvalidValidatorState.selector, 0)); + vm.expectRevert(abi.encodeWithSelector(ProtocolConstants.InvalidValidatorState.selector, 0)); _executeFullWithdrawal( StoppedValidatorInfo({ module: NoRestakingModule, @@ -2034,7 +2035,7 @@ contract PufferProtocolTest is UnitTestHelper { function test_setVTPenalty_invalid_amount() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); + vm.expectRevert(ProtocolConstants.InvalidVTAmount.selector); pufferProtocol.setVTPenalty(type(uint256).max); } @@ -2042,7 +2043,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory invalidPubKey = new bytes(47); // Invalid length ValidatorKeyData memory data = _getMockValidatorKeyData(invalidPubKey, PUFFER_MODULE_0); - vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); + vm.expectRevert(ProtocolConstants.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: 3 ether }( data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -2050,7 +2051,7 @@ contract PufferProtocolTest is UnitTestHelper { function test_changeMinimumVTAmount_invalid_amount() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); + vm.expectRevert(ProtocolConstants.InvalidVTAmount.selector); pufferProtocol.changeMinimumVTAmount(0); } From 0b4a2b8312abb84852dfafac65ea9111c5e5d658 Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 7 Jul 2025 19:29:28 +0200 Subject: [PATCH 57/82] Adapted previous tests and improved signatures --- mainnet-contracts/script/DeployPuffer.s.sol | 5 +- mainnet-contracts/src/GuardianModule.sol | 35 ++- mainnet-contracts/src/LibGuardianMessages.sol | 35 ++- mainnet-contracts/src/ProtocolConstants.sol | 4 +- mainnet-contracts/src/PufferProtocol.sol | 66 +++--- mainnet-contracts/src/PufferProtocolLogic.sol | 9 +- .../src/interface/IGuardianModule.sol | 32 ++- .../src/interface/IPufferProtocol.sol | 2 - .../src/interface/IPufferProtocolLogic.sol | 7 +- .../src/struct/ProtocolStorage.sol | 1 - .../test/unit/PufferProtocol.t.sol | 218 ++++++++++++------ 11 files changed, 274 insertions(+), 140 deletions(-) diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index 13cd6785..3987dd66 100644 --- a/mainnet-contracts/script/DeployPuffer.s.sol +++ b/mainnet-contracts/script/DeployPuffer.s.sol @@ -185,7 +185,10 @@ contract DeployPuffer is BaseScript { PufferProtocolLogic pufferProtocolLogic = new PufferProtocolLogic(); // Initialize the Pool - pufferProtocol.initialize({ accessManager: address(accessManager), pufferProtocolLogic: address(pufferProtocolLogic) }); + pufferProtocol.initialize({ + accessManager: address(accessManager), + pufferProtocolLogic: address(pufferProtocolLogic) + }); vm.label(address(accessManager), "AccessManager"); vm.label(address(operationsCoordinator), "OperationsCoordinator"); diff --git a/mainnet-contracts/src/GuardianModule.sol b/mainnet-contracts/src/GuardianModule.sol index c0233a97..73511835 100644 --- a/mainnet-contracts/src/GuardianModule.sol +++ b/mainnet-contracts/src/GuardianModule.sol @@ -217,12 +217,22 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule */ - function validateWithdrawalRequest(bytes[] calldata eoaSignatures, bytes32 messageHash) external view { + function validateWithdrawalRequest( + address node, + bytes memory pubKey, + uint256 gweiAmount, + uint256 nonce, + uint256 deadline, + bytes[] calldata guardianEOASignatures + ) external view { // Recreate the message hash - bytes32 signedMessageHash = LibGuardianMessages._getAnyHashedMessage(messageHash); + bytes32 signedMessageHash = + LibGuardianMessages._getWithdrawalRequestMessage(node, pubKey, gweiAmount, nonce, deadline); - bool validSignatures = - validateGuardiansEOASignatures({ eoaSignatures: eoaSignatures, signedMessageHash: signedMessageHash }); + bool validSignatures = validateGuardiansEOASignatures({ + eoaSignatures: guardianEOASignatures, + signedMessageHash: signedMessageHash + }); if (!validSignatures) { revert Unauthorized(); @@ -232,12 +242,21 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule */ - function validateTotalEpochsValidated(bytes[] calldata eoaSignatures, bytes32 messageHash) external view { + function validateTotalEpochsValidated( + address node, + uint256 totalEpochsValidated, + uint256 nonce, + uint256 deadline, + bytes[] calldata guardianEOASignatures + ) external view { // Recreate the message hash - bytes32 signedMessageHash = LibGuardianMessages._getAnyHashedMessage(messageHash); + bytes32 signedMessageHash = + LibGuardianMessages._getTotalEpochsValidatedMessage(node, totalEpochsValidated, nonce, deadline); - bool validSignatures = - validateGuardiansEOASignatures({ eoaSignatures: eoaSignatures, signedMessageHash: signedMessageHash }); + bool validSignatures = validateGuardiansEOASignatures({ + eoaSignatures: guardianEOASignatures, + signedMessageHash: signedMessageHash + }); if (!validSignatures) { revert Unauthorized(); diff --git a/mainnet-contracts/src/LibGuardianMessages.sol b/mainnet-contracts/src/LibGuardianMessages.sol index 76112907..4bc85784 100644 --- a/mainnet-contracts/src/LibGuardianMessages.sol +++ b/mainnet-contracts/src/LibGuardianMessages.sol @@ -88,12 +88,39 @@ library LibGuardianMessages { } /** - * @notice Returns the message to be signed for any message - * @param hashedMessage is the hashed message to be signed + * @notice Returns the message to be signed for the total epochs validated + * @param node is the node operator address + * @param totalEpochsValidated is the total epochs validated + * @param nonce is the nonce for the node and the function selector + * @param deadline is the deadline of the signature * @return the message to be signed */ - function _getAnyHashedMessage(bytes32 hashedMessage) internal pure returns (bytes32) { - return hashedMessage.toEthSignedMessageHash(); + function _getTotalEpochsValidatedMessage( + address node, + uint256 totalEpochsValidated, + uint256 nonce, + uint256 deadline + ) internal pure returns (bytes32) { + return keccak256(abi.encode(node, totalEpochsValidated, nonce, deadline)).toEthSignedMessageHash(); + } + + /** + * @notice Returns the message to be signed for the withdrawal request + * @param node is the node operator address + * @param pubKey is the public key + * @param gweiAmount is the amount in gwei + * @param nonce is the nonce for the node and the function selector + * @param deadline is the deadline of the signature + * @return the message to be signed + */ + function _getWithdrawalRequestMessage( + address node, + bytes memory pubKey, + uint256 gweiAmount, + uint256 nonce, + uint256 deadline + ) internal pure returns (bytes32) { + return keccak256(abi.encode(node, pubKey, gweiAmount, nonce, deadline)).toEthSignedMessageHash(); } } /* solhint-disable func-named-parameters */ diff --git a/mainnet-contracts/src/ProtocolConstants.sol b/mainnet-contracts/src/ProtocolConstants.sol index 12b39f2e..ef6018cf 100644 --- a/mainnet-contracts/src/ProtocolConstants.sol +++ b/mainnet-contracts/src/ProtocolConstants.sol @@ -5,7 +5,7 @@ import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; import { Status } from "./struct/Status.sol"; abstract contract ProtocolConstants { - /** + /** * @notice Thrown when the deposit state that is provided doesn't match the one on Beacon deposit contract */ error InvalidDepositRootHash(); @@ -149,4 +149,4 @@ abstract contract ProtocolConstants { bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = IPufferProtocol.requestWithdrawal.selector; bytes32 internal constant _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS = IPufferProtocol.batchHandleWithdrawals.selector; -} \ No newline at end of file +} diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 4f49099d..9c00fdc5 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -64,8 +64,6 @@ contract PufferProtocol is uint256 numBatches; } - - /** * @inheritdoc IPufferProtocol */ @@ -380,7 +378,9 @@ contract PufferProtocol is payable restricted { - bytes memory callData = abi.encodeWithSelector(IPufferProtocolLogic._requestConsolidation.selector, moduleName, srcIndices, targetIndices); + bytes memory callData = abi.encodeWithSelector( + IPufferProtocolLogic._requestConsolidation.selector, moduleName, srcIndices, targetIndices + ); (bool success,) = address(this).delegatecall(callData); if (!success) { revert Failed(); @@ -424,19 +424,14 @@ contract PufferProtocol is } // If downsize or rewards withdrawal, backend needs to validate the amount - bytes32 messageHash = keccak256( - abi.encode( - msg.sender, - pubkeys[i], - gweiAmounts[i], - _useNonce(_FUNCTION_SELECTOR_REQUEST_WITHDRAWAL, msg.sender), - deadline - ) - ); GUARDIAN_MODULE.validateWithdrawalRequest({ - eoaSignatures: validatorAmountsSignatures[i], - messageHash: messageHash + node: msg.sender, + pubKey: pubkeys[i], + gweiAmount: gweiAmounts[i], + nonce: _useNonce(_FUNCTION_SELECTOR_REQUEST_WITHDRAWAL, msg.sender), + deadline: deadline, + guardianEOASignatures: validatorAmountsSignatures[i] }); } } @@ -1008,19 +1003,12 @@ contract PufferProtocol is return; } - // We have no way of getting the present consumed amount for the other validators on-chain, so we use Puffer Backend service to get that amount and a signature from the service - bytes32 messageHash = keccak256( - abi.encode( - node, - epochsValidatedSignature.totalEpochsValidated, - _useNonce(epochsValidatedSignature.functionSelector, node), - epochsValidatedSignature.deadline - ) - ); - GUARDIAN_MODULE.validateTotalEpochsValidated({ - eoaSignatures: epochsValidatedSignature.signatures, - messageHash: messageHash + node: node, + totalEpochsValidated: epochsValidatedSignature.totalEpochsValidated, + nonce: _useNonce(epochsValidatedSignature.functionSelector, node), + deadline: epochsValidatedSignature.deadline, + guardianEOASignatures: epochsValidatedSignature.signatures }); uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); @@ -1171,14 +1159,15 @@ contract PufferProtocol is calldatacopy(ptr, 0, calldatasize()) // Copy all calldata // Perform delegatecall - let success := delegatecall( - gas(), // Forward all available gas - _implementation, // Address of the logic contract - ptr, // Pointer to the calldata - calldatasize(), // Size of the calldata - 0, // Output offset (we'll copy output later) - 0 // Output size (we'll copy output later) - ) + let success := + delegatecall( + gas(), // Forward all available gas + _implementation, // Address of the logic contract + ptr, // Pointer to the calldata + calldatasize(), // Size of the calldata + 0, // Output offset (we'll copy output later) + 0 // Output size (we'll copy output later) + ) // Get the size of the returned data let returndata_size := returndatasize() @@ -1189,12 +1178,9 @@ contract PufferProtocol is // Revert or return based on delegatecall success switch success - case 0 { - revert(returndata_ptr, returndata_size) // Revert with error message - } - default { - return(returndata_ptr, returndata_size) // Return data - } + case 0 { revert(returndata_ptr, returndata_size) } + // Revert with error message + default { return(returndata_ptr, returndata_size) } // Return data } } diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index d3eaea10..608eaf95 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -9,12 +9,7 @@ import { Status } from "./struct/Validator.sol"; import { ProtocolConstants } from "./ProtocolConstants.sol"; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; -contract PufferProtocolLogic is - PufferProtocolStorage, - ProtocolSignatureNonces, - ProtocolConstants -{ - +contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, ProtocolConstants { /** * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ @@ -56,4 +51,4 @@ contract PufferProtocolLogic is emit IPufferProtocol.ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); } -} \ No newline at end of file +} diff --git a/mainnet-contracts/src/interface/IGuardianModule.sol b/mainnet-contracts/src/interface/IGuardianModule.sol index d68d1f5d..28e4549d 100644 --- a/mainnet-contracts/src/interface/IGuardianModule.sol +++ b/mainnet-contracts/src/interface/IGuardianModule.sol @@ -167,17 +167,37 @@ interface IGuardianModule { /** * @notice Validates the withdrawal request - * @param eoaSignatures The guardian EOA signatures - * @param messageHash The message hash + * @param node The node operator address + * @param pubKey The public key + * @param gweiAmount The amount in gwei + * @param nonce The nonce for the node and the function selector + * @param deadline The deadline of the signature + * @param guardianEOASignatures The guardian EOA signatures */ - function validateWithdrawalRequest(bytes[] calldata eoaSignatures, bytes32 messageHash) external view; + function validateWithdrawalRequest( + address node, + bytes memory pubKey, + uint256 gweiAmount, + uint256 nonce, + uint256 deadline, + bytes[] calldata guardianEOASignatures + ) external view; /** * @notice Validates the total epochs validated - * @param eoaSignatures The guardian EOA signatures - * @param messageHash The message hash + * @param node The node operator address + * @param totalEpochsValidated The total epochs validated + * @param nonce The nonce for the node and the function selector + * @param deadline The deadline of the signature + * @param guardianEOASignatures The guardian EOA signatures */ - function validateTotalEpochsValidated(bytes[] calldata eoaSignatures, bytes32 messageHash) external view; + function validateTotalEpochsValidated( + address node, + uint256 totalEpochsValidated, + uint256 nonce, + uint256 deadline, + bytes[] calldata guardianEOASignatures + ) external view; /** * @notice Returns the threshold value for guardian signatures diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 2535009e..2b89c15a 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -23,8 +23,6 @@ import { IBeaconDepositContract } from "../interface/IBeaconDepositContract.sol" * @custom:security-contact security@puffer.fi */ interface IPufferProtocol { - - /** * @notice Emitted when the number of active validators changes * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" diff --git a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol index 8fabd74a..d1b20c97 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; - interface IPufferProtocolLogic { - function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable; -} \ No newline at end of file + function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) + external + payable; +} diff --git a/mainnet-contracts/src/struct/ProtocolStorage.sol b/mainnet-contracts/src/struct/ProtocolStorage.sol index a30cf4cd..a1671f09 100644 --- a/mainnet-contracts/src/struct/ProtocolStorage.sol +++ b/mainnet-contracts/src/struct/ProtocolStorage.sol @@ -66,7 +66,6 @@ struct ProtocolStorage { * Slot 9 */ uint256 vtPenaltyEpochs; - /** * @dev Address of the PufferProtocolLogic contract * Slot 10 diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 3125648e..d8be0943 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -333,10 +333,10 @@ contract PufferProtocolTest is UnitTestHelper { vm.stopPrank(); // 4. validator - _registerValidatorKey(alice, zeroPubKeyPart, PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), zeroPubKeyPart, PUFFER_MODULE_0, 0); // 5. Validator - _registerValidatorKey(alice, zeroPubKeyPart, PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), zeroPubKeyPart, PUFFER_MODULE_0, 0); assertEq(pufferProtocol.getPendingValidatorIndex(PUFFER_MODULE_0), 5, "next pending validator index"); @@ -523,6 +523,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); uint256 amount = BOND + (pufferOracle.getValidatorTicketPrice() * MINIMUM_EPOCHS_VALIDATION); + uint256 deadline = block.timestamp + 1 days; vm.startPrank(alice); @@ -532,9 +533,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); - pufferProtocol.registerValidatorKey{ value: amount }( - data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days - ); + pufferProtocol.registerValidatorKey{ value: amount }(data, PUFFER_MODULE_0, 0, new bytes[](0), deadline); vm.stopPrank(); assertApproxEqAbs( @@ -548,7 +547,9 @@ contract PufferProtocolTest is UnitTestHelper { // alice validated for 20 days * 225 epochs = 4500 epochs with 1 validator uint256 validatedEpochs = 4500; - bytes[] memory vtConsumptionSignatures = _getGuardianSignaturesForRegistration(alice, validatedEpochs); + bytes[] memory vtConsumptionSignatures = _getTotalEpochsValidatedSignatures( + alice, validatedEpochs, deadline, IPufferProtocol.depositValidationTime.selector + ); // We deposit 10 VT for alice (legacy VT) deal(address(validatorTicket), address(this), 10 ether); @@ -563,12 +564,12 @@ contract PufferProtocolTest is UnitTestHelper { emit IPufferProtocol.ValidationTimeConsumed( alice, 10 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 10 ether ); // 10 Legacy VTs got burned - pufferProtocol.depositValidationTime{ value: 1 ether }( + pufferProtocol.depositValidationTime{ value: 0.1 ether }( EpochsValidatedSignature({ nodeOperator: alice, totalEpochsValidated: validatedEpochs, - functionSelector: IPufferProtocol.depositValidationTime.selector, - deadline: block.timestamp + 1 days, + functionSelector: 0, + deadline: deadline, signatures: vtConsumptionSignatures }) ); @@ -606,7 +607,7 @@ contract PufferProtocolTest is UnitTestHelper { } function test_claim_bond_for_single_withdrawal() external { - uint256 startTimestamp = 1707411226; + uint256 startTimestamp = 1707411226; // TODO Remove this if not used // Alice registers one validator and we provision it vm.deal(alice, 3 ether); @@ -616,6 +617,8 @@ contract PufferProtocolTest is UnitTestHelper { _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); + uint256 deadline = block.timestamp + 1 days; + assertApproxEqAbs( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), 1.5 ether, @@ -641,13 +644,15 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: 16 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 16 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 16 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); // Valid proof - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); // Alice got the pufETH assertEq(pufferVault.balanceOf(alice), validator.bond, "alice got the pufETH"); @@ -947,6 +952,8 @@ contract PufferProtocolTest is UnitTestHelper { _getUnderlyingETHAmount(address(pufferProtocol)), 1.5 ether, 1, "protocol should have ~2 eth bond" ); + uint256 deadline = block.timestamp + 1 days; + vm.startPrank(alice); vm.expectEmit(true, true, true, true); @@ -962,10 +969,13 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false - }) + }), + deadline ); // 2 days are leftover from 30 (30 is minimum for registration) @@ -985,7 +995,9 @@ contract PufferProtocolTest is UnitTestHelper { assertApproxEqAbs(_getUnderlyingETHAmount(address(alice)), 1.5 ether, 1, "alice got back the bond"); - bytes[] memory vtConsumptionSignature = _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY); + bytes[] memory vtConsumptionSignature = _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ); // We've removed the validator data, meaning the validator status is 0 (UNINITIALIZED) vm.expectRevert(abi.encodeWithSelector(ProtocolConstants.InvalidValidatorState.selector, 0)); @@ -999,7 +1011,8 @@ contract PufferProtocolTest is UnitTestHelper { vtConsumptionSignature: vtConsumptionSignature, wasSlashed: false, isDownsize: false - }) + }), + deadline ); } @@ -1044,13 +1057,17 @@ contract PufferProtocolTest is UnitTestHelper { // 28 days of epochs uint256 epochsValidated = 28 * EPOCHS_PER_DAY; + uint256 deadline = block.timestamp + 1 days; + StoppedValidatorInfo memory aliceInfo = StoppedValidatorInfo({ module: NoRestakingModule, moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: epochsValidated, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, epochsValidated), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, epochsValidated, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1061,7 +1078,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 1, withdrawalAmount: 32 ether, totalEpochsValidated: epochsValidated, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, epochsValidated), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + bob, epochsValidated, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1084,7 +1103,7 @@ contract PufferProtocolTest is UnitTestHelper { emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); pufferProtocol.batchHandleWithdrawals( - stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline ); assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); @@ -1116,6 +1135,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.depositValidatorTickets(vtPermit, dianna); pufferProtocol.depositValidatorTickets(vtPermit, eve); + uint256 deadline = block.timestamp + 1 days; + StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](5); stopInfos[0] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, @@ -1123,7 +1144,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: 35 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 35 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 35 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1133,7 +1156,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 1, withdrawalAmount: 31.9 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(bob, 28 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + bob, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1143,7 +1168,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 2, withdrawalAmount: 31 ether, totalEpochsValidated: 34 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(charlie, 34 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + charlie, 34 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: true, isDownsize: false }); @@ -1153,7 +1180,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 3, withdrawalAmount: 31.8 ether, totalEpochsValidated: 48 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(dianna, 48 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + dianna, 48 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1163,7 +1192,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 4, withdrawalAmount: 31.5 ether, totalEpochsValidated: 2 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(eve, 2 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + eve, 2 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: true, isDownsize: false }); @@ -1199,7 +1230,7 @@ contract PufferProtocolTest is UnitTestHelper { _getPubKey(bytes32("eve")), 4, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond, 1 ); // got slashed pufferProtocol.batchHandleWithdrawals( - stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline ); assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); @@ -1236,6 +1267,8 @@ contract PufferProtocolTest is UnitTestHelper { vtPermit.amount = 100 ether; pufferProtocol.depositValidatorTickets(vtPermit, alice); + uint256 deadline = block.timestamp + 1 days; + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 100 ether, "100 VT in the protocol"); // Alice is provisioned with 30 'new VT' and has 100 validator tickets deposited @@ -1252,7 +1285,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: 65 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 65 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 65 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1261,7 +1296,7 @@ contract PufferProtocolTest is UnitTestHelper { assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); pufferProtocol.batchHandleWithdrawals( - stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline ); // Validation time is unchanged @@ -1299,6 +1334,8 @@ contract PufferProtocolTest is UnitTestHelper { uint256 exchangeRateAfterVTPurchase = 1000945000000000000; + uint256 deadline = block.timestamp + 1 days; + // Exchange rate remained unchanged, 1 wei diff (rounding) assertApproxEqAbs( pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "initial exchange rate is ~1:1" @@ -1322,7 +1359,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: 120 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 120 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 120 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1331,7 +1370,7 @@ contract PufferProtocolTest is UnitTestHelper { assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); pufferProtocol.batchHandleWithdrawals( - stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline ); // Nothing is changed, we didn't deposit revenue @@ -1370,13 +1409,17 @@ contract PufferProtocolTest is UnitTestHelper { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); + uint256 deadline = block.timestamp + 1 days; + StoppedValidatorInfo memory aliceInfo = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1387,7 +1430,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 1, withdrawalAmount: 32 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + bob, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1395,11 +1440,11 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); // 10 days of VT emit IPufferProtocol.ValidationTimeConsumed(alice, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); - _executeFullWithdrawal(aliceInfo); + _executeFullWithdrawal(aliceInfo, deadline); vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); // 10 days of VT emit IPufferProtocol.ValidationTimeConsumed(bob, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); - _executeFullWithdrawal(bobInfo); + _executeFullWithdrawal(bobInfo, deadline); assertApproxEqAbs( _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" @@ -1431,15 +1476,15 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(bobBalanceBefore, pufferVault.balanceOf(bob), "bob balance"); } - function _executeFullWithdrawal(StoppedValidatorInfo memory validatorInfo) internal { + function _executeFullWithdrawal(StoppedValidatorInfo memory validatorInfo, uint256 deadline) internal { StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](1); stopInfos[0] = validatorInfo; vm.stopPrank(); // this contract has the PAYMASTER role, so we need to stop the prank pufferProtocol.batchHandleWithdrawals({ validatorInfos: stopInfos, - guardianEOASignatures: _getHandleBatchWithdrawalMessage(stopInfos), - deadline: block.timestamp + 1 days + guardianEOASignatures: _getHandleBatchWithdrawalMessage(stopInfos, deadline), + deadline: deadline }); } @@ -1460,6 +1505,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + uint256 deadline = block.timestamp + 1 days; + // Give funds to modules vm.deal(NoRestakingModule, 200 ether); @@ -1472,14 +1519,16 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 29 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: true, isDownsize: false }); // Burns two bonds from Alice (she registered 2 validators, but only one got activated) // If the other one was active it would get ejected by the guardians - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); // 1 ETH gives you more pufETH after the `retrieveBond` call, meaning it is worse than before assertLt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); @@ -1512,6 +1561,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + uint256 deadline = block.timestamp + 1 days; + vm.deal(NoRestakingModule, 200 ether); // Now the node operators submit proofs to get back their bond @@ -1522,14 +1573,16 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, totalEpochsValidated: 28 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), withdrawalAmount: 29.5 ether, wasSlashed: true, isDownsize: false }); // Burns one whole bond - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); // 1 ETH gives you more pufETH after the `retrieveBond` call, meaning it is better for pufETH holders assertLt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); @@ -1570,6 +1623,8 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(pufferVault.convertToAssets(1 ether), 1 ether, "shares after provisioning"); assertEq(weth.balanceOf(address(pufferVault)), 0 ether, "0 WETH in the vault"); + uint256 deadline = block.timestamp + 1 days; + vm.deal(NoRestakingModule, 200 ether); // Now the node operators submit proofs to get back their bond @@ -1580,14 +1635,16 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, totalEpochsValidated: 28 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), withdrawalAmount: 30.9 ether, // 1.1 ETH slashed wasSlashed: true, isDownsize: false }); // Burns one whole bond - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); // 30 ETH was returned to the vault assertEq(address(pufferVault).balance, 1001.9 ether, "1001.9 ETH in the vault"); @@ -1630,6 +1687,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + uint256 deadline = block.timestamp + 1 days; + vm.deal(NoRestakingModule, 200 ether); // Now the node operators submit proofs to get back their bond @@ -1640,14 +1699,16 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, totalEpochsValidated: 28 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 28 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), withdrawalAmount: 31.9 ether, wasSlashed: false, isDownsize: false }); // Burns one whole bond - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); revenueDepositor.depositRevenue(); @@ -1683,6 +1744,8 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(NoRestakingModule, 200 ether); + uint256 deadline = block.timestamp + 1 days; + // Now the node operators submit proofs to get back their bond vm.startPrank(alice); // Invalid block number = invalid proof @@ -1691,14 +1754,16 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, totalEpochsValidated: 15 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 15 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 15 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), withdrawalAmount: 32.1 ether, wasSlashed: false, isDownsize: false }); // Burns one whole bond - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); revenueDepositor.depositRevenue(); @@ -1725,6 +1790,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + uint256 deadline = block.timestamp + 1 days; + // We would handle this case on the backend, the guardians would return a value + a signature to mitigate this // Alice exited after 1 day @@ -1735,10 +1802,13 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: 10 * EPOCHS_PER_DAY, // penalty is 10 - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 10 * EPOCHS_PER_DAY), // penalty is 10 + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 10 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), // penalty is 10 wasSlashed: false, isDownsize: false - }) + }), + deadline ); uint256 leftOverValidationTime = 20 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(); @@ -1764,6 +1834,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.changeMinimumVTAmount(35 ether); vm.stopPrank(); + uint256 deadline = block.timestamp + 1 days; + // Alice exited after 1 day _executeFullWithdrawal( StoppedValidatorInfo({ @@ -1772,10 +1844,13 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: 3 * EPOCHS_PER_DAY, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, 3 * EPOCHS_PER_DAY), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 3 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false - }) + }), + deadline ); assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0 ether, "alice got 0 VT left in the protocol"); @@ -1789,13 +1864,13 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); + uint256 deadline = block.timestamp + 1 days; + // Register validator key by paying SC in ETH and depositing bond in pufETH vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidationTimeDeposited({ node: address(this), ethAmount: 7.5 ether }); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); - pufferProtocol.registerValidatorKey{ value: 9 ether }( - data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days - ); + pufferProtocol.registerValidatorKey{ value: 9 ether }(data, PUFFER_MODULE_0, 0, new bytes[](0), deadline); // Protocol holds 7.5 ETHER assertEq(address(pufferProtocol).balance, 7.5 ether, "7.5 ETH in the protocol"); @@ -1848,16 +1923,19 @@ contract PufferProtocolTest is UnitTestHelper { * @notice Get the guardian signatures from the backend API for the total validated epochs by the node operator * @param node The address of the node operator * @param validatedEpochsTotal The total number of validated epochs (sum for all the validators and their consumption) + * @param deadline The deadline for the signature * @return guardianSignatures The guardian signatures */ - function _getGuardianSignaturesForRegistration(address node, uint256 validatedEpochsTotal) - internal - view - returns (bytes[] memory) - { - uint256 nonce = pufferProtocol.nonces(IPufferProtocol.registerValidatorKey.selector, node); + function _getTotalEpochsValidatedSignatures( + address node, + uint256 validatedEpochsTotal, + uint256 deadline, + bytes32 funcSelector + ) internal view returns (bytes[] memory) { + uint256 nonce = pufferProtocol.nonces(funcSelector, node); - bytes32 digest = keccak256(abi.encode(node, validatedEpochsTotal, nonce)); + bytes32 digest = + LibGuardianMessages._getTotalEpochsValidatedMessage(node, validatedEpochsTotal, nonce, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SK, digest); bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. @@ -1876,12 +1954,12 @@ contract PufferProtocolTest is UnitTestHelper { return guardianSignatures; } - function _getHandleBatchWithdrawalMessage(StoppedValidatorInfo[] memory validatorInfos) + function _getHandleBatchWithdrawalMessage(StoppedValidatorInfo[] memory validatorInfos, uint256 deadline) internal view returns (bytes[] memory) { - bytes32 digest = LibGuardianMessages._getHandleBatchWithdrawalMessage(validatorInfos, block.timestamp + 1 days); + bytes32 digest = LibGuardianMessages._getHandleBatchWithdrawalMessage(validatorInfos, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SK, digest); bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. @@ -1978,13 +2056,17 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, moduleName); uint256 idx = pufferProtocol.getPendingValidatorIndex(moduleName); - bytes[] memory vtConsumptionSignatures = _getGuardianSignaturesForRegistration(nodeOperator, epochsValidated); + uint256 deadline = block.timestamp + 1 days; + + bytes[] memory vtConsumptionSignatures = _getTotalEpochsValidatedSignatures( + nodeOperator, epochsValidated, deadline, IPufferProtocol.registerValidatorKey.selector + ); // Empty permit means that the node operator is paying with ETH for both bond & VT in the registration transaction vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: amount }( - validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures, block.timestamp + 1 days + validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures, deadline ); } @@ -2056,6 +2138,8 @@ contract PufferProtocolTest is UnitTestHelper { } function test_panic_batch_withdrawals() public { + uint256 deadline = block.timestamp + 1 days; + // Test with zero epochs StoppedValidatorInfo memory info = StoppedValidatorInfo({ module: NoRestakingModule, @@ -2063,7 +2147,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, withdrawalAmount: 32 ether, totalEpochsValidated: type(uint256).max, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(bob, type(uint256).max), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + bob, type(uint256).max, deadline, IPufferProtocol.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -2076,7 +2162,7 @@ contract PufferProtocolTest is UnitTestHelper { // Panic Error is expected panic: arithmetic underflow or overflow (0x11) vm.expectRevert(bytes("panic: arithmetic underflow or overflow (0x11)")); pufferProtocol.batchHandleWithdrawals( - validatorInfos, _getHandleBatchWithdrawalMessage(validatorInfos), block.timestamp + 1 days + validatorInfos, _getHandleBatchWithdrawalMessage(validatorInfos, deadline), deadline ); } From 37fff259f54e94b389f3151f942dd02ecac9bb03 Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 7 Jul 2025 20:39:37 +0200 Subject: [PATCH 58/82] Changed ProtocolLogic structure. Moved more logic to ext contract (WIP) --- mainnet-contracts/script/DeployPuffer.s.sol | 10 +- mainnet-contracts/src/ProtocolConstants.sol | 38 ++ mainnet-contracts/src/PufferProtocol.sol | 325 +++++++----------- mainnet-contracts/src/PufferProtocolLogic.sol | 164 +++++++++ .../src/interface/IPufferProtocolLogic.sol | 31 ++ .../test/unit/PufferProtocol.t.sol | 3 + 6 files changed, 375 insertions(+), 196 deletions(-) diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index 3987dd66..c615deeb 100644 --- a/mainnet-contracts/script/DeployPuffer.s.sol +++ b/mainnet-contracts/script/DeployPuffer.s.sol @@ -182,7 +182,15 @@ contract DeployPuffer is BaseScript { address(moduleManager), abi.encodeCall(moduleManager.initialize, (address(accessManager))) ); - PufferProtocolLogic pufferProtocolLogic = new PufferProtocolLogic(); + PufferProtocolLogic pufferProtocolLogic = new PufferProtocolLogic({ + pufferVault: PufferVaultV5(payable(pufferVault)), + validatorTicket: ValidatorTicket(address(validatorTicketProxy)), + guardianModule: GuardianModule(payable(guardiansDeployment.guardianModule)), + moduleManager: address(moduleManagerProxy), + oracle: IPufferOracleV2(oracle), + beaconDepositContract: getStakingContract(), + pufferRevenueDistributor: payable(revenueDepositor) + }); // Initialize the Pool pufferProtocol.initialize({ diff --git a/mainnet-contracts/src/ProtocolConstants.sol b/mainnet-contracts/src/ProtocolConstants.sol index ef6018cf..7a475e59 100644 --- a/mainnet-contracts/src/ProtocolConstants.sol +++ b/mainnet-contracts/src/ProtocolConstants.sol @@ -3,6 +3,12 @@ pragma solidity >=0.8.0 <0.9.0; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; import { Status } from "./struct/Status.sol"; +import { PufferModuleManager } from "./PufferModuleManager.sol"; +import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; +import { IGuardianModule } from "./interface/IGuardianModule.sol"; +import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; +import { ValidatorTicket } from "./ValidatorTicket.sol"; +import { PufferVaultV5 } from "./PufferVaultV5.sol"; abstract contract ProtocolConstants { /** @@ -149,4 +155,36 @@ abstract contract ProtocolConstants { bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = IPufferProtocol.requestWithdrawal.selector; bytes32 internal constant _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS = IPufferProtocol.batchHandleWithdrawals.selector; + + IGuardianModule internal immutable _GUARDIAN_MODULE; + + ValidatorTicket internal immutable _VALIDATOR_TICKET; + + PufferVaultV5 internal immutable _PUFFER_VAULT; + + PufferModuleManager internal immutable _PUFFER_MODULE_MANAGER; + + IPufferOracleV2 internal immutable _PUFFER_ORACLE; + + IBeaconDepositContract internal immutable _BEACON_DEPOSIT_CONTRACT; + + address payable internal immutable _PUFFER_REVENUE_DISTRIBUTOR; + + constructor( + PufferVaultV5 pufferVault, + IGuardianModule guardianModule, + address moduleManager, + ValidatorTicket validatorTicket, + IPufferOracleV2 oracle, + address beaconDepositContract, + address payable pufferRevenueDistributor + ) { + _GUARDIAN_MODULE = guardianModule; + _PUFFER_VAULT = PufferVaultV5(payable(address(pufferVault))); + _PUFFER_MODULE_MANAGER = PufferModuleManager(payable(moduleManager)); + _VALIDATOR_TICKET = validatorTicket; + _PUFFER_ORACLE = oracle; + _BEACON_DEPOSIT_CONTRACT = IBeaconDepositContract(beaconDepositContract); + _PUFFER_REVENUE_DISTRIBUTOR = pufferRevenueDistributor; + } } diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 9c00fdc5..74d4baf0 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -30,6 +30,8 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ProtocolConstants } from "./ProtocolConstants.sol"; import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; +import "forge-std/console.sol"; + /** * @title PufferProtocol * @author Puffer Finance @@ -64,42 +66,6 @@ contract PufferProtocol is uint256 numBatches; } - /** - * @inheritdoc IPufferProtocol - */ - IGuardianModule public immutable override GUARDIAN_MODULE; - - /** - * @inheritdoc IPufferProtocol - * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade - */ - ValidatorTicket public immutable override VALIDATOR_TICKET; - - /** - * @inheritdoc IPufferProtocol - */ - PufferVaultV5 public immutable override PUFFER_VAULT; - - /** - * @inheritdoc IPufferProtocol - */ - PufferModuleManager public immutable PUFFER_MODULE_MANAGER; - - /** - * @inheritdoc IPufferProtocol - */ - IPufferOracleV2 public immutable override PUFFER_ORACLE; - - /** - * @inheritdoc IPufferProtocol - */ - IBeaconDepositContract public immutable override BEACON_DEPOSIT_CONTRACT; - - /** - * @inheritdoc IPufferProtocol - */ - address payable public immutable PUFFER_REVENUE_DISTRIBUTOR; - constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, @@ -108,16 +74,17 @@ contract PufferProtocol is IPufferOracleV2 oracle, address beaconDepositContract, address payable pufferRevenueDistributor - ) { - GUARDIAN_MODULE = guardianModule; - PUFFER_VAULT = PufferVaultV5(payable(address(pufferVault))); - PUFFER_MODULE_MANAGER = PufferModuleManager(payable(moduleManager)); - VALIDATOR_TICKET = validatorTicket; - PUFFER_ORACLE = oracle; - BEACON_DEPOSIT_CONTRACT = IBeaconDepositContract(beaconDepositContract); - PUFFER_REVENUE_DISTRIBUTOR = pufferRevenueDistributor; - _disableInitializers(); - } + ) + ProtocolConstants( + pufferVault, + guardianModule, + moduleManager, + validatorTicket, + oracle, + beaconDepositContract, + pufferRevenueDistributor + ) + { } receive() external payable { } @@ -149,10 +116,10 @@ contract PufferProtocol is // For an invalid signature, the permit will revert, but it is wrapped in try/catch, meaning the transaction execution // will continue. If the `msg.sender` did a `VALIDATOR_TICKET.approve(spender, amount)` before calling this // And the spender is `msg.sender` the Permit call will revert, but the overall transaction will succeed - _callPermit(address(VALIDATOR_TICKET), permit); + _callPermit(address(_VALIDATOR_TICKET), permit); // slither-disable-next-line unchecked-transfer - VALIDATOR_TICKET.transferFrom(msg.sender, address(this), permit.amount); + _VALIDATOR_TICKET.transferFrom(msg.sender, address(this), permit.amount); ProtocolStorage storage $ = _getPufferProtocolStorage(); $.nodeOperatorInfo[node].deprecated_vtBalance += SafeCast.toUint96(permit.amount); @@ -174,7 +141,7 @@ contract PufferProtocol is require(epochsValidatedSignature.nodeOperator != address(0), InvalidAddress()); ProtocolStorage storage $ = _getPufferProtocolStorage(); - uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); + uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); uint8 operatorNumBatches = $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].numBatches; require( msg.value >= operatorNumBatches * _MINIMUM_EPOCHS_VALIDATION_DEPOSIT * epochCurrentPrice @@ -187,7 +154,7 @@ contract PufferProtocol is uint256 burnAmount = _useVTOrValidationTime({ $: $, epochsValidatedSignature: epochsValidatedSignature }); if (burnAmount > 0) { - VALIDATOR_TICKET.burn(burnAmount); + _VALIDATOR_TICKET.burn(burnAmount); } $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].validationTime += SafeCast.toUint96(msg.value); @@ -216,7 +183,7 @@ contract PufferProtocol is $.nodeOperatorInfo[msg.sender].deprecated_vtBalance -= amount; // slither-disable-next-line unchecked-transfer - VALIDATOR_TICKET.transfer(recipient, amount); + _VALIDATOR_TICKET.transfer(recipient, amount); emit ValidatorTicketsWithdrawn(msg.sender, recipient, amount); } @@ -242,7 +209,7 @@ contract PufferProtocol is $.nodeOperatorInfo[msg.sender].validationTime -= amount; // WETH is a contract that has a fallback function that accepts ETH, and never reverts - address weth = PUFFER_VAULT.asset(); + address weth = _PUFFER_VAULT.asset(); weth.call{ value: amount }(""); // Transfer WETH to the recipient ERC20(weth).transfer(recipient, amount); @@ -269,7 +236,7 @@ contract PufferProtocol is _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); - uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); + uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); uint8 numBatches = data.numBatches; uint256 bondAmountEth = _VALIDATOR_BOND * numBatches; @@ -295,7 +262,7 @@ contract PufferProtocol is }); // The bond is converted to pufETH at the current exchange rate - uint256 pufETHBondAmount = PUFFER_VAULT.depositETH{ value: bondAmountEth }(address(this)); + uint256 pufETHBondAmount = _PUFFER_VAULT.depositETH{ value: bondAmountEth }(address(this)); uint256 pufferModuleIndex = $.pendingValidatorIndices[moduleName]; @@ -335,7 +302,7 @@ contract PufferProtocol is * @dev Restricted to Puffer Paymaster */ function provisionNode(bytes calldata validatorSignature, bytes32 depositRootHash) external restricted { - if (depositRootHash != BEACON_DEPOSIT_CONTRACT.get_deposit_root()) { + if (depositRootHash != _BEACON_DEPOSIT_CONTRACT.get_deposit_root()) { revert InvalidDepositRootHash(); } @@ -381,9 +348,12 @@ contract PufferProtocol is bytes memory callData = abi.encodeWithSelector( IPufferProtocolLogic._requestConsolidation.selector, moduleName, srcIndices, targetIndices ); - (bool success,) = address(this).delegatecall(callData); + + (bool success, bytes memory result) = _delegatecall(_getPufferProtocolStorage().pufferProtocolLogic, callData); if (!success) { - revert Failed(); + assembly { + revert(add(result, 32), mload(result)) + } } } @@ -425,7 +395,7 @@ contract PufferProtocol is // If downsize or rewards withdrawal, backend needs to validate the amount - GUARDIAN_MODULE.validateWithdrawalRequest({ + _GUARDIAN_MODULE.validateWithdrawalRequest({ node: msg.sender, pubKey: pubkeys[i], gweiAmount: gweiAmounts[i], @@ -436,7 +406,7 @@ contract PufferProtocol is } } - PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); + _PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); } function _batchHandleWithdrawalsAccounting( @@ -452,7 +422,7 @@ contract PufferProtocol is : validatorInfos[i].withdrawalAmount; //solhint-disable-next-line avoid-low-level-calls (bool success,) = - PufferModule(payable(validatorInfos[i].module)).call(address(PUFFER_VAULT), transferAmount, ""); + PufferModule(payable(validatorInfos[i].module)).call(address(_PUFFER_VAULT), transferAmount, ""); if (!success) { revert Failed(); } @@ -462,7 +432,7 @@ contract PufferProtocol is continue; } // slither-disable-next-line unchecked-transfer - PUFFER_VAULT.transfer(bondWithdrawals[i].node, bondWithdrawals[i].pufETHAmount); + _PUFFER_VAULT.transfer(bondWithdrawals[i].node, bondWithdrawals[i].pufETHAmount); } // slither-disable-start calls-loop } @@ -480,7 +450,7 @@ contract PufferProtocol is revert DeadlineExceeded(); } - GUARDIAN_MODULE.validateBatchWithdrawals(validatorInfos, guardianEOASignatures, deadline); + _GUARDIAN_MODULE.validateBatchWithdrawals(validatorInfos, guardianEOASignatures, deadline); ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -540,15 +510,15 @@ contract PufferProtocol is } if (burnAmounts.vt > 0) { - VALIDATOR_TICKET.burn(burnAmounts.vt); + _VALIDATOR_TICKET.burn(burnAmounts.vt); } if (burnAmounts.pufETH > 0) { // Because we've calculated everything in the previous loop, we can do the burning - PUFFER_VAULT.burn(burnAmounts.pufETH); + _PUFFER_VAULT.burn(burnAmounts.pufETH); } // Deduct 32 ETH per batch from the `lockedETHAmount` on the PufferOracle - PUFFER_ORACLE.exitValidators(numExitedBatches); + _PUFFER_ORACLE.exitValidators(numExitedBatches); _batchHandleWithdrawalsAccounting(bondWithdrawals, validatorInfos); } @@ -565,13 +535,13 @@ contract PufferProtocol is address node = $.validators[moduleName][skippedIndex].node; // Check the signatures (reverts if invalid) - GUARDIAN_MODULE.validateSkipProvisioning({ + _GUARDIAN_MODULE.validateSkipProvisioning({ moduleName: moduleName, skippedIndex: skippedIndex, guardianEOASignatures: guardianEOASignatures }); - uint256 vtPricePerEpoch = PUFFER_ORACLE.getValidatorTicketPrice(); + uint256 vtPricePerEpoch = _PUFFER_ORACLE.getValidatorTicketPrice(); $.nodeOperatorInfo[node].validationTime -= ($.vtPenaltyEpochs * vtPricePerEpoch * $.validators[moduleName][skippedIndex].numBatches); @@ -582,7 +552,7 @@ contract PufferProtocol is // Transfer pufETH to that node operator // slither-disable-next-line unchecked-transfer - PUFFER_VAULT.transfer(node, $.validators[moduleName][skippedIndex].bond); + _PUFFER_VAULT.transfer(node, $.validators[moduleName][skippedIndex].bond); _decreaseNumberOfRegisteredValidators($, moduleName); unchecked { @@ -832,7 +802,7 @@ contract PufferProtocol is if (address($.modules[moduleName]) != address(0)) { revert ModuleAlreadyExists(); } - PufferModule module = PUFFER_MODULE_MANAGER.createNewPufferModule(moduleName); + PufferModule module = _PUFFER_MODULE_MANAGER.createNewPufferModule(moduleName); $.modules[moduleName] = module; $.moduleWeights.push(moduleName); bytes32 withdrawalCredentials = bytes32(module.getWithdrawalCredentials()); @@ -883,7 +853,7 @@ contract PufferProtocol is // The withdrawal amount is less than 32 ETH * numBatches, we burn the difference to cover up the loss for inactivity if (validatorInfo.withdrawalAmount < (uint256(32 ether) * numBatches)) { pufETHBurnAmount = - PUFFER_VAULT.convertToSharesUp((uint256(32 ether) * numBatches) - validatorInfo.withdrawalAmount); + _PUFFER_VAULT.convertToSharesUp((uint256(32 ether) * numBatches) - validatorInfo.withdrawalAmount); } // Case 3: @@ -908,16 +878,16 @@ contract PufferProtocol is PufferModule module = $.modules[moduleName]; // Transfer 32 ETH to this contract for each batch - PUFFER_VAULT.transferETH(address(this), numBatches * 32 ether); + _PUFFER_VAULT.transferETH(address(this), numBatches * 32 ether); emit SuccessfullyProvisioned(validatorPubKey, index, moduleName, numBatches); // Increase lockedETH on Puffer Oracle for (uint256 i = 0; i < numBatches; ++i) { - PUFFER_ORACLE.provisionNode(); + _PUFFER_ORACLE.provisionNode(); } - BEACON_DEPOSIT_CONTRACT.deposit{ value: numBatches * 32 ether }( + _BEACON_DEPOSIT_CONTRACT.deposit{ value: numBatches * 32 ether }( validatorPubKey, module.getWithdrawalCredentials(), validatorSignature, depositDataRoot ); } @@ -939,46 +909,15 @@ contract PufferProtocol is internal returns (uint256 vtAmountToBurn) { - address nodeOperator = epochsValidatedSignature.nodeOperator; - uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[nodeOperator].totalEpochsValidated; - - if (previousTotalEpochsValidated == epochsValidatedSignature.totalEpochsValidated) { - return 0; - } - require( - previousTotalEpochsValidated < epochsValidatedSignature.totalEpochsValidated, InvalidTotalEpochsValidated() - ); - - // Burn the VT first, then fallback to ETH from the node operator - uint256 nodeVTBalance = $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance; - - // If the node operator has VT, we burn it first - if (nodeVTBalance > 0) { - uint256 vtBurnAmount = - _getVTBurnAmount(epochsValidatedSignature.totalEpochsValidated - previousTotalEpochsValidated); - if (nodeVTBalance >= vtBurnAmount) { - // Burn the VT first, and update the node operator VT balance - vtAmountToBurn = vtBurnAmount; - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); - - emit ValidationTimeConsumed({ node: nodeOperator, consumedAmount: 0, deprecated_burntVTs: vtBurnAmount }); - - return vtAmountToBurn; + bytes memory callData = + abi.encodeWithSelector(IPufferProtocolLogic._useVTOrValidationTime.selector, epochsValidatedSignature); + (bool success, bytes memory result) = _delegatecall($.pufferProtocolLogic, callData); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) } - - // If the node operator has less VT than the amount to burn, we burn all of it, and we use the validation time - vtAmountToBurn = nodeVTBalance; - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); } - - // If the node operator has no VT, we use the validation time - _settleVTAccounting({ - $: $, - epochsValidatedSignature: epochsValidatedSignature, - deprecated_burntVTs: nodeVTBalance - }); + vtAmountToBurn = abi.decode(result, (uint256)); } /** @@ -997,62 +936,24 @@ contract PufferProtocol is EpochsValidatedSignature memory epochsValidatedSignature, uint256 deprecated_burntVTs ) internal { - address node = epochsValidatedSignature.nodeOperator; - // There is nothing to settle if this is the first validator for the node operator - if ($.nodeOperatorInfo[node].activeValidatorCount + $.nodeOperatorInfo[node].pendingValidatorCount == 0) { - return; - } - - GUARDIAN_MODULE.validateTotalEpochsValidated({ - node: node, - totalEpochsValidated: epochsValidatedSignature.totalEpochsValidated, - nonce: _useNonce(epochsValidatedSignature.functionSelector, node), - deadline: epochsValidatedSignature.deadline, - guardianEOASignatures: epochsValidatedSignature.signatures - }); - - uint256 epochCurrentPrice = PUFFER_ORACLE.getValidatorTicketPrice(); - - uint256 meanPrice = ($.nodeOperatorInfo[node].epochPrice + epochCurrentPrice) / 2; - - uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[node].totalEpochsValidated; - - // convert burned validator tickets to epochs - uint256 epochsBurntFromDeprecatedVT = deprecated_burntVTs * 225 / 1 ether; // 1 VT = 1 DAY. 1 DAY = 225 Epochs - - uint256 validationTimeToConsume = ( - epochsValidatedSignature.totalEpochsValidated - previousTotalEpochsValidated - epochsBurntFromDeprecatedVT - ) * meanPrice; - - // Update the current epoch VT price for the node operator - $.nodeOperatorInfo[node].epochPrice = epochCurrentPrice; - $.nodeOperatorInfo[node].totalEpochsValidated = epochsValidatedSignature.totalEpochsValidated; - $.nodeOperatorInfo[node].validationTime -= validationTimeToConsume; - - emit ValidationTimeConsumed({ - node: node, - consumedAmount: validationTimeToConsume, - deprecated_burntVTs: deprecated_burntVTs - }); - - address weth = PUFFER_VAULT.asset(); - - // WETH is a contract that has a fallback function that accepts ETH, and never reverts - weth.call{ value: validationTimeToConsume }(""); - - // Transfer WETH to the Revenue Distributor, it will be slow released to the PufferVault - ERC20(weth).transfer(PUFFER_REVENUE_DISTRIBUTOR, validationTimeToConsume); - } + bytes memory callData = abi.encodeWithSelector( + IPufferProtocolLogic._settleVTAccounting.selector, + EpochsValidatedSignature({ + nodeOperator: msg.sender, + totalEpochsValidated: epochsValidatedSignature.totalEpochsValidated, + functionSelector: epochsValidatedSignature.functionSelector, + deadline: epochsValidatedSignature.deadline, + signatures: epochsValidatedSignature.signatures + }), + deprecated_burntVTs + ); - /** - * @dev Internal function to get the amount of VT to burn during a number of epochs - * @param validatedEpochs The number of epochs validated by the node operator (not necessarily the total epochs) - * @return vtBurnAmount The amount of VT to burn - */ - function _getVTBurnAmount(uint256 validatedEpochs) internal pure returns (uint256) { - // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day - // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up - return validatedEpochs * 4444444444444445; + (bool success, bytes memory result) = _delegatecall($.pufferProtocolLogic, callData); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } } function _callPermit(address token, Permit calldata permitData) internal { @@ -1088,7 +989,7 @@ contract PufferProtocol is numBatches: numBatchesBefore }); - exitingBond = validator.bond * exitedBatches / validator.numBatches; + exitingBond = (validator.bond * exitedBatches) / validator.numBatches; // The burned amount is subtracted from the exiting bond, so the remaining bond is kept in full // The backend must prevent any downsize that would result in a burned amount greater than the exiting bond @@ -1151,36 +1052,38 @@ contract PufferProtocol is return (bondBurnAmount, bondAmount - bondBurnAmount, numBatches); } - function _delegatecall(address _implementation) internal { - // Use assembly for delegatecall to forward msg.sender, msg.value, and calldata + function _delegatecall(address target, bytes memory data) internal returns (bool success, bytes memory result) { + console.log("callData"); + console.logBytes(data); + console.log("target", target); + assembly { - // Copy calldata from msg.data (starts at 0x04) - let ptr := mload(0x40) // Get next free memory pointer - calldatacopy(ptr, 0, calldatasize()) // Copy all calldata - - // Perform delegatecall - let success := - delegatecall( - gas(), // Forward all available gas - _implementation, // Address of the logic contract - ptr, // Pointer to the calldata - calldatasize(), // Size of the calldata - 0, // Output offset (we'll copy output later) - 0 // Output size (we'll copy output later) - ) - - // Get the size of the returned data + // Get the size of the input data + let dataSize := mload(data) + + // Allocate memory for input data + let ptr := mload(0x40) + + // Copy input data from memory to memory (skip the length field) + let dataPtr := add(data, 32) + for { let i := 0 } lt(i, dataSize) { i := add(i, 32) } { mstore(add(ptr, i), mload(add(dataPtr, i))) } + + // Perform the delegatecall + success := delegatecall(gas(), target, ptr, dataSize, 0, 0) + + // Handle return data let returndata_size := returndatasize() - // Allocate memory for the return data - let returndata_ptr := mload(0x40) // Get another free memory pointer - // Copy the returned data to memory - returndatacopy(returndata_ptr, 0, returndata_size) - - // Revert or return based on delegatecall success - switch success - case 0 { revert(returndata_ptr, returndata_size) } - // Revert with error message - default { return(returndata_ptr, returndata_size) } // Return data + + // Allocate memory for return data (update free memory pointer) + let result_ptr := add(ptr, and(add(dataSize, 31), not(31))) // Align to 32 bytes + mstore(0x40, add(result_ptr, add(returndata_size, 32))) + + // Store return data length and copy data + mstore(result_ptr, returndata_size) + returndatacopy(add(result_ptr, 32), 0, returndata_size) + + // Set result pointer + result := result_ptr } } @@ -1191,4 +1094,36 @@ contract PufferProtocol is } function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } + + function getPufferProtocolLogic() external view returns (address) { + return _getPufferProtocolStorage().pufferProtocolLogic; + } + + function GUARDIAN_MODULE() external view override returns (IGuardianModule) { + return _GUARDIAN_MODULE; + } + + function VALIDATOR_TICKET() external view override returns (ValidatorTicket) { + return _VALIDATOR_TICKET; + } + + function PUFFER_VAULT() external view override returns (PufferVaultV5) { + return _PUFFER_VAULT; + } + + function PUFFER_MODULE_MANAGER() external view override returns (PufferModuleManager) { + return _PUFFER_MODULE_MANAGER; + } + + function PUFFER_ORACLE() external view override returns (IPufferOracleV2) { + return _PUFFER_ORACLE; + } + + function BEACON_DEPOSIT_CONTRACT() external view override returns (IBeaconDepositContract) { + return _BEACON_DEPOSIT_CONTRACT; + } + + function PUFFER_REVENUE_DISTRIBUTOR() external view override returns (address payable) { + return _PUFFER_REVENUE_DISTRIBUTOR; + } } diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 608eaf95..83565275 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { PufferProtocolStorage } from "./PufferProtocolStorage.sol"; import { ProtocolStorage } from "./struct/ProtocolStorage.sol"; import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; @@ -8,8 +10,37 @@ import { Validator } from "./struct/Validator.sol"; import { Status } from "./struct/Validator.sol"; import { ProtocolConstants } from "./ProtocolConstants.sol"; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; +import { PufferModuleManager } from "./PufferModuleManager.sol"; +import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; +import { IGuardianModule } from "./interface/IGuardianModule.sol"; +import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; +import { ValidatorTicket } from "./ValidatorTicket.sol"; +import { PufferVaultV5 } from "./PufferVaultV5.sol"; +import { EpochsValidatedSignature } from "./struct/Signatures.sol"; + +import "forge-std/console.sol"; contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, ProtocolConstants { + constructor( + PufferVaultV5 pufferVault, + IGuardianModule guardianModule, + address moduleManager, + ValidatorTicket validatorTicket, + IPufferOracleV2 oracle, + address beaconDepositContract, + address payable pufferRevenueDistributor + ) + ProtocolConstants( + pufferVault, + guardianModule, + moduleManager, + validatorTicket, + oracle, + beaconDepositContract, + pufferRevenueDistributor + ) + { } + /** * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ @@ -51,4 +82,137 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, emit IPufferProtocol.ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); } + + /** + * @dev Internal function to return the deprecated validator tickets burn amount + * and/or consume the validation time from the node operator + * @dev The deprecated vt balance is reduced here but the actual VT is not burned here (for efficiency) + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Identifier of the function that initiated this flow + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated + * @return vtAmountToBurn The amount of VT to burn + */ + function _useVTOrValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) + external + returns (uint256 vtAmountToBurn) + { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + address nodeOperator = epochsValidatedSignature.nodeOperator; + uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[nodeOperator].totalEpochsValidated; + + if (previousTotalEpochsValidated == epochsValidatedSignature.totalEpochsValidated) { + return 0; + } + require( + previousTotalEpochsValidated < epochsValidatedSignature.totalEpochsValidated, InvalidTotalEpochsValidated() + ); + + // Burn the VT first, then fallback to ETH from the node operator + uint256 nodeVTBalance = $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance; + + // If the node operator has VT, we burn it first + if (nodeVTBalance > 0) { + uint256 vtBurnAmount = + _getVTBurnAmount(epochsValidatedSignature.totalEpochsValidated - previousTotalEpochsValidated); + if (nodeVTBalance >= vtBurnAmount) { + // Burn the VT first, and update the node operator VT balance + vtAmountToBurn = vtBurnAmount; + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); + + emit IPufferProtocol.ValidationTimeConsumed({ + node: nodeOperator, + consumedAmount: 0, + deprecated_burntVTs: vtBurnAmount + }); + + return vtAmountToBurn; + } + + // If the node operator has less VT than the amount to burn, we burn all of it, and we use the validation time + vtAmountToBurn = nodeVTBalance; + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); + } + + // If the node operator has no VT, we use the validation time + _settleVTAccounting({ epochsValidatedSignature: epochsValidatedSignature, deprecated_burntVTs: nodeVTBalance }); + } + + /** + * @dev Internal function to settle the VT accounting for a node operator + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Identifier of the function that initiated this flow + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated + * @param deprecated_burntVTs The amount of VT to burn (to be deducted from validation time consumption) + */ + function _settleVTAccounting(EpochsValidatedSignature memory epochsValidatedSignature, uint256 deprecated_burntVTs) + public + { + console.log("settleVTAccounting"); + + ProtocolStorage storage $ = _getPufferProtocolStorage(); + address node = epochsValidatedSignature.nodeOperator; + // There is nothing to settle if this is the first validator for the node operator + if ($.nodeOperatorInfo[node].activeValidatorCount + $.nodeOperatorInfo[node].pendingValidatorCount == 0) { + return; + } + + _GUARDIAN_MODULE.validateTotalEpochsValidated({ + node: node, + totalEpochsValidated: epochsValidatedSignature.totalEpochsValidated, + nonce: _useNonce(epochsValidatedSignature.functionSelector, node), + deadline: epochsValidatedSignature.deadline, + guardianEOASignatures: epochsValidatedSignature.signatures + }); + + uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); + + uint256 meanPrice = ($.nodeOperatorInfo[node].epochPrice + epochCurrentPrice) / 2; + + uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[node].totalEpochsValidated; + + // convert burned validator tickets to epochs + uint256 epochsBurntFromDeprecatedVT = (deprecated_burntVTs * 225) / 1 ether; // 1 VT = 1 DAY. 1 DAY = 225 Epochs + + uint256 validationTimeToConsume = ( + epochsValidatedSignature.totalEpochsValidated - previousTotalEpochsValidated - epochsBurntFromDeprecatedVT + ) * meanPrice; + + // Update the current epoch VT price for the node operator + $.nodeOperatorInfo[node].epochPrice = epochCurrentPrice; + $.nodeOperatorInfo[node].totalEpochsValidated = epochsValidatedSignature.totalEpochsValidated; + $.nodeOperatorInfo[node].validationTime -= validationTimeToConsume; + + emit IPufferProtocol.ValidationTimeConsumed({ + node: node, + consumedAmount: validationTimeToConsume, + deprecated_burntVTs: deprecated_burntVTs + }); + + address weth = _PUFFER_VAULT.asset(); + + // WETH is a contract that has a fallback function that accepts ETH, and never reverts + weth.call{ value: validationTimeToConsume }(""); + + // Transfer WETH to the Revenue Distributor, it will be slow released to the PufferVault + ERC20(weth).transfer(_PUFFER_REVENUE_DISTRIBUTOR, validationTimeToConsume); + } + + /** + * @dev Internal function to get the amount of VT to burn during a number of epochs + * @param validatedEpochs The number of epochs validated by the node operator (not necessarily the total epochs) + * @return vtBurnAmount The amount of VT to burn + */ + function _getVTBurnAmount(uint256 validatedEpochs) internal pure returns (uint256) { + // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day + // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up + return validatedEpochs * 4444444444444445; + } } diff --git a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol index d1b20c97..2d1a554b 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -1,8 +1,39 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import { EpochsValidatedSignature } from "../struct/Signatures.sol"; + interface IPufferProtocolLogic { function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable; + + /** + * @dev Internal function to return the deprecated validator tickets burn amount + * and/or consume the validation time from the node operator + * @dev The deprecated vt balance is reduced here but the actual VT is not burned here (for efficiency) + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Identifier of the function that initiated this flow + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated + * @return vtAmountToBurn The amount of VT to burn + */ + function _useVTOrValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) + external + returns (uint256 vtAmountToBurn); + + /** + * @dev Internal function to settle the VT accounting for a node operator + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Identifier of the function that initiated this flow + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated + * @param deprecated_burntVTs The amount of VT to burn (to be deducted from validation time consumption) + */ + function _settleVTAccounting(EpochsValidatedSignature memory epochsValidatedSignature, uint256 deprecated_burntVTs) + external; } diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index d8be0943..98388a0e 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -26,6 +26,8 @@ import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol" import { NodeInfo } from "../../src/struct/NodeInfo.sol"; import { EpochsValidatedSignature } from "../../src/struct/Signatures.sol"; +import "forge-std/console.sol"; + contract PufferProtocolTest is UnitTestHelper { using ECDSA for bytes32; @@ -1049,6 +1051,7 @@ contract PufferProtocolTest is UnitTestHelper { // Batch claim 32 ETH withdrawals function test_batch_claim() public { + console.log(pufferProtocol.getPufferProtocolLogic()); _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); From 27dea1059e246d937527b1035b3ac254e44338a6 Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 8 Jul 2025 16:58:36 +0200 Subject: [PATCH 59/82] Fixed and simplify delegatecalls --- mainnet-contracts/src/PufferProtocol.sol | 53 ++----------------- mainnet-contracts/src/PufferProtocolLogic.sol | 6 +-- .../src/interface/IPufferProtocolLogic.sol | 4 +- 3 files changed, 9 insertions(+), 54 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 74d4baf0..afe24960 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -30,8 +30,6 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { ProtocolConstants } from "./ProtocolConstants.sol"; import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; -import "forge-std/console.sol"; - /** * @title PufferProtocol * @author Puffer Finance @@ -349,7 +347,7 @@ contract PufferProtocol is IPufferProtocolLogic._requestConsolidation.selector, moduleName, srcIndices, targetIndices ); - (bool success, bytes memory result) = _delegatecall(_getPufferProtocolStorage().pufferProtocolLogic, callData); + (bool success, bytes memory result) = _getPufferProtocolStorage().pufferProtocolLogic.delegatecall(callData); if (!success) { assembly { revert(add(result, 32), mload(result)) @@ -911,7 +909,7 @@ contract PufferProtocol is { bytes memory callData = abi.encodeWithSelector(IPufferProtocolLogic._useVTOrValidationTime.selector, epochsValidatedSignature); - (bool success, bytes memory result) = _delegatecall($.pufferProtocolLogic, callData); + (bool success, bytes memory result) = $.pufferProtocolLogic.delegatecall(callData); if (!success) { assembly { revert(add(result, 32), mload(result)) @@ -937,18 +935,10 @@ contract PufferProtocol is uint256 deprecated_burntVTs ) internal { bytes memory callData = abi.encodeWithSelector( - IPufferProtocolLogic._settleVTAccounting.selector, - EpochsValidatedSignature({ - nodeOperator: msg.sender, - totalEpochsValidated: epochsValidatedSignature.totalEpochsValidated, - functionSelector: epochsValidatedSignature.functionSelector, - deadline: epochsValidatedSignature.deadline, - signatures: epochsValidatedSignature.signatures - }), - deprecated_burntVTs + IPufferProtocolLogic._settleVTAccounting.selector, epochsValidatedSignature, deprecated_burntVTs ); - (bool success, bytes memory result) = _delegatecall($.pufferProtocolLogic, callData); + (bool success, bytes memory result) = $.pufferProtocolLogic.delegatecall(callData); if (!success) { assembly { revert(add(result, 32), mload(result)) @@ -1052,41 +1042,6 @@ contract PufferProtocol is return (bondBurnAmount, bondAmount - bondBurnAmount, numBatches); } - function _delegatecall(address target, bytes memory data) internal returns (bool success, bytes memory result) { - console.log("callData"); - console.logBytes(data); - console.log("target", target); - - assembly { - // Get the size of the input data - let dataSize := mload(data) - - // Allocate memory for input data - let ptr := mload(0x40) - - // Copy input data from memory to memory (skip the length field) - let dataPtr := add(data, 32) - for { let i := 0 } lt(i, dataSize) { i := add(i, 32) } { mstore(add(ptr, i), mload(add(dataPtr, i))) } - - // Perform the delegatecall - success := delegatecall(gas(), target, ptr, dataSize, 0, 0) - - // Handle return data - let returndata_size := returndatasize() - - // Allocate memory for return data (update free memory pointer) - let result_ptr := add(ptr, and(add(dataSize, 31), not(31))) // Align to 32 bytes - mstore(0x40, add(result_ptr, add(returndata_size, 32))) - - // Store return data length and copy data - mstore(result_ptr, returndata_size) - returndatacopy(add(result_ptr, 32), 0, returndata_size) - - // Set result pointer - result := result_ptr - } - } - function _setPufferProtocolLogic(address newPufferProtocolLogic) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); emit PufferProtocolLogicSet($.pufferProtocolLogic, newPufferProtocolLogic); diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 83565275..7098cac5 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -18,8 +18,6 @@ import { ValidatorTicket } from "./ValidatorTicket.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; -import "forge-std/console.sol"; - contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, ProtocolConstants { constructor( PufferVaultV5 pufferVault, @@ -97,6 +95,7 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, */ function _useVTOrValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external + payable returns (uint256 vtAmountToBurn) { ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -154,9 +153,8 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, */ function _settleVTAccounting(EpochsValidatedSignature memory epochsValidatedSignature, uint256 deprecated_burntVTs) public + payable { - console.log("settleVTAccounting"); - ProtocolStorage storage $ = _getPufferProtocolStorage(); address node = epochsValidatedSignature.nodeOperator; // There is nothing to settle if this is the first validator for the node operator diff --git a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol index 2d1a554b..82c877ae 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -22,6 +22,7 @@ interface IPufferProtocolLogic { */ function _useVTOrValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external + payable returns (uint256 vtAmountToBurn); /** @@ -35,5 +36,6 @@ interface IPufferProtocolLogic { * @param deprecated_burntVTs The amount of VT to burn (to be deducted from validation time consumption) */ function _settleVTAccounting(EpochsValidatedSignature memory epochsValidatedSignature, uint256 deprecated_burntVTs) - external; + external + payable; } From 50536f7324b09c9ac73f13e65682e00735cfd350 Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 8 Jul 2025 18:03:46 +0200 Subject: [PATCH 60/82] Refactor delegatecalls and small fixes --- mainnet-contracts/src/PufferProtocol.sol | 36 +++++++++---------- .../test/unit/PufferProtocol.t.sol | 2 -- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index afe24960..3be5174b 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -82,7 +82,9 @@ contract PufferProtocol is beaconDepositContract, pufferRevenueDistributor ) - { } + { + _disableInitializers(); + } receive() external payable { } @@ -346,13 +348,7 @@ contract PufferProtocol is bytes memory callData = abi.encodeWithSelector( IPufferProtocolLogic._requestConsolidation.selector, moduleName, srcIndices, targetIndices ); - - (bool success, bytes memory result) = _getPufferProtocolStorage().pufferProtocolLogic.delegatecall(callData); - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } + _delegatecall(_getPufferProtocolStorage(), callData); } /** @@ -909,12 +905,7 @@ contract PufferProtocol is { bytes memory callData = abi.encodeWithSelector(IPufferProtocolLogic._useVTOrValidationTime.selector, epochsValidatedSignature); - (bool success, bytes memory result) = $.pufferProtocolLogic.delegatecall(callData); - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } + bytes memory result = _delegatecall($, callData); vtAmountToBurn = abi.decode(result, (uint256)); } @@ -938,12 +929,7 @@ contract PufferProtocol is IPufferProtocolLogic._settleVTAccounting.selector, epochsValidatedSignature, deprecated_burntVTs ); - (bool success, bytes memory result) = $.pufferProtocolLogic.delegatecall(callData); - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } + _delegatecall($, callData); } function _callPermit(address token, Permit calldata permitData) internal { @@ -1050,6 +1036,16 @@ contract PufferProtocol is function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } + function _delegatecall(ProtocolStorage storage $, bytes memory callData) internal returns (bytes memory) { + (bool success, bytes memory result) = $.pufferProtocolLogic.delegatecall(callData); + if (!success) { + assembly { + revert(add(result, 32), mload(result)) + } + } + return result; + } + function getPufferProtocolLogic() external view returns (address) { return _getPufferProtocolStorage().pufferProtocolLogic; } diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 98388a0e..feda0326 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -26,7 +26,6 @@ import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol" import { NodeInfo } from "../../src/struct/NodeInfo.sol"; import { EpochsValidatedSignature } from "../../src/struct/Signatures.sol"; -import "forge-std/console.sol"; contract PufferProtocolTest is UnitTestHelper { using ECDSA for bytes32; @@ -1051,7 +1050,6 @@ contract PufferProtocolTest is UnitTestHelper { // Batch claim 32 ETH withdrawals function test_batch_claim() public { - console.log(pufferProtocol.getPufferProtocolLogic()); _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); From 7b530765549e0d0a2aa20ca2962b236478306ced Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 8 Jul 2025 19:33:57 +0200 Subject: [PATCH 61/82] Refactor PufferConstants to PufferProtocolBase and moved more logic to logic contract --- mainnet-contracts/src/PufferProtocol.sol | 469 +-------------- ...olConstants.sol => PufferProtocolBase.sol} | 4 +- mainnet-contracts/src/PufferProtocolLogic.sol | 564 ++++++++++++++++-- .../src/interface/IPufferProtocolLogic.sol | 65 +- .../test/unit/PufferProtocol.t.sol | 31 +- 5 files changed, 592 insertions(+), 541 deletions(-) rename mainnet-contracts/src/{ProtocolConstants.sol => PufferProtocolBase.sol} (96%) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 3be5174b..bff1a327 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -5,7 +5,6 @@ import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { PufferProtocolStorage } from "./PufferProtocolStorage.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; import { IGuardianModule } from "./interface/IGuardianModule.sol"; @@ -24,10 +23,9 @@ import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; -import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { ProtocolConstants } from "./ProtocolConstants.sol"; +import { PufferProtocolBase } from "./PufferProtocolBase.sol"; import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; /** @@ -41,29 +39,8 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, - PufferProtocolStorage, - ProtocolSignatureNonces, - ProtocolConstants + PufferProtocolBase { - /** - * @dev Helper struct for the full withdrawals accounting - * The amounts of VT and pufETH to burn at the end of the withdrawal - */ - struct BurnAmounts { - uint256 vt; - uint256 pufETH; - } - - /** - * @dev Helper struct for the full withdrawals accounting - * The amounts of pufETH to send to the node operator - */ - struct Withdrawals { - uint256 pufETHAmount; - address node; - uint256 numBatches; - } - constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, @@ -73,7 +50,7 @@ contract PufferProtocol is address beaconDepositContract, address payable pufferRevenueDistributor ) - ProtocolConstants( + PufferProtocolBase( pufferVault, guardianModule, moduleManager, @@ -135,30 +112,9 @@ contract PufferProtocol is payable restricted { - if (block.timestamp > epochsValidatedSignature.deadline) { - revert DeadlineExceeded(); - } - - require(epochsValidatedSignature.nodeOperator != address(0), InvalidAddress()); - ProtocolStorage storage $ = _getPufferProtocolStorage(); - uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); - uint8 operatorNumBatches = $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].numBatches; - require( - msg.value >= operatorNumBatches * _MINIMUM_EPOCHS_VALIDATION_DEPOSIT * epochCurrentPrice - && msg.value <= operatorNumBatches * _MAXIMUM_EPOCHS_VALIDATION_DEPOSIT * epochCurrentPrice, - InvalidETHAmount() - ); - - epochsValidatedSignature.functionSelector = _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME; - - uint256 burnAmount = _useVTOrValidationTime({ $: $, epochsValidatedSignature: epochsValidatedSignature }); - - if (burnAmount > 0) { - _VALIDATOR_TICKET.burn(burnAmount); - } - - $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].validationTime += SafeCast.toUint96(msg.value); - emit ValidationTimeDeposited({ node: epochsValidatedSignature.nodeOperator, ethAmount: msg.value }); + bytes memory callData = + abi.encodeWithSelector(IPufferProtocolLogic._depositValidationTime.selector, epochsValidatedSignature); + _delegatecall(_getPufferProtocolStorage(), callData); } /** @@ -193,28 +149,9 @@ contract PufferProtocol is * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ function withdrawValidationTime(uint96 amount, address recipient) external restricted { - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - // Node operator can only withdraw if they have no active or pending validators - // In the future, we plan to allow node operators to withdraw VTs even if they have active/pending validators. - if ( - $.nodeOperatorInfo[msg.sender].activeValidatorCount + $.nodeOperatorInfo[msg.sender].pendingValidatorCount - != 0 - ) { - revert ActiveOrPendingValidatorsExist(); - } - - // Reverts if insufficient balance - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[msg.sender].validationTime -= amount; - - // WETH is a contract that has a fallback function that accepts ETH, and never reverts - address weth = _PUFFER_VAULT.asset(); - weth.call{ value: amount }(""); - // Transfer WETH to the recipient - ERC20(weth).transfer(recipient, amount); - - emit ValidationTimeWithdrawn(msg.sender, recipient, amount); + bytes memory callData = + abi.encodeWithSelector(IPufferProtocolLogic._withdrawValidationTime.selector, amount, recipient); + _delegatecall(_getPufferProtocolStorage(), callData); } /** @@ -228,73 +165,15 @@ contract PufferProtocol is bytes[] calldata vtConsumptionSignature, uint256 deadline ) external payable restricted { - if (block.timestamp > deadline) { - revert DeadlineExceeded(); - } - - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); - - uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); - uint8 numBatches = data.numBatches; - uint256 bondAmountEth = _VALIDATOR_BOND * numBatches; - - // The node operator must deposit 1.5 ETH (per batch) or more + minimum validation time for ~30 days - // At the moment that's roughly 30 days * 225 (there is roughly 225 epochs per day) - uint256 minimumETHRequired = - bondAmountEth + (numBatches * _MINIMUM_EPOCHS_VALIDATION_REGISTRATION * epochCurrentPrice); - - require(msg.value >= minimumETHRequired, InvalidETHAmount()); - - emit ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - bondAmountEth) }); - - _settleVTAccounting({ - $: $, - epochsValidatedSignature: EpochsValidatedSignature({ - nodeOperator: msg.sender, - totalEpochsValidated: totalEpochsValidated, - functionSelector: _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY, - deadline: deadline, - signatures: vtConsumptionSignature - }), - deprecated_burntVTs: 0 - }); - - // The bond is converted to pufETH at the current exchange rate - uint256 pufETHBondAmount = _PUFFER_VAULT.depositETH{ value: bondAmountEth }(address(this)); - - uint256 pufferModuleIndex = $.pendingValidatorIndices[moduleName]; - - // No need for SafeCast - $.validators[moduleName][pufferModuleIndex] = Validator({ - pubKey: data.blsPubKey, - status: Status.PENDING, - module: address($.modules[moduleName]), - bond: uint96(pufETHBondAmount), - node: msg.sender, - numBatches: numBatches - }); - - // Increment indices for this module and number of validators registered - unchecked { - $.nodeOperatorInfo[msg.sender].epochPrice = epochCurrentPrice; - $.nodeOperatorInfo[msg.sender].validationTime += (msg.value - bondAmountEth); - ++$.nodeOperatorInfo[msg.sender].pendingValidatorCount; - ++$.pendingValidatorIndices[moduleName]; - ++$.moduleLimits[moduleName].numberOfRegisteredValidators; - } - - emit NumberOfRegisteredValidatorsChanged({ - moduleName: moduleName, - newNumberOfRegisteredValidators: $.moduleLimits[moduleName].numberOfRegisteredValidators - }); - emit ValidatorKeyRegistered({ - pubKey: data.blsPubKey, - pufferModuleIndex: pufferModuleIndex, - moduleName: moduleName, - numBatches: numBatches - }); + bytes memory callData = abi.encodeWithSelector( + IPufferProtocolLogic._registerValidatorKey.selector, + data, + moduleName, + totalEpochsValidated, + vtConsumptionSignature, + deadline + ); + _delegatecall(_getPufferProtocolStorage(), callData); } /** @@ -403,34 +282,6 @@ contract PufferProtocol is _PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); } - function _batchHandleWithdrawalsAccounting( - Withdrawals[] memory bondWithdrawals, - StoppedValidatorInfo[] calldata validatorInfos - ) internal { - // In this loop, we transfer back the bonds, and do the accounting that affects the exchange rate - for (uint256 i = 0; i < validatorInfos.length; ++i) { - // If the withdrawal amount is bigger than 32 ETH * numBatches, we cap it to 32 ETH * numBatches - // The excess is the rewards amount for that Node Operator - uint256 transferAmount = validatorInfos[i].withdrawalAmount > (32 ether * bondWithdrawals[i].numBatches) - ? 32 ether * bondWithdrawals[i].numBatches - : validatorInfos[i].withdrawalAmount; - //solhint-disable-next-line avoid-low-level-calls - (bool success,) = - PufferModule(payable(validatorInfos[i].module)).call(address(_PUFFER_VAULT), transferAmount, ""); - if (!success) { - revert Failed(); - } - - // Skip the empty transfer (validator got slashed) - if (bondWithdrawals[i].pufETHAmount == 0) { - continue; - } - // slither-disable-next-line unchecked-transfer - _PUFFER_VAULT.transfer(bondWithdrawals[i].node, bondWithdrawals[i].pufETHAmount); - } - // slither-disable-start calls-loop - } - /** * @inheritdoc IPufferProtocol * @dev Restricted to Puffer Paymaster @@ -440,81 +291,10 @@ contract PufferProtocol is bytes[] calldata guardianEOASignatures, uint256 deadline ) external restricted { - if (block.timestamp > deadline) { - revert DeadlineExceeded(); - } - - _GUARDIAN_MODULE.validateBatchWithdrawals(validatorInfos, guardianEOASignatures, deadline); - - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - BurnAmounts memory burnAmounts; - Withdrawals[] memory bondWithdrawals = new Withdrawals[](validatorInfos.length); - - // 1 batch = 32 ETH - uint256 numExitedBatches; - - // slither-disable-start calls-loop - for (uint256 i = 0; i < validatorInfos.length; ++i) { - Validator storage validator = - $.validators[validatorInfos[i].moduleName][validatorInfos[i].pufferModuleIndex]; - - if (validator.status != Status.ACTIVE) { - revert InvalidValidatorState(validator.status); - } - - // Save the Node address for the bond transfer - bondWithdrawals[i].node = validator.node; - uint256 bondBurnAmount; - - // We need to scope the variables to avoid stack too deep errors - { - uint256 epochValidated = validatorInfos[i].totalEpochsValidated; - bytes[] memory vtConsumptionSignature = validatorInfos[i].vtConsumptionSignature; - burnAmounts.vt += _useVTOrValidationTime( - $, - EpochsValidatedSignature({ - nodeOperator: bondWithdrawals[i].node, - totalEpochsValidated: epochValidated, - functionSelector: _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS, - deadline: deadline, - signatures: vtConsumptionSignature - }) - ); - } - - if (validatorInfos[i].isDownsize) { - // We update the bondWithdrawals - (bondWithdrawals[i].pufETHAmount, bondWithdrawals[i].numBatches) = - _downsizeValidators($, validatorInfos[i], validator); - - numExitedBatches += bondWithdrawals[i].numBatches; - } else { - // Full validator exit - numExitedBatches += validator.numBatches; - bondWithdrawals[i].numBatches = validator.numBatches > 0 ? validator.numBatches : 1; - - // We update the bondWithdrawals - (bondBurnAmount, bondWithdrawals[i].pufETHAmount, bondWithdrawals[i].numBatches) = - _exitValidator($, validatorInfos[i], validator); - } - - // Update the burnAmounts - burnAmounts.pufETH += bondBurnAmount; - } - - if (burnAmounts.vt > 0) { - _VALIDATOR_TICKET.burn(burnAmounts.vt); - } - if (burnAmounts.pufETH > 0) { - // Because we've calculated everything in the previous loop, we can do the burning - _PUFFER_VAULT.burn(burnAmounts.pufETH); - } - - // Deduct 32 ETH per batch from the `lockedETHAmount` on the PufferOracle - _PUFFER_ORACLE.exitValidators(numExitedBatches); - - _batchHandleWithdrawalsAccounting(bondWithdrawals, validatorInfos); + bytes memory callData = abi.encodeWithSelector( + IPufferProtocolLogic._batchHandleWithdrawals.selector, validatorInfos, guardianEOASignatures, deadline + ); + _delegatecall(_getPufferProtocolStorage(), callData); } /** @@ -522,37 +302,9 @@ contract PufferProtocol is * @dev Restricted to Puffer Paymaster */ function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external restricted { - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - uint256 skippedIndex = $.nextToBeProvisioned[moduleName]; - - address node = $.validators[moduleName][skippedIndex].node; - - // Check the signatures (reverts if invalid) - _GUARDIAN_MODULE.validateSkipProvisioning({ - moduleName: moduleName, - skippedIndex: skippedIndex, - guardianEOASignatures: guardianEOASignatures - }); - - uint256 vtPricePerEpoch = _PUFFER_ORACLE.getValidatorTicketPrice(); - - $.nodeOperatorInfo[node].validationTime -= - ($.vtPenaltyEpochs * vtPricePerEpoch * $.validators[moduleName][skippedIndex].numBatches); - --$.nodeOperatorInfo[node].pendingValidatorCount; - - // Change the status of that validator - $.validators[moduleName][skippedIndex].status = Status.SKIPPED; - - // Transfer pufETH to that node operator - // slither-disable-next-line unchecked-transfer - _PUFFER_VAULT.transfer(node, $.validators[moduleName][skippedIndex].bond); - - _decreaseNumberOfRegisteredValidators($, moduleName); - unchecked { - ++$.nextToBeProvisioned[moduleName]; - } - emit ValidatorSkipped($.validators[moduleName][skippedIndex].pubKey, skippedIndex, moduleName); + bytes memory callData = + abi.encodeWithSelector(IPufferProtocolLogic._skipProvisioning.selector, moduleName, guardianEOASignatures); + _delegatecall(_getPufferProtocolStorage(), callData); } /** @@ -805,24 +557,6 @@ contract PufferProtocol is return address(module); } - function _checkValidatorRegistrationInputs( - ProtocolStorage storage $, - ValidatorKeyData calldata data, - bytes32 moduleName - ) internal view { - // Check number of batches between 1 (32 ETH) and 64 (2048 ETH) - require(0 < data.numBatches && data.numBatches < 65, InvalidNumberOfBatches()); - - // This acts as a validation if the module is existent - // +1 is to validate the current transaction registration - require( - ($.moduleLimits[moduleName].numberOfRegisteredValidators + 1) <= $.moduleLimits[moduleName].allowedLimit, - ValidatorLimitForModuleReached() - ); - - require(data.blsPubKey.length == _BLS_PUB_KEY_LENGTH, InvalidBLSPubKey()); - } - function _changeMinimumVTAmount(uint256 newMinimumVtAmount) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); if (newMinimumVtAmount < $.vtPenaltyEpochs) { @@ -832,29 +566,6 @@ contract PufferProtocol is $.minimumVtAmount = newMinimumVtAmount; } - function _getBondBurnAmount( - StoppedValidatorInfo calldata validatorInfo, - uint256 validatorBondAmount, - uint256 numBatches - ) internal view returns (uint256 pufETHBurnAmount) { - // Case 1: - // The Validator was slashed, we burn the whole bond for that validator - if (validatorInfo.wasSlashed) { - return validatorBondAmount; - } - - // Case 2: - // The withdrawal amount is less than 32 ETH * numBatches, we burn the difference to cover up the loss for inactivity - if (validatorInfo.withdrawalAmount < (uint256(32 ether) * numBatches)) { - pufETHBurnAmount = - _PUFFER_VAULT.convertToSharesUp((uint256(32 ether) * numBatches) - validatorInfo.withdrawalAmount); - } - - // Case 3: - // Withdrawal amount was >= 32 ETH * numBatches, we don't burn anything - return pufETHBurnAmount; - } - function _validateSignaturesAndProvisionValidator( ProtocolStorage storage $, bytes32 moduleName, @@ -886,52 +597,6 @@ contract PufferProtocol is ); } - /** - * @dev Internal function to return the deprecated validator tickets burn amount - * and/or consume the validation time from the node operator - * @dev The deprecated vt balance is reduced here but the actual VT is not burned here (for efficiency) - * @param $ The protocol storage - * @param epochsValidatedSignature is a struct that contains: - * - functionSelector: Identifier of the function that initiated this flow - * - totalEpochsValidated: The total number of epochs validated by that node operator - * - nodeOperator: The node operator address - * - deadline: The deadline for the signature - * - signatures: The signatures of the guardians over the total number of epochs validated - * @return vtAmountToBurn The amount of VT to burn - */ - function _useVTOrValidationTime(ProtocolStorage storage $, EpochsValidatedSignature memory epochsValidatedSignature) - internal - returns (uint256 vtAmountToBurn) - { - bytes memory callData = - abi.encodeWithSelector(IPufferProtocolLogic._useVTOrValidationTime.selector, epochsValidatedSignature); - bytes memory result = _delegatecall($, callData); - vtAmountToBurn = abi.decode(result, (uint256)); - } - - /** - * @dev Internal function to settle the VT accounting for a node operator - * @param $ The protocol storage - * @param epochsValidatedSignature is a struct that contains: - * - functionSelector: Identifier of the function that initiated this flow - * - totalEpochsValidated: The total number of epochs validated by that node operator - * - nodeOperator: The node operator address - * - deadline: The deadline for the signature - * - signatures: The signatures of the guardians over the total number of epochs validated - * @param deprecated_burntVTs The amount of VT to burn (to be deducted from validation time consumption) - */ - function _settleVTAccounting( - ProtocolStorage storage $, - EpochsValidatedSignature memory epochsValidatedSignature, - uint256 deprecated_burntVTs - ) internal { - bytes memory callData = abi.encodeWithSelector( - IPufferProtocolLogic._settleVTAccounting.selector, epochsValidatedSignature, deprecated_burntVTs - ); - - _delegatecall($, callData); - } - function _callPermit(address token, Permit calldata permitData) internal { try IERC20Permit(token).permit({ owner: msg.sender, @@ -944,90 +609,6 @@ contract PufferProtocol is }) { } catch { } } - function _decreaseNumberOfRegisteredValidators(ProtocolStorage storage $, bytes32 moduleName) internal { - --$.moduleLimits[moduleName].numberOfRegisteredValidators; - emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); - } - - function _downsizeValidators( - ProtocolStorage storage $, - StoppedValidatorInfo calldata validatorInfo, - Validator storage validator - ) internal returns (uint256 exitingBond, uint256 exitedBatches) { - exitedBatches = validatorInfo.withdrawalAmount / 32 ether; - - uint256 numBatchesBefore = validator.numBatches; - - // We burn the bond according to previous burn rate (before downsize) - uint256 burnAmount = _getBondBurnAmount({ - validatorInfo: validatorInfo, - validatorBondAmount: validator.bond, - numBatches: numBatchesBefore - }); - - exitingBond = (validator.bond * exitedBatches) / validator.numBatches; - - // The burned amount is subtracted from the exiting bond, so the remaining bond is kept in full - // The backend must prevent any downsize that would result in a burned amount greater than the exiting bond - require(exitingBond >= burnAmount, InvalidWithdrawAmount()); - exitingBond -= burnAmount; - - emit ValidatorDownsized({ - pubKey: validator.pubKey, - pufferModuleIndex: validatorInfo.pufferModuleIndex, - moduleName: validatorInfo.moduleName, - pufETHBurnAmount: burnAmount, - epoch: validatorInfo.totalEpochsValidated, - numBatchesBefore: numBatchesBefore, - numBatchesAfter: validator.numBatches - exitedBatches - }); - - $.nodeOperatorInfo[validator.node].numBatches -= SafeCast.toUint8(exitedBatches); - - validator.bond -= SafeCast.toUint96(exitingBond); - validator.numBatches -= SafeCast.toUint8(exitedBatches); - - return (exitingBond, exitedBatches); - } - - function _exitValidator( - ProtocolStorage storage $, - StoppedValidatorInfo calldata validatorInfo, - Validator storage validator - ) internal returns (uint256 bondBurnAmount, uint256 bondReturnAmount, uint256 exitedBatches) { - uint96 bondAmount = validator.bond; - uint256 numBatches = validator.numBatches; - - // Get the bondBurnAmount for the withdrawal at the current exchange rate - bondBurnAmount = _getBondBurnAmount({ - validatorInfo: validatorInfo, - validatorBondAmount: bondAmount, - numBatches: validator.numBatches - }); - - emit ValidatorExited({ - pubKey: validator.pubKey, - pufferModuleIndex: validatorInfo.pufferModuleIndex, - moduleName: validatorInfo.moduleName, - pufETHBurnAmount: bondBurnAmount, - numBatches: numBatches - }); - - // Decrease the number of registered validators for that module - _decreaseNumberOfRegisteredValidators($, validatorInfo.moduleName); - - // Storage VT and the active validator count update for the Node Operator - // nosemgrep basic-arithmetic-underflow - --$.nodeOperatorInfo[validator.node].activeValidatorCount; - $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; - - delete $.validators[validatorInfo.moduleName][ - validatorInfo.pufferModuleIndex - ]; - // nosemgrep basic-arithmetic-underflow - return (bondBurnAmount, bondAmount - bondBurnAmount, numBatches); - } - function _setPufferProtocolLogic(address newPufferProtocolLogic) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); emit PufferProtocolLogicSet($.pufferProtocolLogic, newPufferProtocolLogic); diff --git a/mainnet-contracts/src/ProtocolConstants.sol b/mainnet-contracts/src/PufferProtocolBase.sol similarity index 96% rename from mainnet-contracts/src/ProtocolConstants.sol rename to mainnet-contracts/src/PufferProtocolBase.sol index 7a475e59..a8d157d1 100644 --- a/mainnet-contracts/src/ProtocolConstants.sol +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -9,8 +9,10 @@ import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; +import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; +import { PufferProtocolStorage } from "./PufferProtocolStorage.sol"; -abstract contract ProtocolConstants { +abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignatureNonces { /** * @notice Thrown when the deposit state that is provided doesn't match the one on Beacon deposit contract */ diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 7098cac5..c1992695 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -3,22 +3,47 @@ pragma solidity >=0.8.0 <0.9.0; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import { PufferProtocolStorage } from "./PufferProtocolStorage.sol"; import { ProtocolStorage } from "./struct/ProtocolStorage.sol"; -import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; import { Validator } from "./struct/Validator.sol"; import { Status } from "./struct/Validator.sol"; -import { ProtocolConstants } from "./ProtocolConstants.sol"; +import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; +import { ValidatorKeyData } from "./struct/ValidatorKeyData.sol"; +import { PufferProtocolBase } from "./PufferProtocolBase.sol"; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; +import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; +import { PufferModule } from "./PufferModule.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; +import { InvalidAddress } from "./Errors.sol"; + +contract PufferProtocolLogic is + PufferProtocolBase, + IPufferProtocolLogic +{ + /** + * @dev Helper struct for the full withdrawals accounting + * The amounts of VT and pufETH to burn at the end of the withdrawal + */ + struct BurnAmounts { + uint256 vt; + uint256 pufETH; + } + + /** + * @dev Helper struct for the full withdrawals accounting + * The amounts of pufETH to send to the node operator + */ + struct Withdrawals { + uint256 pufETHAmount; + address node; + uint256 numBatches; + } -contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, ProtocolConstants { constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, @@ -28,7 +53,7 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, address beaconDepositContract, address payable pufferRevenueDistributor ) - ProtocolConstants( + PufferProtocolBase( pufferVault, guardianModule, moduleManager, @@ -39,12 +64,160 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, ) { } + /** + * @notice Check IPufferProtocol.depositValidationTime + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ + function _depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) + external + payable + override + { + if (block.timestamp > epochsValidatedSignature.deadline) { + revert DeadlineExceeded(); + } + + require(epochsValidatedSignature.nodeOperator != address(0), InvalidAddress()); + ProtocolStorage storage $ = _getPufferProtocolStorage(); + uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); + uint8 operatorNumBatches = $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].numBatches; + require( + msg.value >= operatorNumBatches * _MINIMUM_EPOCHS_VALIDATION_DEPOSIT * epochCurrentPrice + && msg.value <= operatorNumBatches * _MAXIMUM_EPOCHS_VALIDATION_DEPOSIT * epochCurrentPrice, + InvalidETHAmount() + ); + + epochsValidatedSignature.functionSelector = _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME; + + uint256 burnAmount = _useVTOrValidationTime($, epochsValidatedSignature); + + if (burnAmount > 0) { + _VALIDATOR_TICKET.burn(burnAmount); + } + + $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].validationTime += SafeCast.toUint96(msg.value); + emit IPufferProtocol.ValidationTimeDeposited({ + node: epochsValidatedSignature.nodeOperator, + ethAmount: msg.value + }); + } + + /** + * @notice Check IPufferProtocol.withdrawValidationTime + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ + function _withdrawValidationTime(uint96 amount, address recipient) external override { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + // Node operator can only withdraw if they have no active or pending validators + // In the future, we plan to allow node operators to withdraw VTs even if they have active/pending validators. + if ( + $.nodeOperatorInfo[msg.sender].activeValidatorCount + $.nodeOperatorInfo[msg.sender].pendingValidatorCount + != 0 + ) { + revert ActiveOrPendingValidatorsExist(); + } + + // Reverts if insufficient balance + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[msg.sender].validationTime -= amount; + + // WETH is a contract that has a fallback function that accepts ETH, and never reverts + address weth = _PUFFER_VAULT.asset(); + weth.call{ value: amount }(""); + // Transfer WETH to the recipient + ERC20(weth).transfer(recipient, amount); + + emit IPufferProtocol.ValidationTimeWithdrawn(msg.sender, recipient, amount); + } + + /** + * @notice Check IPufferProtocol.registerValidatorKey + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ + function _registerValidatorKey( + ValidatorKeyData calldata data, + bytes32 moduleName, + uint256 totalEpochsValidated, + bytes[] calldata vtConsumptionSignature, + uint256 deadline + ) external payable override { + if (block.timestamp > deadline) { + revert DeadlineExceeded(); + } + + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); + + uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); + uint8 numBatches = data.numBatches; + uint256 bondAmountEth = _VALIDATOR_BOND * numBatches; + + // The node operator must deposit 1.5 ETH (per batch) or more + minimum validation time for ~30 days + // At the moment that's roughly 30 days * 225 (there is roughly 225 epochs per day) + uint256 minimumETHRequired = + bondAmountEth + (numBatches * _MINIMUM_EPOCHS_VALIDATION_REGISTRATION * epochCurrentPrice); + + require(msg.value >= minimumETHRequired, InvalidETHAmount()); + + emit IPufferProtocol.ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - bondAmountEth) }); + + _settleVTAccounting({ + $: $, + epochsValidatedSignature: EpochsValidatedSignature({ + nodeOperator: msg.sender, + totalEpochsValidated: totalEpochsValidated, + functionSelector: _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY, + deadline: deadline, + signatures: vtConsumptionSignature + }), + deprecated_burntVTs: 0 + }); + + // The bond is converted to pufETH at the current exchange rate + uint256 pufETHBondAmount = _PUFFER_VAULT.depositETH{ value: bondAmountEth }(address(this)); + + uint256 pufferModuleIndex = $.pendingValidatorIndices[moduleName]; + + // No need for SafeCast + $.validators[moduleName][pufferModuleIndex] = Validator({ + pubKey: data.blsPubKey, + status: Status.PENDING, + module: address($.modules[moduleName]), + bond: uint96(pufETHBondAmount), + node: msg.sender, + numBatches: numBatches + }); + + // Increment indices for this module and number of validators registered + unchecked { + $.nodeOperatorInfo[msg.sender].epochPrice = epochCurrentPrice; + $.nodeOperatorInfo[msg.sender].validationTime += (msg.value - bondAmountEth); + ++$.nodeOperatorInfo[msg.sender].pendingValidatorCount; + ++$.pendingValidatorIndices[moduleName]; + ++$.moduleLimits[moduleName].numberOfRegisteredValidators; + } + + emit IPufferProtocol.NumberOfRegisteredValidatorsChanged({ + moduleName: moduleName, + newNumberOfRegisteredValidators: $.moduleLimits[moduleName].numberOfRegisteredValidators + }); + emit IPufferProtocol.ValidatorKeyRegistered({ + pubKey: data.blsPubKey, + pufferModuleIndex: pufferModuleIndex, + moduleName: moduleName, + numBatches: numBatches + }); + } + /** * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable + override { if (srcIndices.length == 0) { revert InputArrayLengthZero(); @@ -82,63 +255,127 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, } /** - * @dev Internal function to return the deprecated validator tickets burn amount - * and/or consume the validation time from the node operator - * @dev The deprecated vt balance is reduced here but the actual VT is not burned here (for efficiency) - * @param epochsValidatedSignature is a struct that contains: - * - functionSelector: Identifier of the function that initiated this flow - * - totalEpochsValidated: The total number of epochs validated by that node operator - * - nodeOperator: The node operator address - * - deadline: The deadline for the signature - * - signatures: The signatures of the guardians over the total number of epochs validated - * @return vtAmountToBurn The amount of VT to burn + * @notice Check IPufferProtocol.skipProvisioning + * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _useVTOrValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) - external - payable - returns (uint256 vtAmountToBurn) - { + function _skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external override { ProtocolStorage storage $ = _getPufferProtocolStorage(); - address nodeOperator = epochsValidatedSignature.nodeOperator; - uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[nodeOperator].totalEpochsValidated; - if (previousTotalEpochsValidated == epochsValidatedSignature.totalEpochsValidated) { - return 0; + uint256 skippedIndex = $.nextToBeProvisioned[moduleName]; + + address node = $.validators[moduleName][skippedIndex].node; + + // Check the signatures (reverts if invalid) + _GUARDIAN_MODULE.validateSkipProvisioning({ + moduleName: moduleName, + skippedIndex: skippedIndex, + guardianEOASignatures: guardianEOASignatures + }); + + uint256 vtPricePerEpoch = _PUFFER_ORACLE.getValidatorTicketPrice(); + + $.nodeOperatorInfo[node].validationTime -= + ($.vtPenaltyEpochs * vtPricePerEpoch * $.validators[moduleName][skippedIndex].numBatches); + --$.nodeOperatorInfo[node].pendingValidatorCount; + + // Change the status of that validator + $.validators[moduleName][skippedIndex].status = Status.SKIPPED; + + // Transfer pufETH to that node operator + // slither-disable-next-line unchecked-transfer + _PUFFER_VAULT.transfer(node, $.validators[moduleName][skippedIndex].bond); + + _decreaseNumberOfRegisteredValidators($, moduleName); + unchecked { + ++$.nextToBeProvisioned[moduleName]; } - require( - previousTotalEpochsValidated < epochsValidatedSignature.totalEpochsValidated, InvalidTotalEpochsValidated() - ); + emit IPufferProtocol.ValidatorSkipped($.validators[moduleName][skippedIndex].pubKey, skippedIndex, moduleName); + } - // Burn the VT first, then fallback to ETH from the node operator - uint256 nodeVTBalance = $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance; + /** + * @notice Check IPufferProtocol.batchHandleWithdrawals + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ + function _batchHandleWithdrawals( + StoppedValidatorInfo[] calldata validatorInfos, + bytes[] calldata guardianEOASignatures, + uint256 deadline + ) external payable override { + if (block.timestamp > deadline) { + revert DeadlineExceeded(); + } - // If the node operator has VT, we burn it first - if (nodeVTBalance > 0) { - uint256 vtBurnAmount = - _getVTBurnAmount(epochsValidatedSignature.totalEpochsValidated - previousTotalEpochsValidated); - if (nodeVTBalance >= vtBurnAmount) { - // Burn the VT first, and update the node operator VT balance - vtAmountToBurn = vtBurnAmount; - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); + _GUARDIAN_MODULE.validateBatchWithdrawals(validatorInfos, guardianEOASignatures, deadline); - emit IPufferProtocol.ValidationTimeConsumed({ - node: nodeOperator, - consumedAmount: 0, - deprecated_burntVTs: vtBurnAmount - }); + ProtocolStorage storage $ = _getPufferProtocolStorage(); - return vtAmountToBurn; + BurnAmounts memory burnAmounts; + Withdrawals[] memory bondWithdrawals = new Withdrawals[](validatorInfos.length); + + // 1 batch = 32 ETH + uint256 numExitedBatches; + + // slither-disable-start calls-loop + for (uint256 i = 0; i < validatorInfos.length; ++i) { + Validator storage validator = + $.validators[validatorInfos[i].moduleName][validatorInfos[i].pufferModuleIndex]; + + if (validator.status != Status.ACTIVE) { + revert InvalidValidatorState(validator.status); } - // If the node operator has less VT than the amount to burn, we burn all of it, and we use the validation time - vtAmountToBurn = nodeVTBalance; - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); + // Save the Node address for the bond transfer + bondWithdrawals[i].node = validator.node; + uint256 bondBurnAmount; + + // We need to scope the variables to avoid stack too deep errors + { + uint256 epochValidated = validatorInfos[i].totalEpochsValidated; + bytes[] memory vtConsumptionSignature = validatorInfos[i].vtConsumptionSignature; + burnAmounts.vt += _useVTOrValidationTime( + $, + EpochsValidatedSignature({ + nodeOperator: bondWithdrawals[i].node, + totalEpochsValidated: epochValidated, + functionSelector: _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS, + deadline: deadline, + signatures: vtConsumptionSignature + }) + ); + } + + if (validatorInfos[i].isDownsize) { + // We update the bondWithdrawals + (bondWithdrawals[i].pufETHAmount, bondWithdrawals[i].numBatches) = + _downsizeValidators($, validatorInfos[i], validator); + + numExitedBatches += bondWithdrawals[i].numBatches; + } else { + // Full validator exit + numExitedBatches += validator.numBatches; + bondWithdrawals[i].numBatches = validator.numBatches > 0 ? validator.numBatches : 1; + + // We update the bondWithdrawals + (bondBurnAmount, bondWithdrawals[i].pufETHAmount, bondWithdrawals[i].numBatches) = + _exitValidator($, validatorInfos[i], validator); + } + + // Update the burnAmounts + burnAmounts.pufETH += bondBurnAmount; } - // If the node operator has no VT, we use the validation time - _settleVTAccounting({ epochsValidatedSignature: epochsValidatedSignature, deprecated_burntVTs: nodeVTBalance }); + if (burnAmounts.vt > 0) { + _VALIDATOR_TICKET.burn(burnAmounts.vt); + } + if (burnAmounts.pufETH > 0) { + // Because we've calculated everything in the previous loop, we can do the burning + _PUFFER_VAULT.burn(burnAmounts.pufETH); + } + + // Deduct 32 ETH per batch from the `lockedETHAmount` on the PufferOracle + _PUFFER_ORACLE.exitValidators(numExitedBatches); + + _batchHandleWithdrawalsAccounting(bondWithdrawals, validatorInfos); } /** @@ -151,11 +388,11 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, * - signatures: The signatures of the guardians over the total number of epochs validated * @param deprecated_burntVTs The amount of VT to burn (to be deducted from validation time consumption) */ - function _settleVTAccounting(EpochsValidatedSignature memory epochsValidatedSignature, uint256 deprecated_burntVTs) - public - payable - { - ProtocolStorage storage $ = _getPufferProtocolStorage(); + function _settleVTAccounting( + ProtocolStorage storage $, + EpochsValidatedSignature memory epochsValidatedSignature, + uint256 deprecated_burntVTs + ) internal { address node = epochsValidatedSignature.nodeOperator; // There is nothing to settle if this is the first validator for the node operator if ($.nodeOperatorInfo[node].activeValidatorCount + $.nodeOperatorInfo[node].pendingValidatorCount == 0) { @@ -203,6 +440,68 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, ERC20(weth).transfer(_PUFFER_REVENUE_DISTRIBUTOR, validationTimeToConsume); } + /** + * @dev Internal function to return the deprecated validator tickets burn amount + * and/or consume the validation time from the node operator + * @dev The deprecated vt balance is reduced here but the actual VT is not burned here (for efficiency) + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Identifier of the function that initiated this flow + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated + * @return vtAmountToBurn The amount of VT to burn + */ + function _useVTOrValidationTime(ProtocolStorage storage $, EpochsValidatedSignature memory epochsValidatedSignature) + internal + returns (uint256 vtAmountToBurn) + { + address nodeOperator = epochsValidatedSignature.nodeOperator; + uint256 previousTotalEpochsValidated = $.nodeOperatorInfo[nodeOperator].totalEpochsValidated; + + if (previousTotalEpochsValidated == epochsValidatedSignature.totalEpochsValidated) { + return 0; + } + require( + previousTotalEpochsValidated < epochsValidatedSignature.totalEpochsValidated, InvalidTotalEpochsValidated() + ); + + // Burn the VT first, then fallback to ETH from the node operator + uint256 nodeVTBalance = $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance; + + // If the node operator has VT, we burn it first + if (nodeVTBalance > 0) { + uint256 vtBurnAmount = + _getVTBurnAmount(epochsValidatedSignature.totalEpochsValidated - previousTotalEpochsValidated); + if (nodeVTBalance >= vtBurnAmount) { + // Burn the VT first, and update the node operator VT balance + vtAmountToBurn = vtBurnAmount; + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); + + emit IPufferProtocol.ValidationTimeConsumed({ + node: nodeOperator, + consumedAmount: 0, + deprecated_burntVTs: vtBurnAmount + }); + + return vtAmountToBurn; + } + + // If the node operator has less VT than the amount to burn, we burn all of it, and we use the validation time + vtAmountToBurn = nodeVTBalance; + // nosemgrep basic-arithmetic-underflow + $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(nodeVTBalance); + } + + // If the node operator has no VT, we use the validation time + _settleVTAccounting({ + $: $, + epochsValidatedSignature: epochsValidatedSignature, + deprecated_burntVTs: nodeVTBalance + }); + } + /** * @dev Internal function to get the amount of VT to burn during a number of epochs * @param validatedEpochs The number of epochs validated by the node operator (not necessarily the total epochs) @@ -213,4 +512,159 @@ contract PufferProtocolLogic is PufferProtocolStorage, ProtocolSignatureNonces, // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up return validatedEpochs * 4444444444444445; } + + function _batchHandleWithdrawalsAccounting( + Withdrawals[] memory bondWithdrawals, + StoppedValidatorInfo[] calldata validatorInfos + ) internal { + // In this loop, we transfer back the bonds, and do the accounting that affects the exchange rate + for (uint256 i = 0; i < validatorInfos.length; ++i) { + // If the withdrawal amount is bigger than 32 ETH * numBatches, we cap it to 32 ETH * numBatches + // The excess is the rewards amount for that Node Operator + uint256 transferAmount = validatorInfos[i].withdrawalAmount > (32 ether * bondWithdrawals[i].numBatches) + ? 32 ether * bondWithdrawals[i].numBatches + : validatorInfos[i].withdrawalAmount; + //solhint-disable-next-line avoid-low-level-calls + (bool success,) = + PufferModule(payable(validatorInfos[i].module)).call(address(_PUFFER_VAULT), transferAmount, ""); + if (!success) { + revert Failed(); + } + + // Skip the empty transfer (validator got slashed) + if (bondWithdrawals[i].pufETHAmount == 0) { + continue; + } + // slither-disable-next-line unchecked-transfer + _PUFFER_VAULT.transfer(bondWithdrawals[i].node, bondWithdrawals[i].pufETHAmount); + } + // slither-disable-start calls-loop + } + + function _downsizeValidators( + ProtocolStorage storage $, + StoppedValidatorInfo calldata validatorInfo, + Validator storage validator + ) internal returns (uint256 exitingBond, uint256 exitedBatches) { + exitedBatches = validatorInfo.withdrawalAmount / 32 ether; + + uint256 numBatchesBefore = validator.numBatches; + + // We burn the bond according to previous burn rate (before downsize) + uint256 burnAmount = _getBondBurnAmount({ + validatorInfo: validatorInfo, + validatorBondAmount: validator.bond, + numBatches: numBatchesBefore + }); + + exitingBond = (validator.bond * exitedBatches) / validator.numBatches; + + // The burned amount is subtracted from the exiting bond, so the remaining bond is kept in full + // The backend must prevent any downsize that would result in a burned amount greater than the exiting bond + require(exitingBond >= burnAmount, InvalidWithdrawAmount()); + exitingBond -= burnAmount; + + emit IPufferProtocol.ValidatorDownsized({ + pubKey: validator.pubKey, + pufferModuleIndex: validatorInfo.pufferModuleIndex, + moduleName: validatorInfo.moduleName, + pufETHBurnAmount: burnAmount, + epoch: validatorInfo.totalEpochsValidated, + numBatchesBefore: numBatchesBefore, + numBatchesAfter: validator.numBatches - exitedBatches + }); + + $.nodeOperatorInfo[validator.node].numBatches -= SafeCast.toUint8(exitedBatches); + + validator.bond -= SafeCast.toUint96(exitingBond); + validator.numBatches -= SafeCast.toUint8(exitedBatches); + + return (exitingBond, exitedBatches); + } + + function _exitValidator( + ProtocolStorage storage $, + StoppedValidatorInfo calldata validatorInfo, + Validator storage validator + ) internal returns (uint256 bondBurnAmount, uint256 bondReturnAmount, uint256 exitedBatches) { + uint96 bondAmount = validator.bond; + uint256 numBatches = validator.numBatches; + + // Get the bondBurnAmount for the withdrawal at the current exchange rate + bondBurnAmount = _getBondBurnAmount({ + validatorInfo: validatorInfo, + validatorBondAmount: bondAmount, + numBatches: validator.numBatches + }); + + emit IPufferProtocol.ValidatorExited({ + pubKey: validator.pubKey, + pufferModuleIndex: validatorInfo.pufferModuleIndex, + moduleName: validatorInfo.moduleName, + pufETHBurnAmount: bondBurnAmount, + numBatches: numBatches + }); + + // Decrease the number of registered validators for that module + _decreaseNumberOfRegisteredValidators($, validatorInfo.moduleName); + + // Storage VT and the active validator count update for the Node Operator + // nosemgrep basic-arithmetic-underflow + --$.nodeOperatorInfo[validator.node].activeValidatorCount; + $.nodeOperatorInfo[validator.node].numBatches -= validator.numBatches; + + delete $.validators[validatorInfo.moduleName][ + validatorInfo.pufferModuleIndex + ]; + // nosemgrep basic-arithmetic-underflow + return (bondBurnAmount, bondAmount - bondBurnAmount, numBatches); + } + + function _decreaseNumberOfRegisteredValidators(ProtocolStorage storage $, bytes32 moduleName) internal { + --$.moduleLimits[moduleName].numberOfRegisteredValidators; + emit IPufferProtocol.NumberOfRegisteredValidatorsChanged( + moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators + ); + } + + function _getBondBurnAmount( + StoppedValidatorInfo calldata validatorInfo, + uint256 validatorBondAmount, + uint256 numBatches + ) internal view returns (uint256 pufETHBurnAmount) { + // Case 1: + // The Validator was slashed, we burn the whole bond for that validator + if (validatorInfo.wasSlashed) { + return validatorBondAmount; + } + + // Case 2: + // The withdrawal amount is less than 32 ETH * numBatches, we burn the difference to cover up the loss for inactivity + if (validatorInfo.withdrawalAmount < (uint256(32 ether) * numBatches)) { + pufETHBurnAmount = + _PUFFER_VAULT.convertToSharesUp((uint256(32 ether) * numBatches) - validatorInfo.withdrawalAmount); + } + + // Case 3: + // Withdrawal amount was >= 32 ETH * numBatches, we don't burn anything + return pufETHBurnAmount; + } + + function _checkValidatorRegistrationInputs( + ProtocolStorage storage $, + ValidatorKeyData calldata data, + bytes32 moduleName + ) internal view { + // Check number of batches between 1 (32 ETH) and 64 (2048 ETH) + require(0 < data.numBatches && data.numBatches < 65, InvalidNumberOfBatches()); + + // This acts as a validation if the module is existent + // +1 is to validate the current transaction registration + require( + ($.moduleLimits[moduleName].numberOfRegisteredValidators + 1) <= $.moduleLimits[moduleName].allowedLimit, + ValidatorLimitForModuleReached() + ); + + require(data.blsPubKey.length == _BLS_PUB_KEY_LENGTH, InvalidBLSPubKey()); + } } diff --git a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol index 82c877ae..628e41d1 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -2,40 +2,55 @@ pragma solidity >=0.8.0 <0.9.0; import { EpochsValidatedSignature } from "../struct/Signatures.sol"; +import { StoppedValidatorInfo } from "../struct/StoppedValidatorInfo.sol"; +import { ValidatorKeyData } from "../struct/ValidatorKeyData.sol"; interface IPufferProtocolLogic { + /** + * @notice Check IPufferProtocol.depositValidationTime + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ + function _depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external payable; + + /** + * @notice Check IPufferProtocol.withdrawValidationTime + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ + function _withdrawValidationTime(uint96 amount, address recipient) external; + + /** + * @notice Check IPufferProtocol.registerValidatorKey + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ + function _registerValidatorKey( + ValidatorKeyData calldata data, + bytes32 moduleName, + uint256 totalEpochsValidated, + bytes[] calldata vtConsumptionSignature, + uint256 deadline + ) external payable; + + /** + * @notice Check IPufferProtocol.requestConsolidation + * @dev This function should only be called by the PufferProtocol contract through a delegatecall + */ function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable; /** - * @dev Internal function to return the deprecated validator tickets burn amount - * and/or consume the validation time from the node operator - * @dev The deprecated vt balance is reduced here but the actual VT is not burned here (for efficiency) - * @param epochsValidatedSignature is a struct that contains: - * - functionSelector: Identifier of the function that initiated this flow - * - totalEpochsValidated: The total number of epochs validated by that node operator - * - nodeOperator: The node operator address - * - deadline: The deadline for the signature - * - signatures: The signatures of the guardians over the total number of epochs validated - * @return vtAmountToBurn The amount of VT to burn + * @notice Check IPufferProtocol.skipProvisioning + * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _useVTOrValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) - external - payable - returns (uint256 vtAmountToBurn); + function _skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external; /** - * @dev Internal function to settle the VT accounting for a node operator - * @param epochsValidatedSignature is a struct that contains: - * - functionSelector: Identifier of the function that initiated this flow - * - totalEpochsValidated: The total number of epochs validated by that node operator - * - nodeOperator: The node operator address - * - deadline: The deadline for the signature - * - signatures: The signatures of the guardians over the total number of epochs validated - * @param deprecated_burntVTs The amount of VT to burn (to be deducted from validation time consumption) + * @notice Check IPufferProtocol.batchHandleWithdrawals + * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _settleVTAccounting(EpochsValidatedSignature memory epochsValidatedSignature, uint256 deprecated_burntVTs) - external - payable; + function _batchHandleWithdrawals( + StoppedValidatorInfo[] calldata validatorInfos, + bytes[] calldata guardianEOASignatures, + uint256 deadline + ) external payable; } diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index feda0326..f06db259 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -9,7 +9,7 @@ import { ValidatorKeyData } from "../../src/struct/ValidatorKeyData.sol"; import { Status } from "../../src/struct/Status.sol"; import { Validator } from "../../src/struct/Validator.sol"; import { PufferProtocol } from "../../src/PufferProtocol.sol"; -import { ProtocolConstants } from "../../src/ProtocolConstants.sol"; +import { PufferProtocolBase } from "../../src/PufferProtocolBase.sol"; import { PufferModule } from "../../src/PufferModule.sol"; import { PufferRevenueDepositor } from "../../src/PufferRevenueDepositor.sol"; import { @@ -26,7 +26,6 @@ import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol" import { NodeInfo } from "../../src/struct/NodeInfo.sol"; import { EpochsValidatedSignature } from "../../src/struct/Signatures.sol"; - contract PufferProtocolTest is UnitTestHelper { using ECDSA for bytes32; @@ -188,7 +187,7 @@ contract PufferProtocolTest is UnitTestHelper { // Create an existing module should revert function test_create_existing_module_fails() public { vm.startPrank(DAO); - vm.expectRevert(ProtocolConstants.ModuleAlreadyExists.selector); + vm.expectRevert(PufferProtocolBase.ModuleAlreadyExists.selector); pufferProtocol.createPufferModule(PUFFER_MODULE_0); } @@ -197,7 +196,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 smoothingCommitment = pufferOracle.getValidatorTicketPrice() * 30; bytes memory pubKey = _getPubKey(bytes32("charlie")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - vm.expectRevert(ProtocolConstants.ValidatorLimitForModuleReached.selector); + vm.expectRevert(PufferProtocolBase.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( validatorKeyData, bytes32("imaginary module"), 0, new bytes[](0), block.timestamp + 1 days ); @@ -248,14 +247,14 @@ contract PufferProtocolTest is UnitTestHelper { numBatches: 0 }); - vm.expectRevert(ProtocolConstants.InvalidNumberOfBatches.selector); + vm.expectRevert(PufferProtocolBase.InvalidNumberOfBatches.selector); pufferProtocol.registerValidatorKey{ value: vtPrice }( validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); validatorData.numBatches = 65; - vm.expectRevert(ProtocolConstants.InvalidNumberOfBatches.selector); + vm.expectRevert(PufferProtocolBase.InvalidNumberOfBatches.selector); pufferProtocol.registerValidatorKey{ value: vtPrice }( validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -279,7 +278,7 @@ contract PufferProtocolTest is UnitTestHelper { numBatches: 1 }); - vm.expectRevert(ProtocolConstants.InvalidBLSPubKey.selector); + vm.expectRevert(PufferProtocolBase.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -300,7 +299,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory validatorSignature = _validatorSignature(); - vm.expectRevert(ProtocolConstants.InvalidDepositRootHash.selector); + vm.expectRevert(PufferProtocolBase.InvalidDepositRootHash.selector); pufferProtocol.provisionNode(validatorSignature, bytes32("badDepositRoot")); // "depositRoot" is hardcoded in the mock // now it works @@ -601,7 +600,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory pubKey = _getPubKey(bytes32("bob")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - vm.expectRevert(ProtocolConstants.ValidatorLimitForModuleReached.selector); + vm.expectRevert(PufferProtocolBase.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + BOND) }( validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -819,7 +818,7 @@ contract PufferProtocolTest is UnitTestHelper { // Register Validator key registers validator with 30 VTs _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); - vm.expectRevert(ProtocolConstants.ActiveOrPendingValidatorsExist.selector); + vm.expectRevert(PufferProtocolBase.ActiveOrPendingValidatorsExist.selector); pufferProtocol.withdrawValidatorTickets(30 ether, alice); } @@ -868,13 +867,13 @@ contract PufferProtocolTest is UnitTestHelper { function test_setVTPenalty_bigger_than_minimum_VT_amount() public { vm.startPrank(DAO); - vm.expectRevert(ProtocolConstants.InvalidVTAmount.selector); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); pufferProtocol.setVTPenalty(50 * EPOCHS_PER_DAY); } function test_changeMinimumVTAmount_lower_than_penalty() public { vm.startPrank(DAO); - vm.expectRevert(ProtocolConstants.InvalidVTAmount.selector); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); pufferProtocol.changeMinimumVTAmount(9 * EPOCHS_PER_DAY); } @@ -1001,7 +1000,7 @@ contract PufferProtocolTest is UnitTestHelper { ); // We've removed the validator data, meaning the validator status is 0 (UNINITIALIZED) - vm.expectRevert(abi.encodeWithSelector(ProtocolConstants.InvalidValidatorState.selector, 0)); + vm.expectRevert(abi.encodeWithSelector(PufferProtocolBase.InvalidValidatorState.selector, 0)); _executeFullWithdrawal( StoppedValidatorInfo({ module: NoRestakingModule, @@ -2118,7 +2117,7 @@ contract PufferProtocolTest is UnitTestHelper { function test_setVTPenalty_invalid_amount() public { vm.startPrank(DAO); - vm.expectRevert(ProtocolConstants.InvalidVTAmount.selector); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); pufferProtocol.setVTPenalty(type(uint256).max); } @@ -2126,7 +2125,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory invalidPubKey = new bytes(47); // Invalid length ValidatorKeyData memory data = _getMockValidatorKeyData(invalidPubKey, PUFFER_MODULE_0); - vm.expectRevert(ProtocolConstants.InvalidBLSPubKey.selector); + vm.expectRevert(PufferProtocolBase.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: 3 ether }( data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -2134,7 +2133,7 @@ contract PufferProtocolTest is UnitTestHelper { function test_changeMinimumVTAmount_invalid_amount() public { vm.startPrank(DAO); - vm.expectRevert(ProtocolConstants.InvalidVTAmount.selector); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); pufferProtocol.changeMinimumVTAmount(0); } From ec3e5feedeb3ba52c6879054d8ff194734920654 Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Tue, 8 Jul 2025 17:52:49 +0000 Subject: [PATCH 62/82] forge fmt --- mainnet-contracts/src/PufferProtocol.sol | 7 +------ mainnet-contracts/src/PufferProtocolLogic.sol | 5 +---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index bff1a327..39da3065 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -35,12 +35,7 @@ import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ -contract PufferProtocol is - IPufferProtocol, - AccessManagedUpgradeable, - UUPSUpgradeable, - PufferProtocolBase -{ +contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolBase { constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index c1992695..3b3ed288 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -21,10 +21,7 @@ import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; import { InvalidAddress } from "./Errors.sol"; -contract PufferProtocolLogic is - PufferProtocolBase, - IPufferProtocolLogic -{ +contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { /** * @dev Helper struct for the full withdrawals accounting * The amounts of VT and pufETH to burn at the end of the withdrawal From 9d2251fdf5ac714c38a34f597aeb7a6c781fd67f Mon Sep 17 00:00:00 2001 From: Eladio Date: Thu, 10 Jul 2025 14:00:29 +0200 Subject: [PATCH 63/82] Removed flow to deposit VT with Permit --- mainnet-contracts/src/PufferProtocol.sol | 31 +---- mainnet-contracts/src/PufferProtocolLogic.sol | 2 - .../src/interface/IPufferProtocol.sol | 2 +- .../test/unit/PufferProtocol.t.sol | 129 ++++-------------- 4 files changed, 29 insertions(+), 135 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 39da3065..676aa326 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -5,26 +5,23 @@ import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; import { ValidatorKeyData } from "./struct/ValidatorKeyData.sol"; import { Validator } from "./struct/Validator.sol"; -import { Permit } from "./structs/Permit.sol"; import { Status } from "./struct/Status.sol"; import { WithdrawalType } from "./struct/WithdrawalType.sol"; import { ProtocolStorage, NodeInfo, ModuleLimit } from "./struct/ProtocolStorage.sol"; import { LibBeaconchainContract } from "./LibBeaconchainContract.sol"; -import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { PufferProtocolBase } from "./PufferProtocolBase.sol"; import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; @@ -79,23 +76,17 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ - function depositValidatorTickets(Permit calldata permit, address node) external restricted { + function depositValidatorTickets(address node, uint256 amount) external restricted { if (node == address(0)) { revert InvalidAddress(); } - // owner: msg.sender is intentional - // We only want the owner of the Permit signature to be able to deposit using the signature - // For an invalid signature, the permit will revert, but it is wrapped in try/catch, meaning the transaction execution - // will continue. If the `msg.sender` did a `VALIDATOR_TICKET.approve(spender, amount)` before calling this - // And the spender is `msg.sender` the Permit call will revert, but the overall transaction will succeed - _callPermit(address(_VALIDATOR_TICKET), permit); // slither-disable-next-line unchecked-transfer - _VALIDATOR_TICKET.transferFrom(msg.sender, address(this), permit.amount); + _VALIDATOR_TICKET.transferFrom(msg.sender, address(this), amount); ProtocolStorage storage $ = _getPufferProtocolStorage(); - $.nodeOperatorInfo[node].deprecated_vtBalance += SafeCast.toUint96(permit.amount); - emit ValidatorTicketsDeposited(node, msg.sender, permit.amount); + $.nodeOperatorInfo[node].deprecated_vtBalance += SafeCast.toUint96(amount); + emit ValidatorTicketsDeposited(node, msg.sender, amount); } /** @@ -592,18 +583,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad ); } - function _callPermit(address token, Permit calldata permitData) internal { - try IERC20Permit(token).permit({ - owner: msg.sender, - spender: address(this), - value: permitData.amount, - deadline: permitData.deadline, - v: permitData.v, - s: permitData.s, - r: permitData.r - }) { } catch { } - } - function _setPufferProtocolLogic(address newPufferProtocolLogic) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); emit PufferProtocolLogicSet($.pufferProtocolLogic, newPufferProtocolLogic); diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 3b3ed288..491f5d5f 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -11,11 +11,9 @@ import { ValidatorKeyData } from "./struct/ValidatorKeyData.sol"; import { PufferProtocolBase } from "./PufferProtocolBase.sol"; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; -import { PufferModuleManager } from "./PufferModuleManager.sol"; import { PufferModule } from "./PufferModule.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; import { IGuardianModule } from "./interface/IGuardianModule.sol"; -import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 2b89c15a..5f23b620 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -202,7 +202,7 @@ interface IPufferProtocol { * @notice Deposits Validator Tickets for the `node` * DEPRECATED - This method is deprecated and will be removed in the future upgrade */ - function depositValidatorTickets(Permit calldata permit, address node) external; + function depositValidatorTickets(address node, uint256 vtAmount) external; /** * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`). diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index f06db259..15f98874 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -20,7 +20,6 @@ import { ROLE_ID_REVENUE_DEPOSITOR } from "../../script/Roles.sol"; import { LibGuardianMessages } from "../../src/LibGuardianMessages.sol"; -import { Permit } from "../../src/structs/Permit.sol"; import { ModuleLimit } from "../../src/struct/ProtocolStorage.sol"; import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol"; import { NodeInfo } from "../../src/struct/NodeInfo.sol"; @@ -52,8 +51,6 @@ contract PufferProtocolTest is UnitTestHelper { bytes32 constant CRAZY_GAINS = bytes32("CRAZY_GAINS"); bytes32 constant DEFAULT_DEPOSIT_ROOT = bytes32("depositRoot"); - Permit emptyPermit; - // 0.01 % uint256 pointZeroZeroOne = 0.0001e18; // 0.02 % @@ -554,8 +551,7 @@ contract PufferProtocolTest is UnitTestHelper { // We deposit 10 VT for alice (legacy VT) deal(address(validatorTicket), address(this), 10 ether); validatorTicket.approve(address(pufferProtocol), 10 ether); - emptyPermit.amount = 10 ether; - pufferProtocol.depositValidatorTickets(emptyPermit, alice); + pufferProtocol.depositValidatorTickets(alice, 10 ether); vm.startPrank(alice); @@ -676,8 +672,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(validatorTicket.balanceOf(alice), 200 ether, "alice got 200 VT"); assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 0, "protocol got 0 VT"); - Permit memory vtPermit = emptyPermit; - vtPermit.amount = 200 ether; + uint256 vtAmount = 200 ether; // Approve VT validatorTicket.approve(address(pufferProtocol), 2000 ether); @@ -685,7 +680,7 @@ contract PufferProtocolTest is UnitTestHelper { // Deposit for herself vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorTicketsDeposited(alice, alice, 200 ether); - pufferProtocol.depositValidatorTickets(vtPermit, alice); + pufferProtocol.depositValidatorTickets(alice, vtAmount); assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 200 ether, "protocol got 200 VT"); assertEq(validatorTicket.balanceOf(address(alice)), 0, "alice got 0"); @@ -705,8 +700,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(validatorTicket.balanceOf(alice), 1000 ether, "alice got 1000 VT"); assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 0, "protocol got 0 VT"); - Permit memory vtPermit = emptyPermit; - vtPermit.amount = 200 ether; + uint256 vtAmount = 200 ether; // Approve VT validatorTicket.approve(address(pufferProtocol), 2000 ether); @@ -714,92 +708,20 @@ contract PufferProtocolTest is UnitTestHelper { // Deposit for herself vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorTicketsDeposited(alice, alice, 200 ether); - pufferProtocol.depositValidatorTickets(vtPermit, alice); + pufferProtocol.depositValidatorTickets(alice, vtAmount); assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 200 ether, "protocol got 200 VT"); assertEq(validatorTicket.balanceOf(address(alice)), 800 ether, "alice got 800"); assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 200 ether, "alice got 200 VT in the protocol"); // Perform a second deposit of 800 VT - vtPermit.amount = 800 ether; - pufferProtocol.depositValidatorTickets((vtPermit), alice); + vtAmount = 800 ether; + pufferProtocol.depositValidatorTickets(alice, vtAmount); assertEq( pufferProtocol.getValidatorTicketsBalance(alice), 1000 ether, "alice should have 1000 vt in the protocol" ); } - // Alice deposits VT for bob - function test_deposit_validator_tickets_permit_for_bob() public { - vm.deal(alice, 10 ether); - - uint256 numberOfDays = 200; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; - - vm.startPrank(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - - assertEq(validatorTicket.balanceOf(alice), 200 ether, "alice got 200 VT"); - assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 0, "protocol got 0 VT"); - - // Sign the permit - Permit memory vtPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(numberOfDays), block.timestamp), - validatorTicket.DOMAIN_SEPARATOR() - ); - - // Deposit for Bob - vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsDeposited(bob, alice, 200 ether); - pufferProtocol.depositValidatorTickets(vtPermit, bob); - - assertEq(pufferProtocol.getValidatorTicketsBalance(bob), 200 ether, "bob got the VTS in the protocol"); - assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "alice got no VTS in the protocol"); - } - - // Alice double deposit VT for Bob - function test_double_deposit_validator_tickets_permit_for_bob() public { - vm.deal(alice, 1000 ether); - - uint256 numberOfDays = 1000; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; - - vm.startPrank(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - - assertEq(validatorTicket.balanceOf(alice), 1000 ether, "alice got 1000 VT"); - assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 0, "protocol got 0 VT"); - - // Sign the permit - Permit memory vtPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(200), block.timestamp), - validatorTicket.DOMAIN_SEPARATOR() - ); - - // Deposit for Bob - vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsDeposited(bob, alice, 200 ether); - pufferProtocol.depositValidatorTickets(vtPermit, bob); - - assertEq(pufferProtocol.getValidatorTicketsBalance(bob), 200 ether, "bob got the VTS in the protocol"); - assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "alice got no VTS in the protocol"); - assertEq(validatorTicket.balanceOf(alice), 800 ether, "Alice still has 800 VTs left in wallet"); - - vm.startPrank(alice); - // Deposit for Bob again - Permit memory vtPermit2 = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(800), block.timestamp + 1000), - validatorTicket.DOMAIN_SEPARATOR() - ); - validatorTicket.approve(address(pufferProtocol), 800 ether); - pufferProtocol.depositValidatorTickets(vtPermit2, bob); - - assertEq(pufferProtocol.getValidatorTicketsBalance(bob), 1000 ether, "bob got the VTS in the protocol"); - assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "alice got no VTS in the protocol"); - assertEq(validatorTicket.balanceOf(alice), 0, "Alice has no more VTs"); - } - function test_changeMinimumVTAmount() public { assertEq(pufferProtocol.getMinimumVtAmount(), 30 * EPOCHS_PER_DAY, "initial value"); @@ -1127,13 +1049,12 @@ contract PufferProtocolTest is UnitTestHelper { _registerAndProvisionNode(bytes32("eve"), PUFFER_MODULE_0, eve); // Free VTS for everybody!! - Permit memory vtPermit = emptyPermit; - vtPermit.amount = 100 ether; - pufferProtocol.depositValidatorTickets(vtPermit, alice); - pufferProtocol.depositValidatorTickets(vtPermit, bob); - pufferProtocol.depositValidatorTickets(vtPermit, charlie); - pufferProtocol.depositValidatorTickets(vtPermit, dianna); - pufferProtocol.depositValidatorTickets(vtPermit, eve); + uint256 vtAmount = 100 ether; + pufferProtocol.depositValidatorTickets(alice, vtAmount); + pufferProtocol.depositValidatorTickets(bob, vtAmount); + pufferProtocol.depositValidatorTickets(charlie, vtAmount); + pufferProtocol.depositValidatorTickets(dianna, vtAmount); + pufferProtocol.depositValidatorTickets(eve, vtAmount); uint256 deadline = block.timestamp + 1 days; @@ -1263,9 +1184,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "initial exchange rate is ~1:1" ); - Permit memory vtPermit = emptyPermit; - vtPermit.amount = 100 ether; - pufferProtocol.depositValidatorTickets(vtPermit, alice); + uint256 vtAmount = 100 ether; + pufferProtocol.depositValidatorTickets(alice, vtAmount); uint256 deadline = block.timestamp + 1 days; @@ -1341,9 +1261,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "initial exchange rate is ~1:1" ); - Permit memory vtPermit = emptyPermit; - vtPermit.amount = 100 ether; - pufferProtocol.depositValidatorTickets(vtPermit, alice); + uint256 vtAmount = 100 ether; + pufferProtocol.depositValidatorTickets(alice, vtAmount); assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 100 ether, "100 VT in the protocol"); @@ -1883,13 +1802,12 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); validatorTicket.purchaseValidatorTicket{ value: 10 ether }(alice); - Permit memory vtPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), 50 ether, block.timestamp), validatorTicket.DOMAIN_SEPARATOR() - ); + uint256 vtAmount = 50 ether; + validatorTicket.approve(address(pufferProtocol), vtAmount); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsDeposited(bob, alice, 50 ether); - pufferProtocol.depositValidatorTickets(vtPermit, bob); + emit IPufferProtocol.ValidatorTicketsDeposited(bob, alice, vtAmount); + pufferProtocol.depositValidatorTickets(bob, vtAmount); vm.startPrank(bob); pufferProtocol.withdrawValidatorTickets(50 ether, bob); @@ -2062,7 +1980,6 @@ contract PufferProtocolTest is UnitTestHelper { nodeOperator, epochsValidated, deadline, IPufferProtocol.registerValidatorKey.selector ); - // Empty permit means that the node operator is paying with ETH for both bond & VT in the registration transaction vm.expectEmit(true, true, true, true); emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: amount }( @@ -2175,7 +2092,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); validatorTicket.purchaseValidatorTicket{ value: 1000 ether }(alice); validatorTicket.approve(address(pufferProtocol), type(uint256).max); - pufferProtocol.depositValidatorTickets(emptyPermit, alice); + pufferProtocol.depositValidatorTickets(alice, 0); vm.stopPrank(); } @@ -2188,7 +2105,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); validatorTicket.purchaseValidatorTicket{ value: 1000 ether }(alice); validatorTicket.approve(address(pufferProtocol), type(uint256).max); - pufferProtocol.depositValidatorTickets(emptyPermit, alice); + pufferProtocol.depositValidatorTickets(alice, 0); vm.stopPrank(); } } From c104f39d1fa271afd0c92bf457eeea12fc327cf3 Mon Sep 17 00:00:00 2001 From: Eladio Date: Thu, 10 Jul 2025 17:05:18 +0200 Subject: [PATCH 64/82] Added tests to increase coverage --- mainnet-contracts/script/DeployPuffer.s.sol | 3 ++- mainnet-contracts/script/DeploymentStructs.sol | 1 + mainnet-contracts/test/helpers/UnitTestHelper.sol | 4 ++-- mainnet-contracts/test/unit/PufferProtocol.t.sol | 12 ++++++++++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index c615deeb..76d3a68b 100644 --- a/mainnet-contracts/script/DeployPuffer.s.sol +++ b/mainnet-contracts/script/DeployPuffer.s.sol @@ -230,7 +230,8 @@ contract DeployPuffer is BaseScript { pufferVault: address(0), // overwritten in DeployEverything pufferDepositor: address(0), // overwritten in DeployEverything weth: address(0), // overwritten in DeployEverything - revenueDepositor: address(0) // overwritten in DeployEverything + revenueDepositor: address(0), // overwritten in DeployEverything + pufferProtocolLogic: address(pufferProtocolLogic) }); } diff --git a/mainnet-contracts/script/DeploymentStructs.sol b/mainnet-contracts/script/DeploymentStructs.sol index b3ce371d..1ad906c3 100644 --- a/mainnet-contracts/script/DeploymentStructs.sol +++ b/mainnet-contracts/script/DeploymentStructs.sol @@ -34,6 +34,7 @@ struct PufferProtocolDeployment { address weth; // from pufETH repository (dependency) address timelock; // from pufETH repository (dependency) address revenueDepositor; + address pufferProtocolLogic; } struct BridgingDeployment { diff --git a/mainnet-contracts/test/helpers/UnitTestHelper.sol b/mainnet-contracts/test/helpers/UnitTestHelper.sol index cc8ed660..a8939968 100644 --- a/mainnet-contracts/test/helpers/UnitTestHelper.sol +++ b/mainnet-contracts/test/helpers/UnitTestHelper.sol @@ -110,7 +110,7 @@ contract UnitTestHelper is Test, BaseScript { L2RewardManager public l2RewardManager; PufferRevenueDepositor public revenueDepositor; ConnextMock public connext; - + PufferProtocol public pufferProtocolLogic; address public DAO = makeAddr("DAO"); address public PAYMASTER = makeAddr("PUFFER_PAYMASTER"); // 0xA540f91Fb840381BCCf825a16A9fbDD0a19deFB1 address public l2RewardsManagerMock = makeAddr("l2RewardsManagerMock"); @@ -220,7 +220,7 @@ contract UnitTestHelper is Test, BaseScript { l2RewardManager = L2RewardManager(payable(bridgingDeployment.l2RewardManager)); connext = ConnextMock(payable(bridgingDeployment.connext)); revenueDepositor = PufferRevenueDepositor(payable(pufferDeployment.revenueDepositor)); - + pufferProtocolLogic = PufferProtocol(payable(pufferDeployment.pufferProtocolLogic)); // pufETH dependencies pufferVault = PufferVaultV5(payable(pufferDeployment.pufferVault)); pufferDepositor = PufferDepositor(payable(pufferDeployment.pufferDepositor)); diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 15f98874..2474da91 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -117,6 +117,18 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(PufferModule(payable(module)).NAME(), PUFFER_MODULE_0, "bad name"); } + function test_immutables() public view { + assertEq(address(pufferProtocol.PUFFER_VAULT()), address(pufferVault), "puffer vault address"); + assertEq(pufferProtocol.PUFFER_REVENUE_DISTRIBUTOR(), address(revenueDepositor), "puffer revenue distributor address"); + assertEq(address(pufferProtocol.PUFFER_ORACLE()), address(pufferOracle), "puffer oracle address"); + assertEq(address(pufferProtocol.PUFFER_MODULE_MANAGER()), address(pufferModuleManager), "puffer module manager address"); + assertEq(address(pufferProtocol.GUARDIAN_MODULE()), address(guardianModule), "puffer guardian module address"); + assertEq(address(pufferProtocol.VALIDATOR_TICKET()), address(validatorTicket), "validator ticket address"); + assertNotEq(address(pufferProtocol.BEACON_DEPOSIT_CONTRACT()), address(0), "beacon deposit contract address"); + + assertEq(address(pufferProtocol.getPufferProtocolLogic()), address(pufferProtocolLogic), "puffer protocol logic address"); + } + // Register validator key function test_register_validator_key() public { _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); From 48c5b53171ab5b77dce56df78e4fd6b249cbc461 Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:06:04 +0000 Subject: [PATCH 65/82] forge fmt --- mainnet-contracts/script/DeployPuffer.s.sol | 2 +- mainnet-contracts/test/unit/PufferProtocol.t.sol | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index 76d3a68b..e02f9c9e 100644 --- a/mainnet-contracts/script/DeployPuffer.s.sol +++ b/mainnet-contracts/script/DeployPuffer.s.sol @@ -232,7 +232,7 @@ contract DeployPuffer is BaseScript { weth: address(0), // overwritten in DeployEverything revenueDepositor: address(0), // overwritten in DeployEverything pufferProtocolLogic: address(pufferProtocolLogic) - }); + }); } function getStakingContract() internal returns (address) { diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 2474da91..c074927e 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -119,14 +119,24 @@ contract PufferProtocolTest is UnitTestHelper { function test_immutables() public view { assertEq(address(pufferProtocol.PUFFER_VAULT()), address(pufferVault), "puffer vault address"); - assertEq(pufferProtocol.PUFFER_REVENUE_DISTRIBUTOR(), address(revenueDepositor), "puffer revenue distributor address"); + assertEq( + pufferProtocol.PUFFER_REVENUE_DISTRIBUTOR(), address(revenueDepositor), "puffer revenue distributor address" + ); assertEq(address(pufferProtocol.PUFFER_ORACLE()), address(pufferOracle), "puffer oracle address"); - assertEq(address(pufferProtocol.PUFFER_MODULE_MANAGER()), address(pufferModuleManager), "puffer module manager address"); + assertEq( + address(pufferProtocol.PUFFER_MODULE_MANAGER()), + address(pufferModuleManager), + "puffer module manager address" + ); assertEq(address(pufferProtocol.GUARDIAN_MODULE()), address(guardianModule), "puffer guardian module address"); assertEq(address(pufferProtocol.VALIDATOR_TICKET()), address(validatorTicket), "validator ticket address"); assertNotEq(address(pufferProtocol.BEACON_DEPOSIT_CONTRACT()), address(0), "beacon deposit contract address"); - assertEq(address(pufferProtocol.getPufferProtocolLogic()), address(pufferProtocolLogic), "puffer protocol logic address"); + assertEq( + address(pufferProtocol.getPufferProtocolLogic()), + address(pufferProtocolLogic), + "puffer protocol logic address" + ); } // Register validator key From 4e44801d5b801c9cd34e275dffd1fee2ead3411a Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 14 Jul 2025 12:47:46 +0200 Subject: [PATCH 66/82] Refactored interfaces and adapted tests (protocol WIP) --- ...GenerateBLSKeysAndRegisterValidators.s.sol | 8 +- mainnet-contracts/src/PufferProtocol.sol | 15 +- mainnet-contracts/src/PufferProtocolBase.sol | 14 +- mainnet-contracts/src/PufferProtocolLogic.sol | 40 ++-- .../src/interface/IPufferProtocol.sol | 224 ------------------ .../src/interface/IPufferProtocolEvents.sol | 157 ++++++++++++ .../src/interface/IPufferProtocolFull.sol | 8 + .../src/interface/IPufferProtocolLogic.sol | 60 ++++- .../test/handlers/PufferProtocolHandler.sol | 4 +- .../test/unit/PufferProtocol.t.sol | 139 +++++------ 10 files changed, 327 insertions(+), 342 deletions(-) create mode 100644 mainnet-contracts/src/interface/IPufferProtocolEvents.sol create mode 100644 mainnet-contracts/src/interface/IPufferProtocolFull.sol diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index 0d11cb53..b9102990 100644 --- a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol +++ b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol @@ -5,7 +5,7 @@ import "forge-std/Script.sol"; import { stdJson } from "forge-std/StdJson.sol"; import { Permit } from "../src/structs/Permit.sol"; import { ValidatorKeyData } from "../src/struct/ValidatorKeyData.sol"; -import { IPufferProtocol } from "../src/interface/IPufferProtocol.sol"; +import { IPufferProtocolFull } from "../src/interface/IPufferProtocolFull.sol"; import { PufferProtocol } from "../src/PufferProtocol.sol"; import { PufferVaultV5 } from "../src/PufferVaultV5.sol"; import { ValidatorTicket } from "../src/ValidatorTicket.sol"; @@ -107,7 +107,7 @@ contract GenerateBLSKeysAndRegisterValidators is Script { numBatches: 1 }); - IPufferProtocol(protocolAddress).registerValidatorKey( + IPufferProtocolFull(protocolAddress).registerValidatorKey( validatorData, moduleName, 0, new bytes[](0), block.timestamp + SIGNATURE_VALIDITY_PERIOD ); @@ -156,8 +156,8 @@ contract GenerateBLSKeysAndRegisterValidators is Script { function _generateValidatorKey(uint256 idx, bytes32 moduleName) internal { uint256 numberOfGuardians = pufferProtocol.GUARDIAN_MODULE().getGuardians().length; bytes[] memory guardianPubKeys = pufferProtocol.GUARDIAN_MODULE().getGuardiansEnclavePubkeys(); - address moduleAddress = IPufferProtocol(protocolAddress).getModuleAddress(moduleName); - bytes memory withdrawalCredentials = IPufferProtocol(protocolAddress).getWithdrawalCredentials(moduleAddress); + address moduleAddress = IPufferProtocolFull(protocolAddress).getModuleAddress(moduleName); + bytes memory withdrawalCredentials = IPufferProtocolFull(protocolAddress).getWithdrawalCredentials(moduleAddress); string[] memory inputs = new string[](17); inputs[0] = "coral-cli"; diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 676aa326..bba8a482 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0 <0.9.0; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; +import { IPufferProtocolEvents } from "./interface/IPufferProtocolEvents.sol"; import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -32,7 +33,7 @@ import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ -contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolBase { +contract PufferProtocol is IPufferProtocolEvents, IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolBase { constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, @@ -99,7 +100,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad restricted { bytes memory callData = - abi.encodeWithSelector(IPufferProtocolLogic._depositValidationTime.selector, epochsValidatedSignature); + abi.encodeWithSelector(IPufferProtocolLogic.depositValidationTime.selector, epochsValidatedSignature); _delegatecall(_getPufferProtocolStorage(), callData); } @@ -136,7 +137,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ function withdrawValidationTime(uint96 amount, address recipient) external restricted { bytes memory callData = - abi.encodeWithSelector(IPufferProtocolLogic._withdrawValidationTime.selector, amount, recipient); + abi.encodeWithSelector(IPufferProtocolLogic.withdrawValidationTime.selector, amount, recipient); _delegatecall(_getPufferProtocolStorage(), callData); } @@ -152,7 +153,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad uint256 deadline ) external payable restricted { bytes memory callData = abi.encodeWithSelector( - IPufferProtocolLogic._registerValidatorKey.selector, + IPufferProtocolLogic.registerValidatorKey.selector, data, moduleName, totalEpochsValidated, @@ -211,7 +212,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad restricted { bytes memory callData = abi.encodeWithSelector( - IPufferProtocolLogic._requestConsolidation.selector, moduleName, srcIndices, targetIndices + IPufferProtocolLogic.requestConsolidation.selector, moduleName, srcIndices, targetIndices ); _delegatecall(_getPufferProtocolStorage(), callData); } @@ -278,7 +279,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad uint256 deadline ) external restricted { bytes memory callData = abi.encodeWithSelector( - IPufferProtocolLogic._batchHandleWithdrawals.selector, validatorInfos, guardianEOASignatures, deadline + IPufferProtocolLogic.batchHandleWithdrawals.selector, validatorInfos, guardianEOASignatures, deadline ); _delegatecall(_getPufferProtocolStorage(), callData); } @@ -289,7 +290,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external restricted { bytes memory callData = - abi.encodeWithSelector(IPufferProtocolLogic._skipProvisioning.selector, moduleName, guardianEOASignatures); + abi.encodeWithSelector(IPufferProtocolLogic.skipProvisioning.selector, moduleName, guardianEOASignatures); _delegatecall(_getPufferProtocolStorage(), callData); } diff --git a/mainnet-contracts/src/PufferProtocolBase.sol b/mainnet-contracts/src/PufferProtocolBase.sol index a8d157d1..54715aba 100644 --- a/mainnet-contracts/src/PufferProtocolBase.sol +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0 <0.9.0; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; +import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; import { Status } from "./struct/Status.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; @@ -11,8 +12,9 @@ import { ValidatorTicket } from "./ValidatorTicket.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; import { PufferProtocolStorage } from "./PufferProtocolStorage.sol"; +import { IPufferProtocolEvents } from "./interface/IPufferProtocolEvents.sol"; -abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignatureNonces { +abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignatureNonces, IPufferProtocolEvents { /** * @notice Thrown when the deposit state that is provided doesn't match the one on Beacon deposit contract */ @@ -151,12 +153,14 @@ abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignature */ uint256 internal constant _32_ETH_GWEI = 32 * 10 ** 9; - bytes32 internal constant _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY = IPufferProtocol.registerValidatorKey.selector; + bytes32 internal constant _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY = + IPufferProtocolLogic.registerValidatorKey.selector; bytes32 internal constant _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME = - IPufferProtocol.depositValidationTime.selector; - bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = IPufferProtocol.requestWithdrawal.selector; + IPufferProtocolLogic.depositValidationTime.selector; + bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = + IPufferProtocol.requestWithdrawal.selector; bytes32 internal constant _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS = - IPufferProtocol.batchHandleWithdrawals.selector; + IPufferProtocolLogic.batchHandleWithdrawals.selector; IGuardianModule internal immutable _GUARDIAN_MODULE; diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 491f5d5f..b695ec61 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -63,7 +63,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { * @notice Check IPufferProtocol.depositValidationTime * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) + function depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external payable override @@ -91,7 +91,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { } $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].validationTime += SafeCast.toUint96(msg.value); - emit IPufferProtocol.ValidationTimeDeposited({ + emit ValidationTimeDeposited({ node: epochsValidatedSignature.nodeOperator, ethAmount: msg.value }); @@ -101,7 +101,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { * @notice Check IPufferProtocol.withdrawValidationTime * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _withdrawValidationTime(uint96 amount, address recipient) external override { + function withdrawValidationTime(uint96 amount, address recipient) external override { ProtocolStorage storage $ = _getPufferProtocolStorage(); // Node operator can only withdraw if they have no active or pending validators @@ -123,14 +123,14 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { // Transfer WETH to the recipient ERC20(weth).transfer(recipient, amount); - emit IPufferProtocol.ValidationTimeWithdrawn(msg.sender, recipient, amount); + emit ValidationTimeWithdrawn(msg.sender, recipient, amount); } /** * @notice Check IPufferProtocol.registerValidatorKey * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _registerValidatorKey( + function registerValidatorKey( ValidatorKeyData calldata data, bytes32 moduleName, uint256 totalEpochsValidated, @@ -156,7 +156,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { require(msg.value >= minimumETHRequired, InvalidETHAmount()); - emit IPufferProtocol.ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - bondAmountEth) }); + emit ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - bondAmountEth) }); _settleVTAccounting({ $: $, @@ -194,11 +194,11 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { ++$.moduleLimits[moduleName].numberOfRegisteredValidators; } - emit IPufferProtocol.NumberOfRegisteredValidatorsChanged({ + emit NumberOfRegisteredValidatorsChanged({ moduleName: moduleName, newNumberOfRegisteredValidators: $.moduleLimits[moduleName].numberOfRegisteredValidators }); - emit IPufferProtocol.ValidatorKeyRegistered({ + emit ValidatorKeyRegistered({ pubKey: data.blsPubKey, pufferModuleIndex: pufferModuleIndex, moduleName: moduleName, @@ -209,7 +209,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { /** * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) + function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable override @@ -246,14 +246,14 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { $.modules[moduleName].requestConsolidation{ value: msg.value }(srcPubkeys, targetPubkeys); - emit IPufferProtocol.ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); + emit ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); } /** * @notice Check IPufferProtocol.skipProvisioning * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external override { + function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external override { ProtocolStorage storage $ = _getPufferProtocolStorage(); uint256 skippedIndex = $.nextToBeProvisioned[moduleName]; @@ -284,14 +284,14 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { unchecked { ++$.nextToBeProvisioned[moduleName]; } - emit IPufferProtocol.ValidatorSkipped($.validators[moduleName][skippedIndex].pubKey, skippedIndex, moduleName); + emit ValidatorSkipped($.validators[moduleName][skippedIndex].pubKey, skippedIndex, moduleName); } /** * @notice Check IPufferProtocol.batchHandleWithdrawals * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _batchHandleWithdrawals( + function batchHandleWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, bytes[] calldata guardianEOASignatures, uint256 deadline @@ -370,7 +370,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { // Deduct 32 ETH per batch from the `lockedETHAmount` on the PufferOracle _PUFFER_ORACLE.exitValidators(numExitedBatches); - _batchHandleWithdrawalsAccounting(bondWithdrawals, validatorInfos); + batchHandleWithdrawalsAccounting(bondWithdrawals, validatorInfos); } /** @@ -420,7 +420,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { $.nodeOperatorInfo[node].totalEpochsValidated = epochsValidatedSignature.totalEpochsValidated; $.nodeOperatorInfo[node].validationTime -= validationTimeToConsume; - emit IPufferProtocol.ValidationTimeConsumed({ + emit ValidationTimeConsumed({ node: node, consumedAmount: validationTimeToConsume, deprecated_burntVTs: deprecated_burntVTs @@ -474,7 +474,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { // nosemgrep basic-arithmetic-underflow $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); - emit IPufferProtocol.ValidationTimeConsumed({ + emit ValidationTimeConsumed({ node: nodeOperator, consumedAmount: 0, deprecated_burntVTs: vtBurnAmount @@ -508,7 +508,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { return validatedEpochs * 4444444444444445; } - function _batchHandleWithdrawalsAccounting( + function batchHandleWithdrawalsAccounting( Withdrawals[] memory bondWithdrawals, StoppedValidatorInfo[] calldata validatorInfos ) internal { @@ -559,7 +559,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { require(exitingBond >= burnAmount, InvalidWithdrawAmount()); exitingBond -= burnAmount; - emit IPufferProtocol.ValidatorDownsized({ + emit ValidatorDownsized({ pubKey: validator.pubKey, pufferModuleIndex: validatorInfo.pufferModuleIndex, moduleName: validatorInfo.moduleName, @@ -592,7 +592,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { numBatches: validator.numBatches }); - emit IPufferProtocol.ValidatorExited({ + emit ValidatorExited({ pubKey: validator.pubKey, pufferModuleIndex: validatorInfo.pufferModuleIndex, moduleName: validatorInfo.moduleName, @@ -617,7 +617,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { function _decreaseNumberOfRegisteredValidators(ProtocolStorage storage $, bytes32 moduleName) internal { --$.moduleLimits[moduleName].numberOfRegisteredValidators; - emit IPufferProtocol.NumberOfRegisteredValidatorsChanged( + emit NumberOfRegisteredValidatorsChanged( moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators ); } diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 5f23b620..97f77946 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -23,157 +23,6 @@ import { IBeaconDepositContract } from "../interface/IBeaconDepositContract.sol" * @custom:security-contact security@puffer.fi */ interface IPufferProtocol { - /** - * @notice Emitted when the number of active validators changes - * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" - */ - event NumberOfRegisteredValidatorsChanged(bytes32 indexed moduleName, uint256 newNumberOfRegisteredValidators); - - /** - * @notice Emitted when the validation time is deposited - * @dev Signature "0xdab70193ab2d6948fc2f6da9e82794bf650dc3099e042b6510f9e5019735545c" - */ - event ValidationTimeDeposited(address indexed node, uint256 ethAmount); - - /** - * @notice Emitted when the new Puffer module is created - * @dev Signature "0x8ad2a9260a8e9a01d1ccd66b3875bcbdf8c4d0c552bc51a7d2125d4146e1d2d6" - */ - event NewPufferModuleCreated(address module, bytes32 indexed moduleName, bytes32 withdrawalCredentials); - - /** - * @notice Emitted when the module's validator limit is changed from `oldLimit` to `newLimit` - * @dev Signature "0x21e92cbdc47ef718b9c77ea6a6ee50ff4dd6362ee22041ab77a46dacb93f5355" - */ - event ValidatorLimitPerModuleChanged(uint256 oldLimit, uint256 newLimit); - - /** - * @notice Emitted when the minimum number of days for ValidatorTickets is changed from `oldMinimumNumberOfDays` to `newMinimumNumberOfDays` - * @dev Signature "0xc6f97db308054b44394df54aa17699adff6b9996e9cffb4dcbcb127e20b68abc" - */ - event MinimumVTAmountChanged(uint256 oldMinimumNumberOfDays, uint256 newMinimumNumberOfDays); - - /** - * @notice Emitted when the VT Penalty amount is changed from `oldPenalty` to `newPenalty` - * @dev Signature "0xfceca97b5d1d1164f9a15e42f38eaf4a6e760d8505f06161a258d4bf21cc4ee7" - */ - event VTPenaltyChanged(uint256 oldPenalty, uint256 newPenalty); - - /** - * @notice Emitted when VT is deposited to the protocol - * @dev Signature "0xd47eb90c0b945baf5f3ae3f1384a7a524a6f78f1461b354c4a09c4001a5cee9c" - */ - event ValidatorTicketsDeposited(address indexed node, address indexed depositor, uint256 amount); - - /** - * @notice Emitted when VT is withdrawn from the protocol - * @dev Signature "0xdf7e884ecac11650e1285647b057fa733a7bb9f1da100e7a8c22aafe4bdf6f40" - */ - event ValidatorTicketsWithdrawn(address indexed node, address indexed recipient, uint256 amount); - - /** - * @notice Emitted when Validation Time is withdrawn from the protocol - * @dev Signature "0xd19b9bc208843da6deef01aa6dedd607204c4f8b6d02f79b60e326a8c6e2b6e8" - */ - event ValidationTimeWithdrawn(address indexed node, address indexed recipient, uint256 ethAmount); - - /** - * @notice Emitted when the guardians decide to skip validator provisioning for `moduleName` - * @dev Signature "0x088dc5dc64f3e8df8da5140a284d3018a717d6b009e605513bb28a2b466d38ee" - */ - event ValidatorSkipped(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName); - - /** - * @notice Emitted when the module weights changes from `oldWeights` to `newWeights` - * @dev Signature "0xd4c9924bd67ff5bd900dc6b1e03b839c6ffa35386096b0c2a17c03638fa4ebff" - */ - event ModuleWeightsChanged(bytes32[] oldWeights, bytes32[] newWeights); - - /** - * @notice Emitted when the Validator key is registered - * @param pubKey is the validator public key - * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain - * @param moduleName is the staking Module - * @param numBatches is the number of batches the validator has - * @dev Signature "0xd97b45553982eba642947754e3448d2142408b73d3e4be6b760a89066eb6c00a" - */ - event ValidatorKeyRegistered( - bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint8 numBatches - ); - - /** - * @notice Emitted when the Validator exited and stopped validating - * @param pubKey is the validator public key - * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain - * @param moduleName is the staking Module - * @param pufETHBurnAmount The amount of pufETH burned from the Node Operator - * @param numBatches is the number of batches the validator had - * @dev Signature "0xf435da9e3aeccc40d39fece7829f9941965ceee00d31fa7a89d608a273ea906e" - */ - event ValidatorExited( - bytes pubKey, - uint256 indexed pufferModuleIndex, - bytes32 indexed moduleName, - uint256 pufETHBurnAmount, - uint256 numBatches - ); - - /** - * @notice Emitted when a validator is downsized - * @param pubKey is the validator public key - * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain - * @param moduleName is the staking Module - * @param pufETHBurnAmount The amount of pufETH burned from the Node Operator - * @param epoch The epoch of the downsize - * @param numBatchesBefore The number of batches before the downsize - * @param numBatchesAfter The number of batches after the downsize - * @dev Signature "0x75afd977bd493b29a8e699e6b7a9ab85df6b62f4ba5664e370bd5cb0b0e2b776" - */ - event ValidatorDownsized( - bytes pubKey, - uint256 indexed pufferModuleIndex, - bytes32 indexed moduleName, - uint256 pufETHBurnAmount, - uint256 epoch, - uint256 numBatchesBefore, - uint256 numBatchesAfter - ); - - /** - * @notice Emitted when validation time is consumed - * @param node is the node operator address - * @param consumedAmount is the amount of validation time that was consumed - * @param deprecated_burntVTs is the amount of VT that was burnt - * @dev Signature "0x4b16b7334c6437660b5530a3a5893e7a10fa5424e5c0d67806687147553544ef" - */ - event ValidationTimeConsumed(address indexed node, uint256 consumedAmount, uint256 deprecated_burntVTs); - - /** - * @notice Emitted when a consolidation is requested - * @param moduleName is the module name - * @param srcPubkeys is the list of pubkeys to consolidate from - * @param targetPubkeys is the list of pubkeys to consolidate to - * @dev Signature "0xdc26585f08f92fc2f54b80496c32d3c20cfa17f1e91d9afc8449c17d1b4f85bb" - */ - event ConsolidationRequested(bytes32 indexed moduleName, bytes[] srcPubkeys, bytes[] targetPubkeys); - - /** - * @notice Emitted when the Validator is provisioned - * @param pubKey is the validator public key - * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain - * @param moduleName is the staking Module - * @param numBatches is the number of batches the validator has - * @dev Signature "0xfed1ead36b4481c77b26f25acade13754ce94663e2515f15507b2cfbade3ed8d" - */ - event SuccessfullyProvisioned( - bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint256 numBatches - ); - - /** - * @notice Emitted when the PufferProtocolLogic is set - * @dev Signature "0xe271f36954242c619ce9d0f727a7d3b5f4db04666752aaeb20bca6d52098792a" - */ - event PufferProtocolLogicSet(address oldPufferProtocolLogic, address newPufferProtocolLogic); /** * @notice Returns validator information @@ -204,25 +53,6 @@ interface IPufferProtocol { */ function depositValidatorTickets(address node, uint256 vtAmount) external; - /** - * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`). - * Deposits Validation Time for the `node`. Validation Time is in native ETH. - * @param epochsValidatedSignature is a struct that contains: - * - functionSelector: Can be left empty, it will be used to prevent replay attacks - * - totalEpochsValidated: The total number of epochs validated by that node operator - * - nodeOperator: The node operator address - * - deadline: The deadline for the signature - * - signatures: The signatures of the guardians over the total number of epochs validated - */ - function depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external payable; - - /** - * @notice New function that allows the transaction sender (node operator) to withdraw WETH to a recipient (use this instead of `withdrawValidatorTickets`) - * The Validation time can be withdrawn if there are no active or pending validators - * The WETH is sent to the recipient - */ - function withdrawValidationTime(uint96 amount, address recipient) external; - /** * @notice Withdraws the `amount` of Validator Tickers from the `msg.sender` to the `recipient` * DEPRECATED - This method is deprecated and will be removed in the future upgrade @@ -230,19 +60,6 @@ interface IPufferProtocol { */ function withdrawValidatorTickets(uint96 amount, address recipient) external; - /** - * @notice Requests a consolidation for the given validators. This consolidation consists on merging one validator into another one - * @param moduleName The name of the module - * @param srcIndices The indices of the validators to consolidate from - * @param targetIndices The indices of the validators to consolidate to - * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) - * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule - * to the caller from the EigenPod - */ - function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) - external - payable; - /** * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. * If the amount is 0, the withdrawal is total and the validator will be fully exited. @@ -272,31 +89,6 @@ interface IPufferProtocol { uint256 deadline ) external payable; - /** - * @notice Batch settling of validator withdrawals - * @notice Settles a validator withdrawal - * @dev This is one of the most important methods in the protocol - * The withdrawals might be partial or total, and the validator might be downsized or fully exited - * It has multiple tasks: - * 1. Burn the pufETH from the node operator (if the withdrawal amount was lower than 32 ETH * numBatches or completely if the validator was slashed) - * 2. Burn the Validator Tickets from the node operator (deprecated) and transfer consumed validation time (as WETH) to the PUFFER_REVENUE_DISTRIBUTOR - * 3. Transfer withdrawal ETH from the PufferModule of the Validator to the PufferVault - * 4. Decrement the `lockedETHAmount` on the PufferOracle to reflect the new amount of locked ETH - * @dev If a node operator exits early, will be penalized by the protocol by increasing the totalEpochsValidated so the VT consumption is higher than the actual amount of epochs validated - */ - function batchHandleWithdrawals( - StoppedValidatorInfo[] calldata validatorInfos, - bytes[] calldata guardianEOASignatures, - uint256 deadline - ) external; - - /** - * @notice Skips the next validator for `moduleName` - * @param moduleName The name of the module - * @param guardianEOASignatures The signatures of the guardians to validate the skipping of provisioning - * @dev Restricted to Guardians - */ - function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external; /** * @notice Returns the guardian module @@ -388,22 +180,6 @@ interface IPufferProtocol { */ function createPufferModule(bytes32 moduleName) external returns (address); - /** - * @notice Registers a validator key and consumes the ETH for the validation time for the other active validators. - * @dev There is a queue per moduleName and it is FIFO - * @param data The validator key data - * @param moduleName The name of the module - * @param totalEpochsValidated The total number of epochs validated by the validator - * @param vtConsumptionSignature The signature of the guardians to validate the number of epochs validated - * @param deadline The deadline for the signature - */ - function registerValidatorKey( - ValidatorKeyData calldata data, - bytes32 moduleName, - uint256 totalEpochsValidated, - bytes[] calldata vtConsumptionSignature, - uint256 deadline - ) external payable; /** * @notice Returns the pending validator index for `moduleName` diff --git a/mainnet-contracts/src/interface/IPufferProtocolEvents.sol b/mainnet-contracts/src/interface/IPufferProtocolEvents.sol new file mode 100644 index 00000000..ecde2053 --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferProtocolEvents.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +interface IPufferProtocolEvents { + /** + * @notice Emitted when the number of active validators changes + * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" + */ + event NumberOfRegisteredValidatorsChanged(bytes32 indexed moduleName, uint256 newNumberOfRegisteredValidators); + + /** + * @notice Emitted when the validation time is deposited + * @dev Signature "0xdab70193ab2d6948fc2f6da9e82794bf650dc3099e042b6510f9e5019735545c" + */ + event ValidationTimeDeposited(address indexed node, uint256 ethAmount); + + /** + * @notice Emitted when the new Puffer module is created + * @dev Signature "0x8ad2a9260a8e9a01d1ccd66b3875bcbdf8c4d0c552bc51a7d2125d4146e1d2d6" + */ + event NewPufferModuleCreated(address module, bytes32 indexed moduleName, bytes32 withdrawalCredentials); + + /** + * @notice Emitted when the module's validator limit is changed from `oldLimit` to `newLimit` + * @dev Signature "0x21e92cbdc47ef718b9c77ea6a6ee50ff4dd6362ee22041ab77a46dacb93f5355" + */ + event ValidatorLimitPerModuleChanged(uint256 oldLimit, uint256 newLimit); + + /** + * @notice Emitted when the minimum number of days for ValidatorTickets is changed from `oldMinimumNumberOfDays` to `newMinimumNumberOfDays` + * @dev Signature "0xc6f97db308054b44394df54aa17699adff6b9996e9cffb4dcbcb127e20b68abc" + */ + event MinimumVTAmountChanged(uint256 oldMinimumNumberOfDays, uint256 newMinimumNumberOfDays); + + /** + * @notice Emitted when the VT Penalty amount is changed from `oldPenalty` to `newPenalty` + * @dev Signature "0xfceca97b5d1d1164f9a15e42f38eaf4a6e760d8505f06161a258d4bf21cc4ee7" + */ + event VTPenaltyChanged(uint256 oldPenalty, uint256 newPenalty); + + /** + * @notice Emitted when VT is deposited to the protocol + * @dev Signature "0xd47eb90c0b945baf5f3ae3f1384a7a524a6f78f1461b354c4a09c4001a5cee9c" + */ + event ValidatorTicketsDeposited(address indexed node, address indexed depositor, uint256 amount); + + /** + * @notice Emitted when VT is withdrawn from the protocol + * @dev Signature "0xdf7e884ecac11650e1285647b057fa733a7bb9f1da100e7a8c22aafe4bdf6f40" + */ + event ValidatorTicketsWithdrawn(address indexed node, address indexed recipient, uint256 amount); + + /** + * @notice Emitted when Validation Time is withdrawn from the protocol + * @dev Signature "0xd19b9bc208843da6deef01aa6dedd607204c4f8b6d02f79b60e326a8c6e2b6e8" + */ + event ValidationTimeWithdrawn(address indexed node, address indexed recipient, uint256 ethAmount); + + /** + * @notice Emitted when the guardians decide to skip validator provisioning for `moduleName` + * @dev Signature "0x088dc5dc64f3e8df8da5140a284d3018a717d6b009e605513bb28a2b466d38ee" + */ + event ValidatorSkipped(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName); + + /** + * @notice Emitted when the module weights changes from `oldWeights` to `newWeights` + * @dev Signature "0xd4c9924bd67ff5bd900dc6b1e03b839c6ffa35386096b0c2a17c03638fa4ebff" + */ + event ModuleWeightsChanged(bytes32[] oldWeights, bytes32[] newWeights); + + /** + * @notice Emitted when the Validator key is registered + * @param pubKey is the validator public key + * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain + * @param moduleName is the staking Module + * @param numBatches is the number of batches the validator has + * @dev Signature "0xd97b45553982eba642947754e3448d2142408b73d3e4be6b760a89066eb6c00a" + */ + event ValidatorKeyRegistered( + bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint8 numBatches + ); + + /** + * @notice Emitted when the Validator exited and stopped validating + * @param pubKey is the validator public key + * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain + * @param moduleName is the staking Module + * @param pufETHBurnAmount The amount of pufETH burned from the Node Operator + * @param numBatches is the number of batches the validator had + * @dev Signature "0xf435da9e3aeccc40d39fece7829f9941965ceee00d31fa7a89d608a273ea906e" + */ + event ValidatorExited( + bytes pubKey, + uint256 indexed pufferModuleIndex, + bytes32 indexed moduleName, + uint256 pufETHBurnAmount, + uint256 numBatches + ); + + /** + * @notice Emitted when a validator is downsized + * @param pubKey is the validator public key + * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain + * @param moduleName is the staking Module + * @param pufETHBurnAmount The amount of pufETH burned from the Node Operator + * @param epoch The epoch of the downsize + * @param numBatchesBefore The number of batches before the downsize + * @param numBatchesAfter The number of batches after the downsize + * @dev Signature "0x75afd977bd493b29a8e699e6b7a9ab85df6b62f4ba5664e370bd5cb0b0e2b776" + */ + event ValidatorDownsized( + bytes pubKey, + uint256 indexed pufferModuleIndex, + bytes32 indexed moduleName, + uint256 pufETHBurnAmount, + uint256 epoch, + uint256 numBatchesBefore, + uint256 numBatchesAfter + ); + + /** + * @notice Emitted when validation time is consumed + * @param node is the node operator address + * @param consumedAmount is the amount of validation time that was consumed + * @param deprecated_burntVTs is the amount of VT that was burnt + * @dev Signature "0x4b16b7334c6437660b5530a3a5893e7a10fa5424e5c0d67806687147553544ef" + */ + event ValidationTimeConsumed(address indexed node, uint256 consumedAmount, uint256 deprecated_burntVTs); + + /** + * @notice Emitted when a consolidation is requested + * @param moduleName is the module name + * @param srcPubkeys is the list of pubkeys to consolidate from + * @param targetPubkeys is the list of pubkeys to consolidate to + * @dev Signature "0xdc26585f08f92fc2f54b80496c32d3c20cfa17f1e91d9afc8449c17d1b4f85bb" + */ + event ConsolidationRequested(bytes32 indexed moduleName, bytes[] srcPubkeys, bytes[] targetPubkeys); + + /** + * @notice Emitted when the Validator is provisioned + * @param pubKey is the validator public key + * @param pufferModuleIndex is the internal validator index in Puffer Finance, not to be mistaken with validator index on Beacon Chain + * @param moduleName is the staking Module + * @param numBatches is the number of batches the validator has + * @dev Signature "0xfed1ead36b4481c77b26f25acade13754ce94663e2515f15507b2cfbade3ed8d" + */ + event SuccessfullyProvisioned( + bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, uint256 numBatches + ); + + /** + * @notice Emitted when the PufferProtocolLogic is set + * @dev Signature "0xe271f36954242c619ce9d0f727a7d3b5f4db04666752aaeb20bca6d52098792a" + */ + event PufferProtocolLogicSet(address oldPufferProtocolLogic, address newPufferProtocolLogic); + +} diff --git a/mainnet-contracts/src/interface/IPufferProtocolFull.sol b/mainnet-contracts/src/interface/IPufferProtocolFull.sol new file mode 100644 index 00000000..b0feb249 --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferProtocolFull.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { IPufferProtocol } from "./IPufferProtocol.sol"; +import { IPufferProtocolLogic } from "./IPufferProtocolLogic.sol"; +import { IPufferProtocolEvents } from "./IPufferProtocolEvents.sol"; + +interface IPufferProtocolFull is IPufferProtocol, IPufferProtocolLogic, IPufferProtocolEvents {} diff --git a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol index 628e41d1..c8ac551f 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -6,23 +6,39 @@ import { StoppedValidatorInfo } from "../struct/StoppedValidatorInfo.sol"; import { ValidatorKeyData } from "../struct/ValidatorKeyData.sol"; interface IPufferProtocolLogic { + /** - * @notice Check IPufferProtocol.depositValidationTime + * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`). + * Deposits Validation Time for the `node`. Validation Time is in native ETH. + * @param epochsValidatedSignature is a struct that contains: + * - functionSelector: Can be left empty, it will be used to prevent replay attacks + * - totalEpochsValidated: The total number of epochs validated by that node operator + * - nodeOperator: The node operator address + * - deadline: The deadline for the signature + * - signatures: The signatures of the guardians over the total number of epochs validated * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external payable; + function depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external payable; /** - * @notice Check IPufferProtocol.withdrawValidationTime + * @notice New function that allows the transaction sender (node operator) to withdraw WETH to a recipient (use this instead of `withdrawValidatorTickets`) + * The Validation time can be withdrawn if there are no active or pending validators + * The WETH is sent to the recipient * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _withdrawValidationTime(uint96 amount, address recipient) external; + function withdrawValidationTime(uint96 amount, address recipient) external; /** - * @notice Check IPufferProtocol.registerValidatorKey + * @notice Registers a validator key and consumes the ETH for the validation time for the other active validators. + * @dev There is a queue per moduleName and it is FIFO + * @param data The validator key data + * @param moduleName The name of the module + * @param totalEpochsValidated The total number of epochs validated by the validator + * @param vtConsumptionSignature The signature of the guardians to validate the number of epochs validated + * @param deadline The deadline for the signature * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _registerValidatorKey( + function registerValidatorKey( ValidatorKeyData calldata data, bytes32 moduleName, uint256 totalEpochsValidated, @@ -31,24 +47,44 @@ interface IPufferProtocolLogic { ) external payable; /** - * @notice Check IPufferProtocol.requestConsolidation + * @notice Requests a consolidation for the given validators. This consolidation consists on merging one validator into another one + * @param moduleName The name of the module + * @param srcIndices The indices of the validators to consolidate from + * @param targetIndices The indices of the validators to consolidate to + * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule + * to the caller from the EigenPod * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) + function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) external payable; + /** - * @notice Check IPufferProtocol.skipProvisioning + * @notice Skips the next validator for `moduleName` + * @param moduleName The name of the module + * @param guardianEOASignatures The signatures of the guardians to validate the skipping of provisioning + * @dev Restricted to Guardians * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external; + function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external; + /** - * @notice Check IPufferProtocol.batchHandleWithdrawals + * @notice Batch settling of validator withdrawals + * @notice Settles a validator withdrawal + * @dev This is one of the most important methods in the protocol + * The withdrawals might be partial or total, and the validator might be downsized or fully exited + * It has multiple tasks: + * 1. Burn the pufETH from the node operator (if the withdrawal amount was lower than 32 ETH * numBatches or completely if the validator was slashed) + * 2. Burn the Validator Tickets from the node operator (deprecated) and transfer consumed validation time (as WETH) to the PUFFER_REVENUE_DISTRIBUTOR + * 3. Transfer withdrawal ETH from the PufferModule of the Validator to the PufferVault + * 4. Decrement the `lockedETHAmount` on the PufferOracle to reflect the new amount of locked ETH + * @dev If a node operator exits early, will be penalized by the protocol by increasing the totalEpochsValidated so the VT consumption is higher than the actual amount of epochs validated * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ - function _batchHandleWithdrawals( + function batchHandleWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, bytes[] calldata guardianEOASignatures, uint256 deadline diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index 10cab22d..9f48324f 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; -import { IPufferProtocol } from "../../src/interface/IPufferProtocol.sol"; +import { IPufferProtocolEvents } from "../../src/interface/IPufferProtocolEvents.sol"; import { EnumerableMap } from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { RaveEvidence } from "../../src/struct/RaveEvidence.sol"; @@ -582,7 +582,7 @@ contract PufferProtocolHandler is Test { uint256 bond = 1 ether; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( validatorKeyData, moduleName, 0, new bytes[](0), block.timestamp + 1 days ); diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index c074927e..dd5e46cd 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -5,6 +5,9 @@ import { PufferProtocolMockUpgrade } from "../mocks/PufferProtocolMockUpgrade.so import { UnitTestHelper } from "../helpers/UnitTestHelper.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { IPufferProtocol } from "../../src/interface/IPufferProtocol.sol"; +import { IPufferProtocolLogic } from "../../src/interface/IPufferProtocolLogic.sol"; +import { IPufferProtocolFull } from "../../src/interface/IPufferProtocolFull.sol"; +import { IPufferProtocolEvents } from "../../src/interface/IPufferProtocolEvents.sol"; import { ValidatorKeyData } from "../../src/struct/ValidatorKeyData.sol"; import { Status } from "../../src/struct/Status.sol"; import { Validator } from "../../src/struct/Validator.sol"; @@ -178,7 +181,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(moduleLimit.numberOfRegisteredValidators, 2, "2 active validators"); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorSkipped(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0); + emit IPufferProtocolEvents.ValidatorSkipped(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0); pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); moduleLimit = pufferProtocol.getModuleLimitInformation(PUFFER_MODULE_0); @@ -197,7 +200,7 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(idx, 1, "idx should be 1"); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); moduleSelectionIndex = pufferProtocol.getModuleSelectIndex(); assertEq(moduleSelectionIndex, 1, "module idx changed"); @@ -361,12 +364,12 @@ contract PufferProtocolTest is UnitTestHelper { // 1. provision zero key vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(zeroPubKey, 0, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.SuccessfullyProvisioned(zeroPubKey, 0, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Provision Bob that is not zero pubKey vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(bobPubKey, 1, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.SuccessfullyProvisioned(bobPubKey, 1, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); Validator memory bobValidator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 1); @@ -375,7 +378,7 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); - emit IPufferProtocol.SuccessfullyProvisioned(zeroPubKey, 3, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.SuccessfullyProvisioned(zeroPubKey, 3, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Get validators @@ -404,7 +407,7 @@ contract PufferProtocolTest is UnitTestHelper { newWeights[3] = CRAZY_GAINS; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ModuleWeightsChanged(oldWeights, newWeights); + emit IPufferProtocolEvents.ModuleWeightsChanged(oldWeights, newWeights); pufferProtocol.setModuleWeights(newWeights); vm.deal(address(pufferVault), 10000 ether); @@ -424,7 +427,7 @@ contract PufferProtocolTest is UnitTestHelper { // Provision Bob that is not zero pubKey vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 0, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 0, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -434,7 +437,7 @@ contract PufferProtocolTest is UnitTestHelper { assertTrue(nextId == 0, "module id"); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("benjamin")), 0, EIGEN_DA, 1); + emit IPufferProtocolEvents.SuccessfullyProvisioned(_getPubKey(bytes32("benjamin")), 0, EIGEN_DA, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -474,7 +477,7 @@ contract PufferProtocolTest is UnitTestHelper { ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.SuccessfullyProvisioned(_getPubKey(bytes32("alice")), 1, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.SuccessfullyProvisioned(_getPubKey(bytes32("alice")), 1, PUFFER_MODULE_0, 1); pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } @@ -525,7 +528,7 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); pufferProtocol.registerValidatorKey{ value: amount }( data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -551,7 +554,7 @@ contract PufferProtocolTest is UnitTestHelper { ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); pufferProtocol.registerValidatorKey{ value: amount }(data, PUFFER_MODULE_0, 0, new bytes[](0), deadline); vm.stopPrank(); @@ -567,7 +570,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 validatedEpochs = 4500; bytes[] memory vtConsumptionSignatures = _getTotalEpochsValidatedSignatures( - alice, validatedEpochs, deadline, IPufferProtocol.depositValidationTime.selector + alice, validatedEpochs, deadline, IPufferProtocolLogic.depositValidationTime.selector ); // We deposit 10 VT for alice (legacy VT) @@ -579,7 +582,7 @@ contract PufferProtocolTest is UnitTestHelper { // We then deposit validation time for Alice, it should burn 10 legacy VTs, and 10 of the validation time vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed( + emit IPufferProtocolEvents.ValidationTimeConsumed( alice, 10 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 10 ether ); // 10 Legacy VTs got burned pufferProtocol.depositValidationTime{ value: 0.1 ether }( @@ -610,7 +613,7 @@ contract PufferProtocolTest is UnitTestHelper { _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorLimitPerModuleChanged(500, 1); + emit IPufferProtocolEvents.ValidatorLimitPerModuleChanged(500, 1); pufferProtocol.setValidatorLimitPerModule(PUFFER_MODULE_0, 1); // Revert if the registration will be over the limit @@ -663,7 +666,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 16 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 16 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 16 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -701,7 +704,7 @@ contract PufferProtocolTest is UnitTestHelper { // Deposit for herself vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsDeposited(alice, alice, 200 ether); + emit IPufferProtocolEvents.ValidatorTicketsDeposited(alice, alice, 200 ether); pufferProtocol.depositValidatorTickets(alice, vtAmount); assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 200 ether, "protocol got 200 VT"); @@ -729,7 +732,7 @@ contract PufferProtocolTest is UnitTestHelper { // Deposit for herself vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsDeposited(alice, alice, 200 ether); + emit IPufferProtocolEvents.ValidatorTicketsDeposited(alice, alice, 200 ether); pufferProtocol.depositValidatorTickets(alice, vtAmount); assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 200 ether, "protocol got 200 VT"); @@ -783,7 +786,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.stopPrank(); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.NumberOfRegisteredValidatorsChanged(PUFFER_MODULE_0, 0); + emit IPufferProtocolEvents.NumberOfRegisteredValidatorsChanged(PUFFER_MODULE_0, 0); pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); assertApproxEqRel( @@ -803,7 +806,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(DAO); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.VTPenaltyChanged(penaltyETHAmount, newPenaltyAmount); + emit IPufferProtocolEvents.VTPenaltyChanged(penaltyETHAmount, newPenaltyAmount); pufferProtocol.setVTPenalty(newPenaltyAmount); assertEq(pufferProtocol.getVTPenalty(), newPenaltyAmount, "value after change"); @@ -865,7 +868,7 @@ contract PufferProtocolTest is UnitTestHelper { // Set penalty to 0 vm.startPrank(DAO); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.VTPenaltyChanged(20 * EPOCHS_PER_DAY, 0); + emit IPufferProtocolEvents.VTPenaltyChanged(20 * EPOCHS_PER_DAY, 0); pufferProtocol.setVTPenalty(0); vm.startPrank(alice); @@ -901,11 +904,11 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed( + emit IPufferProtocolEvents.ValidationTimeConsumed( alice, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); _executeFullWithdrawal( StoppedValidatorInfo({ module: NoRestakingModule, @@ -914,7 +917,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -940,7 +943,7 @@ contract PufferProtocolTest is UnitTestHelper { assertApproxEqAbs(_getUnderlyingETHAmount(address(alice)), 1.5 ether, 1, "alice got back the bond"); bytes[] memory vtConsumptionSignature = _getTotalEpochsValidatedSignatures( - alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ); // We've removed the validator data, meaning the validator status is 0 (UNINITIALIZED) @@ -972,7 +975,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsWithdrawn(alice, alice, aliceVTBalance); + emit IPufferProtocolEvents.ValidatorTicketsWithdrawn(alice, alice, aliceVTBalance); pufferProtocol.withdrawValidatorTickets(uint96(aliceVTBalance), alice); assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "0 vt token balance after"); @@ -985,7 +988,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(bob); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsWithdrawn(bob, alice, bobVTBalance); + emit IPufferProtocolEvents.ValidatorTicketsWithdrawn(bob, alice, bobVTBalance); pufferProtocol.withdrawValidatorTickets(uint96(bobVTBalance), alice); assertEq(pufferProtocol.getValidatorTicketsBalance(bob), 0, "0 vt token balance after bob"); @@ -1010,7 +1013,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: epochsValidated, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, epochsValidated, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, epochsValidated, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1023,7 +1026,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: epochsValidated, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - bob, epochsValidated, deadline, IPufferProtocol.batchHandleWithdrawals.selector + bob, epochsValidated, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1034,17 +1037,17 @@ contract PufferProtocolTest is UnitTestHelper { stopInfos[1] = bobInfo; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed( + emit IPufferProtocolEvents.ValidationTimeConsumed( alice, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed( + emit IPufferProtocolEvents.ValidationTimeConsumed( bob, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); pufferProtocol.batchHandleWithdrawals( stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline @@ -1088,7 +1091,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 35 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 35 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 35 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1100,7 +1103,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 31.9 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - bob, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + bob, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1112,7 +1115,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 31 ether, totalEpochsValidated: 34 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - charlie, 34 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + charlie, 34 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: true, isDownsize: false @@ -1124,7 +1127,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 31.8 ether, totalEpochsValidated: 48 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - dianna, 48 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + dianna, 48 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1136,25 +1139,25 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 31.5 ether, totalEpochsValidated: 2 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - eve, 2 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + eve, 2 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: true, isDownsize: false }); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed(alice, 0, 35 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + emit IPufferProtocolEvents.ValidationTimeConsumed(alice, 0, 35 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed(bob, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + emit IPufferProtocolEvents.ValidationTimeConsumed(bob, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( + emit IPufferProtocolEvents.ValidatorExited( _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.1 ether), 1 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed(charlie, 0, 34 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); - emit IPufferProtocol.ValidatorExited( + emit IPufferProtocolEvents.ValidationTimeConsumed(charlie, 0, 34 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + emit IPufferProtocolEvents.ValidatorExited( _getPubKey(bytes32("charlie")), 2, PUFFER_MODULE_0, @@ -1162,14 +1165,14 @@ contract PufferProtocolTest is UnitTestHelper { 1 ); // got slashed vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed(dianna, 0, 48 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); - emit IPufferProtocol.ValidatorExited( + emit IPufferProtocolEvents.ValidationTimeConsumed(dianna, 0, 48 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + emit IPufferProtocolEvents.ValidatorExited( _getPubKey(bytes32("dianna")), 3, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.2 ether), 1 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeConsumed(eve, 0, 2 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + emit IPufferProtocolEvents.ValidationTimeConsumed(eve, 0, 2 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( + emit IPufferProtocolEvents.ValidatorExited( _getPubKey(bytes32("eve")), 4, PUFFER_MODULE_0, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond, 1 ); // got slashed pufferProtocol.batchHandleWithdrawals( @@ -1228,7 +1231,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 65 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 65 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 65 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1301,7 +1304,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 120 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 120 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 120 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1359,7 +1362,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1372,19 +1375,19 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - bob, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + bob, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false }); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); // 10 days of VT - emit IPufferProtocol.ValidationTimeConsumed(alice, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); // 10 days of VT + emit IPufferProtocolEvents.ValidationTimeConsumed(alice, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); _executeFullWithdrawal(aliceInfo, deadline); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); // 10 days of VT - emit IPufferProtocol.ValidationTimeConsumed(bob, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); // 10 days of VT + emit IPufferProtocolEvents.ValidationTimeConsumed(bob, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH, 0); _executeFullWithdrawal(bobInfo, deadline); assertApproxEqAbs( @@ -1461,7 +1464,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 29 ether, totalEpochsValidated: 28 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: true, isDownsize: false @@ -1515,7 +1518,7 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, totalEpochsValidated: 28 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), withdrawalAmount: 29.5 ether, wasSlashed: true, @@ -1577,7 +1580,7 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, totalEpochsValidated: 28 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), withdrawalAmount: 30.9 ether, // 1.1 ETH slashed wasSlashed: true, @@ -1641,7 +1644,7 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, totalEpochsValidated: 28 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), withdrawalAmount: 31.9 ether, wasSlashed: false, @@ -1696,7 +1699,7 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 0, totalEpochsValidated: 15 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 15 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 15 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), withdrawalAmount: 32.1 ether, wasSlashed: false, @@ -1744,7 +1747,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 10 * EPOCHS_PER_DAY, // penalty is 10 vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 10 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 10 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), // penalty is 10 wasSlashed: false, isDownsize: false @@ -1786,7 +1789,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: 3 * EPOCHS_PER_DAY, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - alice, 3 * EPOCHS_PER_DAY, deadline, IPufferProtocol.batchHandleWithdrawals.selector + alice, 3 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false @@ -1809,8 +1812,8 @@ contract PufferProtocolTest is UnitTestHelper { // Register validator key by paying SC in ETH and depositing bond in pufETH vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidationTimeDeposited({ node: address(this), ethAmount: 7.5 ether }); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); + emit IPufferProtocolEvents.ValidationTimeDeposited({ node: address(this), ethAmount: 7.5 ether }); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); pufferProtocol.registerValidatorKey{ value: 9 ether }(data, PUFFER_MODULE_0, 0, new bytes[](0), deadline); // Protocol holds 7.5 ETHER @@ -1828,7 +1831,7 @@ contract PufferProtocolTest is UnitTestHelper { validatorTicket.approve(address(pufferProtocol), vtAmount); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsDeposited(bob, alice, vtAmount); + emit IPufferProtocolEvents.ValidatorTicketsDeposited(bob, alice, vtAmount); pufferProtocol.depositValidatorTickets(bob, vtAmount); vm.startPrank(bob); @@ -1999,11 +2002,11 @@ contract PufferProtocolTest is UnitTestHelper { uint256 deadline = block.timestamp + 1 days; bytes[] memory vtConsumptionSignatures = _getTotalEpochsValidatedSignatures( - nodeOperator, epochsValidated, deadline, IPufferProtocol.registerValidatorKey.selector + nodeOperator, epochsValidated, deadline, IPufferProtocolLogic.registerValidatorKey.selector ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: amount }( validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures, deadline ); @@ -2087,7 +2090,7 @@ contract PufferProtocolTest is UnitTestHelper { withdrawalAmount: 32 ether, totalEpochsValidated: type(uint256).max, vtConsumptionSignature: _getTotalEpochsValidatedSignatures( - bob, type(uint256).max, deadline, IPufferProtocol.batchHandleWithdrawals.selector + bob, type(uint256).max, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector ), wasSlashed: false, isDownsize: false From eab8cfc2f87dc56455fbb3995e257aad2e672b91 Mon Sep 17 00:00:00 2001 From: Eladio Date: Mon, 14 Jul 2025 13:46:08 +0200 Subject: [PATCH 67/82] Changed approach of the delegatecall to logic contract --- ...GenerateBLSKeysAndRegisterValidators.s.sol | 3 +- mainnet-contracts/script/SetupAccess.s.sol | 11 +- mainnet-contracts/src/PufferProtocol.sol | 120 ++++-------------- mainnet-contracts/src/PufferProtocolBase.sol | 3 +- mainnet-contracts/src/PufferProtocolLogic.sol | 15 +-- .../src/interface/IPufferProtocol.sol | 13 +- .../src/interface/IPufferProtocolEvents.sol | 1 - .../src/interface/IPufferProtocolFull.sol | 12 +- .../src/interface/IPufferProtocolLogic.sol | 3 - .../interface/IPufferProtocolManagement.sol | 29 +++++ .../test/handlers/PufferProtocolHandler.sol | 5 +- .../test/helpers/UnitTestHelper.sol | 5 +- .../invariant/PufferProtocolInvariants.sol | 8 +- .../test/unit/PufferProtocol.t.sol | 2 +- 14 files changed, 98 insertions(+), 132 deletions(-) create mode 100644 mainnet-contracts/src/interface/IPufferProtocolManagement.sol diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index b9102990..1cbd59bf 100644 --- a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol +++ b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol @@ -157,7 +157,8 @@ contract GenerateBLSKeysAndRegisterValidators is Script { uint256 numberOfGuardians = pufferProtocol.GUARDIAN_MODULE().getGuardians().length; bytes[] memory guardianPubKeys = pufferProtocol.GUARDIAN_MODULE().getGuardiansEnclavePubkeys(); address moduleAddress = IPufferProtocolFull(protocolAddress).getModuleAddress(moduleName); - bytes memory withdrawalCredentials = IPufferProtocolFull(protocolAddress).getWithdrawalCredentials(moduleAddress); + bytes memory withdrawalCredentials = + IPufferProtocolFull(protocolAddress).getWithdrawalCredentials(moduleAddress); string[] memory inputs = new string[](17); inputs[0] = "coral-cli"; diff --git a/mainnet-contracts/script/SetupAccess.s.sol b/mainnet-contracts/script/SetupAccess.s.sol index c71aab9b..10334034 100644 --- a/mainnet-contracts/script/SetupAccess.s.sol +++ b/mainnet-contracts/script/SetupAccess.s.sol @@ -21,6 +21,7 @@ import { GenerateAccessManagerCallData } from "../script/GenerateAccessManagerCa import { GenerateAccessManagerCalldata2 } from "../script/AccessManagerMigrations/GenerateAccessManagerCalldata2.s.sol"; import { GenerateRestakingOperatorCalldata } from "../script/AccessManagerMigrations/07_GenerateRestakingOperatorCalldata.s.sol"; +import { IPufferProtocolLogic } from "../src/interface/IPufferProtocolLogic.sol"; import { ROLE_ID_OPERATIONS_MULTISIG, @@ -322,8 +323,8 @@ contract SetupAccess is BaseScript { bytes4[] memory paymasterSelectors = new bytes4[](3); paymasterSelectors[0] = PufferProtocol.provisionNode.selector; - paymasterSelectors[1] = PufferProtocol.skipProvisioning.selector; - paymasterSelectors[2] = PufferProtocol.batchHandleWithdrawals.selector; + paymasterSelectors[1] = IPufferProtocolLogic.skipProvisioning.selector; + paymasterSelectors[2] = IPufferProtocolLogic.batchHandleWithdrawals.selector; calldatas[1] = abi.encodeWithSelector( AccessManager.setTargetFunctionRole.selector, @@ -333,12 +334,12 @@ contract SetupAccess is BaseScript { ); bytes4[] memory publicSelectors = new bytes4[](6); - publicSelectors[0] = PufferProtocol.registerValidatorKey.selector; + publicSelectors[0] = IPufferProtocolLogic.registerValidatorKey.selector; publicSelectors[1] = PufferProtocol.depositValidatorTickets.selector; publicSelectors[2] = PufferProtocol.withdrawValidatorTickets.selector; publicSelectors[3] = PufferProtocol.revertIfPaused.selector; - publicSelectors[4] = PufferProtocol.depositValidationTime.selector; - publicSelectors[5] = PufferProtocol.withdrawValidationTime.selector; + publicSelectors[4] = IPufferProtocolLogic.depositValidationTime.selector; + publicSelectors[5] = IPufferProtocolLogic.withdrawValidationTime.selector; calldatas[2] = abi.encodeWithSelector( AccessManager.setTargetFunctionRole.selector, diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index bba8a482..67fab5d7 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -33,7 +33,13 @@ import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ -contract PufferProtocol is IPufferProtocolEvents, IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolBase { +contract PufferProtocol is + IPufferProtocolEvents, + IPufferProtocol, + AccessManagedUpgradeable, + UUPSUpgradeable, + PufferProtocolBase +{ constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, @@ -58,6 +64,20 @@ contract PufferProtocol is IPufferProtocolEvents, IPufferProtocol, AccessManaged receive() external payable { } + fallback() external payable { + (bool success, bytes memory returnData) = _getPufferProtocolStorage().pufferProtocolLogic.delegatecall(msg.data); + + if (success) { + assembly { + return(add(returnData, 0x20), mload(returnData)) + } + } else { + assembly { + revert(add(returnData, 0x20), mload(returnData)) + } + } + } + /** * @notice Initializes the contract */ @@ -90,20 +110,6 @@ contract PufferProtocol is IPufferProtocolEvents, IPufferProtocol, AccessManaged emit ValidatorTicketsDeposited(node, msg.sender, amount); } - /** - * @inheritdoc IPufferProtocol - * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol - */ - function depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) - external - payable - restricted - { - bytes memory callData = - abi.encodeWithSelector(IPufferProtocolLogic.depositValidationTime.selector, epochsValidatedSignature); - _delegatecall(_getPufferProtocolStorage(), callData); - } - /** * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol @@ -131,38 +137,6 @@ contract PufferProtocol is IPufferProtocolEvents, IPufferProtocol, AccessManaged emit ValidatorTicketsWithdrawn(msg.sender, recipient, amount); } - /** - * @inheritdoc IPufferProtocol - * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol - */ - function withdrawValidationTime(uint96 amount, address recipient) external restricted { - bytes memory callData = - abi.encodeWithSelector(IPufferProtocolLogic.withdrawValidationTime.selector, amount, recipient); - _delegatecall(_getPufferProtocolStorage(), callData); - } - - /** - * @inheritdoc IPufferProtocol - * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol - */ - function registerValidatorKey( - ValidatorKeyData calldata data, - bytes32 moduleName, - uint256 totalEpochsValidated, - bytes[] calldata vtConsumptionSignature, - uint256 deadline - ) external payable restricted { - bytes memory callData = abi.encodeWithSelector( - IPufferProtocolLogic.registerValidatorKey.selector, - data, - moduleName, - totalEpochsValidated, - vtConsumptionSignature, - deadline - ); - _delegatecall(_getPufferProtocolStorage(), callData); - } - /** * @inheritdoc IPufferProtocol * @dev Restricted to Puffer Paymaster @@ -202,21 +176,6 @@ contract PufferProtocol is IPufferProtocolEvents, IPufferProtocol, AccessManaged $.validators[moduleName][index].status = Status.ACTIVE; } - /** - * @inheritdoc IPufferProtocol - * @dev Restricted to Node Operators - */ - function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) - external - payable - restricted - { - bytes memory callData = abi.encodeWithSelector( - IPufferProtocolLogic.requestConsolidation.selector, moduleName, srcIndices, targetIndices - ); - _delegatecall(_getPufferProtocolStorage(), callData); - } - /** * @inheritdoc IPufferProtocol * @dev Restricted to Node Operators @@ -269,31 +228,6 @@ contract PufferProtocol is IPufferProtocolEvents, IPufferProtocol, AccessManaged _PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); } - /** - * @inheritdoc IPufferProtocol - * @dev Restricted to Puffer Paymaster - */ - function batchHandleWithdrawals( - StoppedValidatorInfo[] calldata validatorInfos, - bytes[] calldata guardianEOASignatures, - uint256 deadline - ) external restricted { - bytes memory callData = abi.encodeWithSelector( - IPufferProtocolLogic.batchHandleWithdrawals.selector, validatorInfos, guardianEOASignatures, deadline - ); - _delegatecall(_getPufferProtocolStorage(), callData); - } - - /** - * @inheritdoc IPufferProtocol - * @dev Restricted to Puffer Paymaster - */ - function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external restricted { - bytes memory callData = - abi.encodeWithSelector(IPufferProtocolLogic.skipProvisioning.selector, moduleName, guardianEOASignatures); - _delegatecall(_getPufferProtocolStorage(), callData); - } - /** * @dev Restricted to the DAO */ @@ -592,17 +526,7 @@ contract PufferProtocol is IPufferProtocolEvents, IPufferProtocol, AccessManaged function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } - function _delegatecall(ProtocolStorage storage $, bytes memory callData) internal returns (bytes memory) { - (bool success, bytes memory result) = $.pufferProtocolLogic.delegatecall(callData); - if (!success) { - assembly { - revert(add(result, 32), mload(result)) - } - } - return result; - } - - function getPufferProtocolLogic() external view returns (address) { + function getPufferProtocolLogic() external view override returns (address) { return _getPufferProtocolStorage().pufferProtocolLogic; } diff --git a/mainnet-contracts/src/PufferProtocolBase.sol b/mainnet-contracts/src/PufferProtocolBase.sol index 54715aba..ca839411 100644 --- a/mainnet-contracts/src/PufferProtocolBase.sol +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -157,8 +157,7 @@ abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignature IPufferProtocolLogic.registerValidatorKey.selector; bytes32 internal constant _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME = IPufferProtocolLogic.depositValidationTime.selector; - bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = - IPufferProtocol.requestWithdrawal.selector; + bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = IPufferProtocol.requestWithdrawal.selector; bytes32 internal constant _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS = IPufferProtocolLogic.batchHandleWithdrawals.selector; diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index b695ec61..0f4dfb67 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -91,10 +91,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { } $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].validationTime += SafeCast.toUint96(msg.value); - emit ValidationTimeDeposited({ - node: epochsValidatedSignature.nodeOperator, - ethAmount: msg.value - }); + emit ValidationTimeDeposited({ node: epochsValidatedSignature.nodeOperator, ethAmount: msg.value }); } /** @@ -474,11 +471,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { // nosemgrep basic-arithmetic-underflow $.nodeOperatorInfo[nodeOperator].deprecated_vtBalance -= SafeCast.toUint96(vtBurnAmount); - emit ValidationTimeConsumed({ - node: nodeOperator, - consumedAmount: 0, - deprecated_burntVTs: vtBurnAmount - }); + emit ValidationTimeConsumed({ node: nodeOperator, consumedAmount: 0, deprecated_burntVTs: vtBurnAmount }); return vtAmountToBurn; } @@ -617,9 +610,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { function _decreaseNumberOfRegisteredValidators(ProtocolStorage storage $, bytes32 moduleName) internal { --$.moduleLimits[moduleName].numberOfRegisteredValidators; - emit NumberOfRegisteredValidatorsChanged( - moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators - ); + emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); } function _getBondBurnAmount( diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 97f77946..3b20287f 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -23,7 +23,6 @@ import { IBeaconDepositContract } from "../interface/IBeaconDepositContract.sol" * @custom:security-contact security@puffer.fi */ interface IPufferProtocol { - /** * @notice Returns validator information * @param moduleName is the staking Module @@ -89,7 +88,6 @@ interface IPufferProtocol { uint256 deadline ) external payable; - /** * @notice Returns the guardian module */ @@ -180,7 +178,6 @@ interface IPufferProtocol { */ function createPufferModule(bytes32 moduleName) external returns (address); - /** * @notice Returns the pending validator index for `moduleName` */ @@ -216,6 +213,16 @@ interface IPufferProtocol { */ function getMinimumVtAmount() external view returns (uint256); + /** + * @notice Returns the Puffer Protocol Logic + */ + function getPufferProtocolLogic() external view returns (address); + + /** + * @notice Returns the validation time for the `owner` + */ + function getValidationTime(address owner) external view returns (uint256); + /** * @notice Reverts if the system is paused */ diff --git a/mainnet-contracts/src/interface/IPufferProtocolEvents.sol b/mainnet-contracts/src/interface/IPufferProtocolEvents.sol index ecde2053..586a69b3 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolEvents.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolEvents.sol @@ -153,5 +153,4 @@ interface IPufferProtocolEvents { * @dev Signature "0xe271f36954242c619ce9d0f727a7d3b5f4db04666752aaeb20bca6d52098792a" */ event PufferProtocolLogicSet(address oldPufferProtocolLogic, address newPufferProtocolLogic); - } diff --git a/mainnet-contracts/src/interface/IPufferProtocolFull.sol b/mainnet-contracts/src/interface/IPufferProtocolFull.sol index b0feb249..a011aec1 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolFull.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolFull.sol @@ -4,5 +4,15 @@ pragma solidity >=0.8.0 <0.9.0; import { IPufferProtocol } from "./IPufferProtocol.sol"; import { IPufferProtocolLogic } from "./IPufferProtocolLogic.sol"; import { IPufferProtocolEvents } from "./IPufferProtocolEvents.sol"; +import { IPufferProtocolManagement } from "./IPufferProtocolManagement.sol"; +import { IAccessManaged } from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; -interface IPufferProtocolFull is IPufferProtocol, IPufferProtocolLogic, IPufferProtocolEvents {} +interface IPufferProtocolFull is + IPufferProtocol, + IPufferProtocolLogic, + IPufferProtocolEvents, + IPufferProtocolManagement, + IAccessManaged +{ + function nonces(bytes32 selector, address owner) external view returns (uint256); +} diff --git a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol index c8ac551f..fcc8403a 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -6,7 +6,6 @@ import { StoppedValidatorInfo } from "../struct/StoppedValidatorInfo.sol"; import { ValidatorKeyData } from "../struct/ValidatorKeyData.sol"; interface IPufferProtocolLogic { - /** * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`). * Deposits Validation Time for the `node`. Validation Time is in native ETH. @@ -60,7 +59,6 @@ interface IPufferProtocolLogic { external payable; - /** * @notice Skips the next validator for `moduleName` * @param moduleName The name of the module @@ -70,7 +68,6 @@ interface IPufferProtocolLogic { */ function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external; - /** * @notice Batch settling of validator withdrawals * @notice Settles a validator withdrawal diff --git a/mainnet-contracts/src/interface/IPufferProtocolManagement.sol b/mainnet-contracts/src/interface/IPufferProtocolManagement.sol new file mode 100644 index 00000000..d56613d2 --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferProtocolManagement.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +interface IPufferProtocolManagement { + /** + * @dev Restricted to the DAO + */ + function changeMinimumVTAmount(uint256 newMinimumVTAmount) external; + + /** + * @dev Restricted to the DAO + */ + function setModuleWeights(bytes32[] calldata newModuleWeights) external; + + /** + * @dev Restricted to the DAO + */ + function setValidatorLimitPerModule(bytes32 moduleName, uint128 limit) external; + + /** + * @dev Restricted to the DAO + */ + function setVTPenalty(uint256 newPenaltyAmount) external; + + /** + * @dev Restricted to the DAO + */ + function setPufferProtocolLogic(address newPufferProtocolLogic) external; +} diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index 9f48324f..49184ae1 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0 <0.9.0; import { IPufferProtocolEvents } from "../../src/interface/IPufferProtocolEvents.sol"; +import { IPufferProtocolFull } from "../../src/interface/IPufferProtocolFull.sol"; import { EnumerableMap } from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol"; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { RaveEvidence } from "../../src/struct/RaveEvidence.sol"; @@ -52,7 +53,7 @@ contract PufferProtocolHandler is Test { address DAO = makeAddr("DAO"); uint256[] guardiansEnclavePks; - PufferProtocol pufferProtocol; + IPufferProtocolFull pufferProtocol; IWETH weth; stETHMock stETH; @@ -136,7 +137,7 @@ contract PufferProtocolHandler is Test { } testhelper = helper; - pufferProtocol = protocol; + pufferProtocol = IPufferProtocolFull(address(protocol)); // This is after the upgrade to PufferVaultV5, when the WETH is the underlying asset weth = IWETH(vault.asset()); stETH = stETHMock(steth); diff --git a/mainnet-contracts/test/helpers/UnitTestHelper.sol b/mainnet-contracts/test/helpers/UnitTestHelper.sol index a8939968..13bb2653 100644 --- a/mainnet-contracts/test/helpers/UnitTestHelper.sol +++ b/mainnet-contracts/test/helpers/UnitTestHelper.sol @@ -38,6 +38,7 @@ import { ROLE_ID_LOCKBOX } from "../../script/Roles.sol"; import { GenerateSlashingELCalldata } from "../../script/AccessManagerMigrations/07_GenerateSlashingELCalldata.s.sol"; +import { IPufferProtocolFull } from "../../src/interface/IPufferProtocolFull.sol"; contract UnitTestHelper is Test, BaseScript { bytes32 private constant _PERMIT_TYPEHASH = @@ -90,7 +91,7 @@ contract UnitTestHelper is Test, BaseScript { stETHMock public stETH; IWETH public weth; - PufferProtocol public pufferProtocol; + IPufferProtocolFull public pufferProtocol; UpgradeableBeacon public beacon; PufferModuleManager public pufferModuleManager; ValidatorTicket public validatorTicket; @@ -201,7 +202,7 @@ contract UnitTestHelper is Test, BaseScript { (pufferDeployment, bridgingDeployment) = new DeployEverything().run(guardians, 1, PAYMASTER); - pufferProtocol = PufferProtocol(payable(pufferDeployment.pufferProtocol)); + pufferProtocol = IPufferProtocolFull(payable(pufferDeployment.pufferProtocol)); accessManager = AccessManager(pufferDeployment.accessManager); timelock = pufferDeployment.timelock; verifier = IEnclaveVerifier(pufferDeployment.enclaveVerifier); diff --git a/mainnet-contracts/test/invariant/PufferProtocolInvariants.sol b/mainnet-contracts/test/invariant/PufferProtocolInvariants.sol index 0aac5549..95c9f90f 100644 --- a/mainnet-contracts/test/invariant/PufferProtocolInvariants.sol +++ b/mainnet-contracts/test/invariant/PufferProtocolInvariants.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; import { PufferProtocolHandler } from "../handlers/PufferProtocolHandler.sol"; import { UnitTestHelper } from "../helpers/UnitTestHelper.sol"; +import { PufferProtocol } from "../../src/PufferProtocol.sol"; contract PufferProtocolInvariants is UnitTestHelper { PufferProtocolHandler handler; @@ -11,7 +12,12 @@ contract PufferProtocolInvariants is UnitTestHelper { super.setUp(); handler = new PufferProtocolHandler( - this, pufferVault, address(stETH), pufferProtocol, guardiansEnclavePks, _broadcaster + this, + pufferVault, + address(stETH), + PufferProtocol(payable(address(pufferProtocol))), + guardiansEnclavePks, + _broadcaster ); // Set handler as a target contract for invariant test diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index dd5e46cd..6e080020 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -494,7 +494,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 result = PufferProtocolMockUpgrade(payable(address(pufferVault))).returnSomething(); PufferProtocolMockUpgrade newImplementation = new PufferProtocolMockUpgrade(address(beacon)); - pufferProtocol.upgradeToAndCall(address(newImplementation), ""); + PufferProtocol(payable(address(pufferProtocol))).upgradeToAndCall(address(newImplementation), ""); result = PufferProtocolMockUpgrade(payable(address(pufferProtocol))).returnSomething(); From 495ac9d8b52d41fb2ff5bf9a61705057d46071fd Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 15 Jul 2025 12:07:34 +0200 Subject: [PATCH 68/82] Removed selector constants --- mainnet-contracts/src/PufferProtocol.sol | 2 +- mainnet-contracts/src/PufferProtocolBase.sol | 8 -------- mainnet-contracts/src/PufferProtocolLogic.sol | 6 +++--- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 67fab5d7..067cf999 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -218,7 +218,7 @@ contract PufferProtocol is node: msg.sender, pubKey: pubkeys[i], gweiAmount: gweiAmounts[i], - nonce: _useNonce(_FUNCTION_SELECTOR_REQUEST_WITHDRAWAL, msg.sender), + nonce: _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), deadline: deadline, guardianEOASignatures: validatorAmountsSignatures[i] }); diff --git a/mainnet-contracts/src/PufferProtocolBase.sol b/mainnet-contracts/src/PufferProtocolBase.sol index ca839411..080053af 100644 --- a/mainnet-contracts/src/PufferProtocolBase.sol +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -153,14 +153,6 @@ abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignature */ uint256 internal constant _32_ETH_GWEI = 32 * 10 ** 9; - bytes32 internal constant _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY = - IPufferProtocolLogic.registerValidatorKey.selector; - bytes32 internal constant _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME = - IPufferProtocolLogic.depositValidationTime.selector; - bytes32 internal constant _FUNCTION_SELECTOR_REQUEST_WITHDRAWAL = IPufferProtocol.requestWithdrawal.selector; - bytes32 internal constant _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS = - IPufferProtocolLogic.batchHandleWithdrawals.selector; - IGuardianModule internal immutable _GUARDIAN_MODULE; ValidatorTicket internal immutable _VALIDATOR_TICKET; diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 0f4dfb67..69b42717 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -82,7 +82,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { InvalidETHAmount() ); - epochsValidatedSignature.functionSelector = _FUNCTION_SELECTOR_DEPOSIT_VALIDATION_TIME; + epochsValidatedSignature.functionSelector = IPufferProtocolLogic.depositValidationTime.selector; uint256 burnAmount = _useVTOrValidationTime($, epochsValidatedSignature); @@ -160,7 +160,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { epochsValidatedSignature: EpochsValidatedSignature({ nodeOperator: msg.sender, totalEpochsValidated: totalEpochsValidated, - functionSelector: _FUNCTION_SELECTOR_REGISTER_VALIDATOR_KEY, + functionSelector: IPufferProtocolLogic.registerValidatorKey.selector, deadline: deadline, signatures: vtConsumptionSignature }), @@ -329,7 +329,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { EpochsValidatedSignature({ nodeOperator: bondWithdrawals[i].node, totalEpochsValidated: epochValidated, - functionSelector: _FUNCTION_SELECTOR_BATCH_HANDLE_WITHDRAWALS, + functionSelector: IPufferProtocolLogic.batchHandleWithdrawals.selector, deadline: deadline, signatures: vtConsumptionSignature }) From bf0c713be10d37231ae059d0f811053a7b375d3c Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 15 Jul 2025 12:12:19 +0200 Subject: [PATCH 69/82] Added some natspec to the fallback function --- mainnet-contracts/src/PufferProtocol.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 067cf999..8206e4d0 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -64,6 +64,12 @@ contract PufferProtocol is receive() external payable { } + /** + * @notice Fallback function to delegatecall the Puffer Protocol Logic + * @dev If a function selector is not found in this contract, it will delegatecall the Puffer Protocol Logic. + * This is done to be able to call functions from the Puffer Protocol Logic contract without having to + * declare them in this contract as well, manually forwarding them to the Puffer Protocol Logic contract. + */ fallback() external payable { (bool success, bytes memory returnData) = _getPufferProtocolStorage().pufferProtocolLogic.delegatecall(msg.data); From a19d9a04d33391adefe73b10a8019fd68cf851cf Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 15 Jul 2025 17:59:00 +0200 Subject: [PATCH 70/82] Moved logic from GuardianModule to Protocol to avoid re-deploying contract --- mainnet-contracts/src/GuardianModule.sol | 35 +++++-------------- mainnet-contracts/src/LibGuardianMessages.sol | 35 +++---------------- mainnet-contracts/src/PufferProtocol.sol | 22 ++++++------ mainnet-contracts/src/PufferProtocolBase.sol | 2 -- mainnet-contracts/src/PufferProtocolLogic.sol | 25 ++++++++----- .../src/interface/IGuardianModule.sol | 32 ++++------------- .../test/unit/PufferProtocol.t.sol | 13 ++++++- 7 files changed, 57 insertions(+), 107 deletions(-) diff --git a/mainnet-contracts/src/GuardianModule.sol b/mainnet-contracts/src/GuardianModule.sol index 73511835..c0233a97 100644 --- a/mainnet-contracts/src/GuardianModule.sol +++ b/mainnet-contracts/src/GuardianModule.sol @@ -217,22 +217,12 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule */ - function validateWithdrawalRequest( - address node, - bytes memory pubKey, - uint256 gweiAmount, - uint256 nonce, - uint256 deadline, - bytes[] calldata guardianEOASignatures - ) external view { + function validateWithdrawalRequest(bytes[] calldata eoaSignatures, bytes32 messageHash) external view { // Recreate the message hash - bytes32 signedMessageHash = - LibGuardianMessages._getWithdrawalRequestMessage(node, pubKey, gweiAmount, nonce, deadline); + bytes32 signedMessageHash = LibGuardianMessages._getAnyHashedMessage(messageHash); - bool validSignatures = validateGuardiansEOASignatures({ - eoaSignatures: guardianEOASignatures, - signedMessageHash: signedMessageHash - }); + bool validSignatures = + validateGuardiansEOASignatures({ eoaSignatures: eoaSignatures, signedMessageHash: signedMessageHash }); if (!validSignatures) { revert Unauthorized(); @@ -242,21 +232,12 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule */ - function validateTotalEpochsValidated( - address node, - uint256 totalEpochsValidated, - uint256 nonce, - uint256 deadline, - bytes[] calldata guardianEOASignatures - ) external view { + function validateTotalEpochsValidated(bytes[] calldata eoaSignatures, bytes32 messageHash) external view { // Recreate the message hash - bytes32 signedMessageHash = - LibGuardianMessages._getTotalEpochsValidatedMessage(node, totalEpochsValidated, nonce, deadline); + bytes32 signedMessageHash = LibGuardianMessages._getAnyHashedMessage(messageHash); - bool validSignatures = validateGuardiansEOASignatures({ - eoaSignatures: guardianEOASignatures, - signedMessageHash: signedMessageHash - }); + bool validSignatures = + validateGuardiansEOASignatures({ eoaSignatures: eoaSignatures, signedMessageHash: signedMessageHash }); if (!validSignatures) { revert Unauthorized(); diff --git a/mainnet-contracts/src/LibGuardianMessages.sol b/mainnet-contracts/src/LibGuardianMessages.sol index 4bc85784..76112907 100644 --- a/mainnet-contracts/src/LibGuardianMessages.sol +++ b/mainnet-contracts/src/LibGuardianMessages.sol @@ -88,39 +88,12 @@ library LibGuardianMessages { } /** - * @notice Returns the message to be signed for the total epochs validated - * @param node is the node operator address - * @param totalEpochsValidated is the total epochs validated - * @param nonce is the nonce for the node and the function selector - * @param deadline is the deadline of the signature + * @notice Returns the message to be signed for any message + * @param hashedMessage is the hashed message to be signed * @return the message to be signed */ - function _getTotalEpochsValidatedMessage( - address node, - uint256 totalEpochsValidated, - uint256 nonce, - uint256 deadline - ) internal pure returns (bytes32) { - return keccak256(abi.encode(node, totalEpochsValidated, nonce, deadline)).toEthSignedMessageHash(); - } - - /** - * @notice Returns the message to be signed for the withdrawal request - * @param node is the node operator address - * @param pubKey is the public key - * @param gweiAmount is the amount in gwei - * @param nonce is the nonce for the node and the function selector - * @param deadline is the deadline of the signature - * @return the message to be signed - */ - function _getWithdrawalRequestMessage( - address node, - bytes memory pubKey, - uint256 gweiAmount, - uint256 nonce, - uint256 deadline - ) internal pure returns (bytes32) { - return keccak256(abi.encode(node, pubKey, gweiAmount, nonce, deadline)).toEthSignedMessageHash(); + function _getAnyHashedMessage(bytes32 hashedMessage) internal pure returns (bytes32) { + return hashedMessage.toEthSignedMessageHash(); } } /* solhint-disable func-named-parameters */ diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 8206e4d0..d763fdd0 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -2,11 +2,11 @@ pragma solidity >=0.8.0 <0.9.0; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; -import { IPufferProtocolEvents } from "./interface/IPufferProtocolEvents.sol"; import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; import { IGuardianModule } from "./interface/IGuardianModule.sol"; @@ -19,7 +19,7 @@ import { ProtocolStorage, NodeInfo, ModuleLimit } from "./struct/ProtocolStorage import { LibBeaconchainContract } from "./LibBeaconchainContract.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; -import { InvalidAddress } from "./Errors.sol"; +import { Unauthorized, InvalidAddress } from "./Errors.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; @@ -34,12 +34,13 @@ import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; * Storage variables are located in PufferProtocolStorage.sol */ contract PufferProtocol is - IPufferProtocolEvents, IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolBase { + using MessageHashUtils for bytes32; + constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, @@ -220,14 +221,13 @@ contract PufferProtocol is // If downsize or rewards withdrawal, backend needs to validate the amount - _GUARDIAN_MODULE.validateWithdrawalRequest({ - node: msg.sender, - pubKey: pubkeys[i], - gweiAmount: gweiAmounts[i], - nonce: _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), - deadline: deadline, - guardianEOASignatures: validatorAmountsSignatures[i] - }); + // bytes32 messageHash = keccak256(abi.encode(msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), deadline)).toEthSignedMessageHash(); + bytes32 messageHash = keccak256(abi.encode(msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), deadline)).toEthSignedMessageHash(); + bool validSignatures = _GUARDIAN_MODULE.validateGuardiansEOASignatures(validatorAmountsSignatures[i], messageHash); + if (!validSignatures) { + revert Unauthorized(); + } + } } diff --git a/mainnet-contracts/src/PufferProtocolBase.sol b/mainnet-contracts/src/PufferProtocolBase.sol index 080053af..2a03ee37 100644 --- a/mainnet-contracts/src/PufferProtocolBase.sol +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; -import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; -import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; import { Status } from "./struct/Status.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 69b42717..a9b98276 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -3,6 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { ProtocolStorage } from "./struct/ProtocolStorage.sol"; import { Validator } from "./struct/Validator.sol"; import { Status } from "./struct/Validator.sol"; @@ -17,9 +18,11 @@ import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; -import { InvalidAddress } from "./Errors.sol"; +import { InvalidAddress, Unauthorized } from "./Errors.sol"; contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { + using MessageHashUtils for bytes32; + /** * @dev Helper struct for the full withdrawals accounting * The amounts of VT and pufETH to burn at the end of the withdrawal @@ -297,7 +300,11 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { revert DeadlineExceeded(); } - _GUARDIAN_MODULE.validateBatchWithdrawals(validatorInfos, guardianEOASignatures, deadline); + bytes32 messageHash = keccak256(abi.encode(validatorInfos, deadline)).toEthSignedMessageHash(); + bool validSignatures = _GUARDIAN_MODULE.validateGuardiansEOASignatures(guardianEOASignatures, messageHash); + if (!validSignatures) { + revert Unauthorized(); + } ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -391,13 +398,13 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { return; } - _GUARDIAN_MODULE.validateTotalEpochsValidated({ - node: node, - totalEpochsValidated: epochsValidatedSignature.totalEpochsValidated, - nonce: _useNonce(epochsValidatedSignature.functionSelector, node), - deadline: epochsValidatedSignature.deadline, - guardianEOASignatures: epochsValidatedSignature.signatures - }); + bytes32 messageHash = keccak256(abi.encode(node, epochsValidatedSignature.totalEpochsValidated, _useNonce(epochsValidatedSignature.functionSelector, node), epochsValidatedSignature.deadline)).toEthSignedMessageHash(); + + bool validSignatures = _GUARDIAN_MODULE.validateGuardiansEOASignatures(epochsValidatedSignature.signatures, messageHash); + + if (!validSignatures) { + revert Unauthorized(); + } uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); diff --git a/mainnet-contracts/src/interface/IGuardianModule.sol b/mainnet-contracts/src/interface/IGuardianModule.sol index 28e4549d..d68d1f5d 100644 --- a/mainnet-contracts/src/interface/IGuardianModule.sol +++ b/mainnet-contracts/src/interface/IGuardianModule.sol @@ -167,37 +167,17 @@ interface IGuardianModule { /** * @notice Validates the withdrawal request - * @param node The node operator address - * @param pubKey The public key - * @param gweiAmount The amount in gwei - * @param nonce The nonce for the node and the function selector - * @param deadline The deadline of the signature - * @param guardianEOASignatures The guardian EOA signatures + * @param eoaSignatures The guardian EOA signatures + * @param messageHash The message hash */ - function validateWithdrawalRequest( - address node, - bytes memory pubKey, - uint256 gweiAmount, - uint256 nonce, - uint256 deadline, - bytes[] calldata guardianEOASignatures - ) external view; + function validateWithdrawalRequest(bytes[] calldata eoaSignatures, bytes32 messageHash) external view; /** * @notice Validates the total epochs validated - * @param node The node operator address - * @param totalEpochsValidated The total epochs validated - * @param nonce The nonce for the node and the function selector - * @param deadline The deadline of the signature - * @param guardianEOASignatures The guardian EOA signatures + * @param eoaSignatures The guardian EOA signatures + * @param messageHash The message hash */ - function validateTotalEpochsValidated( - address node, - uint256 totalEpochsValidated, - uint256 nonce, - uint256 deadline, - bytes[] calldata guardianEOASignatures - ) external view; + function validateTotalEpochsValidated(bytes[] calldata eoaSignatures, bytes32 messageHash) external view; /** * @notice Returns the threshold value for guardian signatures diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index 6e080020..b860b617 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -27,9 +27,11 @@ import { ModuleLimit } from "../../src/struct/ProtocolStorage.sol"; import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol"; import { NodeInfo } from "../../src/struct/NodeInfo.sol"; import { EpochsValidatedSignature } from "../../src/struct/Signatures.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; contract PufferProtocolTest is UnitTestHelper { using ECDSA for bytes32; + using MessageHashUtils for bytes32; /** * @dev New bond is reduced from 2 to 1.5 ETH @@ -1878,7 +1880,7 @@ contract PufferProtocolTest is UnitTestHelper { uint256 nonce = pufferProtocol.nonces(funcSelector, node); bytes32 digest = - LibGuardianMessages._getTotalEpochsValidatedMessage(node, validatedEpochsTotal, nonce, deadline); + _getTotalEpochsValidatedMessage(node, validatedEpochsTotal, nonce, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SK, digest); bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. @@ -2133,6 +2135,15 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.depositValidatorTickets(alice, 0); vm.stopPrank(); } + + function _getTotalEpochsValidatedMessage( + address node, + uint256 totalEpochsValidated, + uint256 nonce, + uint256 deadline + ) internal pure returns (bytes32) { + return keccak256(abi.encode(node, totalEpochsValidated, nonce, deadline)).toEthSignedMessageHash(); + } } struct MerkleProofData { From 5501856d56cc6dd319e6b627f3a064b135bf79fa Mon Sep 17 00:00:00 2001 From: eladiosch <3090613+eladiosch@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:59:50 +0000 Subject: [PATCH 71/82] forge fmt --- mainnet-contracts/src/PufferProtocol.sol | 21 +++++++++++-------- mainnet-contracts/src/PufferProtocolLogic.sol | 14 ++++++++++--- .../test/unit/PufferProtocol.t.sol | 3 +-- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index d763fdd0..635e8e42 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -33,12 +33,7 @@ import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ -contract PufferProtocol is - IPufferProtocol, - AccessManagedUpgradeable, - UUPSUpgradeable, - PufferProtocolBase -{ +contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolBase { using MessageHashUtils for bytes32; constructor( @@ -222,12 +217,20 @@ contract PufferProtocol is // If downsize or rewards withdrawal, backend needs to validate the amount // bytes32 messageHash = keccak256(abi.encode(msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), deadline)).toEthSignedMessageHash(); - bytes32 messageHash = keccak256(abi.encode(msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), deadline)).toEthSignedMessageHash(); - bool validSignatures = _GUARDIAN_MODULE.validateGuardiansEOASignatures(validatorAmountsSignatures[i], messageHash); + bytes32 messageHash = keccak256( + abi.encode( + msg.sender, + pubkeys[i], + gweiAmounts[i], + _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), + deadline + ) + ).toEthSignedMessageHash(); + bool validSignatures = + _GUARDIAN_MODULE.validateGuardiansEOASignatures(validatorAmountsSignatures[i], messageHash); if (!validSignatures) { revert Unauthorized(); } - } } diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index a9b98276..2cdf3e2d 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -398,9 +398,17 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { return; } - bytes32 messageHash = keccak256(abi.encode(node, epochsValidatedSignature.totalEpochsValidated, _useNonce(epochsValidatedSignature.functionSelector, node), epochsValidatedSignature.deadline)).toEthSignedMessageHash(); - - bool validSignatures = _GUARDIAN_MODULE.validateGuardiansEOASignatures(epochsValidatedSignature.signatures, messageHash); + bytes32 messageHash = keccak256( + abi.encode( + node, + epochsValidatedSignature.totalEpochsValidated, + _useNonce(epochsValidatedSignature.functionSelector, node), + epochsValidatedSignature.deadline + ) + ).toEthSignedMessageHash(); + + bool validSignatures = + _GUARDIAN_MODULE.validateGuardiansEOASignatures(epochsValidatedSignature.signatures, messageHash); if (!validSignatures) { revert Unauthorized(); diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index b860b617..b4f43621 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -1879,8 +1879,7 @@ contract PufferProtocolTest is UnitTestHelper { ) internal view returns (bytes[] memory) { uint256 nonce = pufferProtocol.nonces(funcSelector, node); - bytes32 digest = - _getTotalEpochsValidatedMessage(node, validatedEpochsTotal, nonce, deadline); + bytes32 digest = _getTotalEpochsValidatedMessage(node, validatedEpochsTotal, nonce, deadline); (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SK, digest); bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. From f3dafabc3d0026c21679a6ddc0a4c0fd5e813344 Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 15 Jul 2025 18:34:58 +0200 Subject: [PATCH 72/82] Added basic checks to withdrawValidationTime and added missing natspec --- mainnet-contracts/src/PufferProtocolBase.sol | 6 ++++++ mainnet-contracts/src/PufferProtocolLogic.sol | 13 +++++++++++-- .../src/interface/IPufferProtocolEvents.sol | 5 +++++ .../src/interface/IPufferProtocolFull.sol | 15 +++++++++++++++ .../src/interface/IPufferProtocolLogic.sol | 5 +++++ .../src/interface/IPufferProtocolManagement.sol | 5 +++++ 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocolBase.sol b/mainnet-contracts/src/PufferProtocolBase.sol index 2a03ee37..7d21465a 100644 --- a/mainnet-contracts/src/PufferProtocolBase.sol +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -12,6 +12,12 @@ import { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; import { PufferProtocolStorage } from "./PufferProtocolStorage.sol"; import { IPufferProtocolEvents } from "./interface/IPufferProtocolEvents.sol"; +/** + * @title PufferProtocolBase + * @author Puffer Finance + * @notice This abstract contract contains constants, immutable variables, events and errors for the Puffer Protocol contract + * and the PufferProtocolLogic contract. Both of these contracts inherit from this one. + */ abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignatureNonces, IPufferProtocolEvents { /** * @notice Thrown when the deposit state that is provided doesn't match the one on Beacon deposit contract diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 2cdf3e2d..523c653d 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -18,8 +18,15 @@ import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; -import { InvalidAddress, Unauthorized } from "./Errors.sol"; - +import { InvalidAddress, Unauthorized, InvalidAmount } from "./Errors.sol"; + +/** + * @title PufferProtocolLogic + * @author Puffer Finance + * @notice This contract contains part of the logic for the Puffer Protocol + * @dev The functions in this contract are called by the PufferProtocol contract via delegatecall, + * therefore using PufferProtocol's storage + */ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { using MessageHashUtils for bytes32; @@ -102,6 +109,8 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ function withdrawValidationTime(uint96 amount, address recipient) external override { + require(recipient != address(0), InvalidAddress()); + require(amount > 0, InvalidAmount()); ProtocolStorage storage $ = _getPufferProtocolStorage(); // Node operator can only withdraw if they have no active or pending validators diff --git a/mainnet-contracts/src/interface/IPufferProtocolEvents.sol b/mainnet-contracts/src/interface/IPufferProtocolEvents.sol index 586a69b3..b52ddb86 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolEvents.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolEvents.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +/** + * @title IPufferProtocolEvents + * @author Puffer Finance + * @notice This interface contains the events emitted by the PufferProtocol contract + */ interface IPufferProtocolEvents { /** * @notice Emitted when the number of active validators changes diff --git a/mainnet-contracts/src/interface/IPufferProtocolFull.sol b/mainnet-contracts/src/interface/IPufferProtocolFull.sol index a011aec1..22f0e229 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolFull.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolFull.sol @@ -7,6 +7,13 @@ import { IPufferProtocolEvents } from "./IPufferProtocolEvents.sol"; import { IPufferProtocolManagement } from "./IPufferProtocolManagement.sol"; import { IAccessManaged } from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; + +/** + * @title IPufferProtocolFull + * @author Puffer Finance + * @notice This interface contains all the functions and events of the Puffer Protocol and the PufferProtocolLogic contract + * @dev This interface is used in tests and to use the whole Puffer Protocol in one contract + */ interface IPufferProtocolFull is IPufferProtocol, IPufferProtocolLogic, @@ -14,5 +21,13 @@ interface IPufferProtocolFull is IPufferProtocolManagement, IAccessManaged { + + /** + * @notice Returns the next unused nonce for an address in a specific function context. + * @dev Check ProtocolSignatureNonces.sol for more details + * @param selector The function selector that determines the nonce space + * @param owner The address to get the nonce for + * @return The current nonce value for the owner in the specified function context + */ function nonces(bytes32 selector, address owner) external view returns (uint256); } diff --git a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol index fcc8403a..548f9bcb 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolLogic.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -5,6 +5,11 @@ import { EpochsValidatedSignature } from "../struct/Signatures.sol"; import { StoppedValidatorInfo } from "../struct/StoppedValidatorInfo.sol"; import { ValidatorKeyData } from "../struct/ValidatorKeyData.sol"; +/** + * @title IPufferProtocolLogic + * @author Puffer Finance + * @notice This interface contains the functions that are implemented by the PufferProtocolLogic contract + */ interface IPufferProtocolLogic { /** * @notice New function that allows anybody to deposit ETH for a node operator (use this instead of `depositValidatorTickets`). diff --git a/mainnet-contracts/src/interface/IPufferProtocolManagement.sol b/mainnet-contracts/src/interface/IPufferProtocolManagement.sol index d56613d2..a85a9dba 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolManagement.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolManagement.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +/** + * @title IPufferProtocolManagement + * @author Puffer Finance + * @notice This interface contains the functions that are restricted to the DAO + */ interface IPufferProtocolManagement { /** * @dev Restricted to the DAO From 8d546b12b2f2c0492e91f66653b95a8dc7087d4c Mon Sep 17 00:00:00 2001 From: Eladio Date: Tue, 15 Jul 2025 18:35:14 +0200 Subject: [PATCH 73/82] forge fmt --- mainnet-contracts/src/interface/IPufferProtocolFull.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/mainnet-contracts/src/interface/IPufferProtocolFull.sol b/mainnet-contracts/src/interface/IPufferProtocolFull.sol index 22f0e229..6a59b7a4 100644 --- a/mainnet-contracts/src/interface/IPufferProtocolFull.sol +++ b/mainnet-contracts/src/interface/IPufferProtocolFull.sol @@ -7,7 +7,6 @@ import { IPufferProtocolEvents } from "./IPufferProtocolEvents.sol"; import { IPufferProtocolManagement } from "./IPufferProtocolManagement.sol"; import { IAccessManaged } from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; - /** * @title IPufferProtocolFull * @author Puffer Finance @@ -21,7 +20,6 @@ interface IPufferProtocolFull is IPufferProtocolManagement, IAccessManaged { - /** * @notice Returns the next unused nonce for an address in a specific function context. * @dev Check ProtocolSignatureNonces.sol for more details From 522f5032031308ae90e43e8235d6237d4713cb18 Mon Sep 17 00:00:00 2001 From: eladiosch Date: Wed, 16 Jul 2025 10:37:19 +0200 Subject: [PATCH 74/82] Update mainnet-contracts/src/PufferProtocol.sol Co-authored-by: Benjamin --- mainnet-contracts/src/PufferProtocol.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 635e8e42..f577d594 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -11,7 +11,6 @@ import { PufferModuleManager } from "./PufferModuleManager.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; -import { ValidatorKeyData } from "./struct/ValidatorKeyData.sol"; import { Validator } from "./struct/Validator.sol"; import { Status } from "./struct/Status.sol"; import { WithdrawalType } from "./struct/WithdrawalType.sol"; From e7fb64e27b878165bb7da2357f2bfb37316a9a6a Mon Sep 17 00:00:00 2001 From: eladiosch Date: Wed, 16 Jul 2025 10:37:45 +0200 Subject: [PATCH 75/82] Update mainnet-contracts/src/PufferProtocol.sol Co-authored-by: Benjamin --- mainnet-contracts/src/PufferProtocol.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index f577d594..c5f98785 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -19,7 +19,6 @@ import { LibBeaconchainContract } from "./LibBeaconchainContract.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { Unauthorized, InvalidAddress } from "./Errors.sol"; -import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; import { PufferModule } from "./PufferModule.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; import { PufferProtocolBase } from "./PufferProtocolBase.sol"; From 76a3233e8124b83e0b4beaf342f323801e1c68f7 Mon Sep 17 00:00:00 2001 From: eladiosch Date: Wed, 16 Jul 2025 10:38:18 +0200 Subject: [PATCH 76/82] Update mainnet-contracts/src/PufferProtocol.sol Co-authored-by: Benjamin --- mainnet-contracts/src/PufferProtocol.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index c5f98785..bfd81fba 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -20,7 +20,6 @@ import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { Unauthorized, InvalidAddress } from "./Errors.sol"; import { PufferModule } from "./PufferModule.sol"; -import { EpochsValidatedSignature } from "./struct/Signatures.sol"; import { PufferProtocolBase } from "./PufferProtocolBase.sol"; import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; From c90f5da8c6aae2e1c6bf712e34df1581e6262b51 Mon Sep 17 00:00:00 2001 From: eladiosch Date: Wed, 16 Jul 2025 10:38:42 +0200 Subject: [PATCH 77/82] Update mainnet-contracts/src/PufferProtocol.sol Co-authored-by: Benjamin --- mainnet-contracts/src/PufferProtocol.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index bfd81fba..ae997b98 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -21,7 +21,6 @@ import { ValidatorTicket } from "./ValidatorTicket.sol"; import { Unauthorized, InvalidAddress } from "./Errors.sol"; import { PufferModule } from "./PufferModule.sol"; import { PufferProtocolBase } from "./PufferProtocolBase.sol"; -import { IPufferProtocolLogic } from "./interface/IPufferProtocolLogic.sol"; /** * @title PufferProtocol From e181e10ecd9dc98e632feb5080ef1e638281d6a9 Mon Sep 17 00:00:00 2001 From: eladiosch Date: Wed, 16 Jul 2025 10:38:56 +0200 Subject: [PATCH 78/82] Update mainnet-contracts/src/PufferProtocolLogic.sol Co-authored-by: Benjamin --- mainnet-contracts/src/PufferProtocolLogic.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 523c653d..3a8c5da7 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -23,6 +23,7 @@ import { InvalidAddress, Unauthorized, InvalidAmount } from "./Errors.sol"; /** * @title PufferProtocolLogic * @author Puffer Finance + * @custom:security-contact security@puffer.fi * @notice This contract contains part of the logic for the Puffer Protocol * @dev The functions in this contract are called by the PufferProtocol contract via delegatecall, * therefore using PufferProtocol's storage From fad967d87e01f8f6726f6a8d07a9e0d366bcefce Mon Sep 17 00:00:00 2001 From: eladiosch Date: Wed, 16 Jul 2025 10:39:09 +0200 Subject: [PATCH 79/82] Update mainnet-contracts/src/PufferProtocol.sol Co-authored-by: Benjamin --- mainnet-contracts/src/PufferProtocol.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index ae997b98..70c1f805 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -212,7 +212,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // If downsize or rewards withdrawal, backend needs to validate the amount - // bytes32 messageHash = keccak256(abi.encode(msg.sender, pubkeys[i], gweiAmounts[i], _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), deadline)).toEthSignedMessageHash(); bytes32 messageHash = keccak256( abi.encode( msg.sender, From b982b2ed25c02eec2b1d34bbedd84b06a4333284 Mon Sep 17 00:00:00 2001 From: Eladio Date: Wed, 16 Jul 2025 12:25:08 +0200 Subject: [PATCH 80/82] Using modifier (with require) to check deadlines --- mainnet-contracts/src/PufferProtocol.sol | 6 +----- mainnet-contracts/src/PufferProtocolBase.sol | 5 +++++ mainnet-contracts/src/PufferProtocolLogic.sol | 17 +++-------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 70c1f805..b08f0a00 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -185,11 +185,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad WithdrawalType[] calldata withdrawalType, bytes[][] calldata validatorAmountsSignatures, uint256 deadline - ) external payable restricted { - if (block.timestamp > deadline) { - revert DeadlineExceeded(); - } - + ) external payable restricted validDeadline(deadline) { ProtocolStorage storage $ = _getPufferProtocolStorage(); bytes[] memory pubkeys = new bytes[](indices.length); diff --git a/mainnet-contracts/src/PufferProtocolBase.sol b/mainnet-contracts/src/PufferProtocolBase.sol index 7d21465a..138d3faa 100644 --- a/mainnet-contracts/src/PufferProtocolBase.sol +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -171,6 +171,11 @@ abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignature address payable internal immutable _PUFFER_REVENUE_DISTRIBUTOR; + modifier validDeadline(uint256 deadline) { + require(block.timestamp <= deadline, DeadlineExceeded()); + _; + } + constructor( PufferVaultV5 pufferVault, IGuardianModule guardianModule, diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 3a8c5da7..69de54dd 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -78,11 +78,8 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { external payable override + validDeadline(epochsValidatedSignature.deadline) { - if (block.timestamp > epochsValidatedSignature.deadline) { - revert DeadlineExceeded(); - } - require(epochsValidatedSignature.nodeOperator != address(0), InvalidAddress()); ProtocolStorage storage $ = _getPufferProtocolStorage(); uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); @@ -146,11 +143,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { uint256 totalEpochsValidated, bytes[] calldata vtConsumptionSignature, uint256 deadline - ) external payable override { - if (block.timestamp > deadline) { - revert DeadlineExceeded(); - } - + ) external payable override validDeadline(deadline) { ProtocolStorage storage $ = _getPufferProtocolStorage(); _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); @@ -305,11 +298,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { StoppedValidatorInfo[] calldata validatorInfos, bytes[] calldata guardianEOASignatures, uint256 deadline - ) external payable override { - if (block.timestamp > deadline) { - revert DeadlineExceeded(); - } - + ) external payable override validDeadline(deadline) { bytes32 messageHash = keccak256(abi.encode(validatorInfos, deadline)).toEthSignedMessageHash(); bool validSignatures = _GUARDIAN_MODULE.validateGuardiansEOASignatures(guardianEOASignatures, messageHash); if (!validSignatures) { From 3c16eb37e71ef07aa0059735e2f4acbc2574f452 Mon Sep 17 00:00:00 2001 From: Eladio Date: Wed, 16 Jul 2025 13:17:02 +0200 Subject: [PATCH 81/82] Changed reverts to require and refactored to avoid stack too deep --- mainnet-contracts/src/PufferProtocol.sol | 76 +++++++++---------- mainnet-contracts/src/PufferProtocolBase.sol | 6 ++ mainnet-contracts/src/PufferProtocolLogic.sol | 47 ++++-------- 3 files changed, 55 insertions(+), 74 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index b08f0a00..f2901e1b 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -18,7 +18,7 @@ import { ProtocolStorage, NodeInfo, ModuleLimit } from "./struct/ProtocolStorage import { LibBeaconchainContract } from "./LibBeaconchainContract.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; -import { Unauthorized, InvalidAddress } from "./Errors.sol"; +import { InvalidAddress } from "./Errors.sol"; import { PufferModule } from "./PufferModule.sol"; import { PufferProtocolBase } from "./PufferProtocolBase.sol"; @@ -80,9 +80,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @notice Initializes the contract */ function initialize(address accessManager, address pufferProtocolLogic) external initializer { - if (address(accessManager) == address(0)) { - revert InvalidAddress(); - } + require(address(accessManager) != address(0), InvalidAddress()); __AccessManaged_init(accessManager); _createPufferModule(_PUFFER_MODULE_0); _changeMinimumVTAmount(30 * _EPOCHS_PER_DAY); // 30 days worth of ETH is the minimum VT amount @@ -96,9 +94,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function depositValidatorTickets(address node, uint256 amount) external restricted { - if (node == address(0)) { - revert InvalidAddress(); - } + require(node != address(0), InvalidAddress()); // slither-disable-next-line unchecked-transfer _VALIDATOR_TICKET.transferFrom(msg.sender, address(this), amount); @@ -118,12 +114,11 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad // Node operator can only withdraw if they have no active or pending validators // In the future, we plan to allow node operators to withdraw VTs even if they have active/pending validators. - if ( + require( $.nodeOperatorInfo[msg.sender].activeValidatorCount + $.nodeOperatorInfo[msg.sender].pendingValidatorCount - != 0 - ) { - revert ActiveOrPendingValidatorsExist(); - } + == 0, + ActiveOrPendingValidatorsExist() + ); // Reverts if insufficient balance // nosemgrep basic-arithmetic-underflow @@ -140,9 +135,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @dev Restricted to Puffer Paymaster */ function provisionNode(bytes calldata validatorSignature, bytes32 depositRootHash) external restricted { - if (depositRootHash != _BEACON_DEPOSIT_CONTRACT.get_deposit_root()) { - revert InvalidDepositRootHash(); - } + require(depositRootHash == _BEACON_DEPOSIT_CONTRACT.get_deposit_root(), InvalidDepositRootHash()); ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -186,46 +179,51 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad bytes[][] calldata validatorAmountsSignatures, uint256 deadline ) external payable restricted validDeadline(deadline) { - ProtocolStorage storage $ = _getPufferProtocolStorage(); + // Using internal function to avoid stack too deep + bytes[] memory pubkeys = _processWithdrawalValidation( + moduleName, indices, gweiAmounts, withdrawalType, validatorAmountsSignatures, deadline + ); - bytes[] memory pubkeys = new bytes[](indices.length); + _PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); + } + + function _processWithdrawalValidation( + bytes32 moduleName, + uint256[] calldata indices, + uint64[] calldata gweiAmounts, + WithdrawalType[] calldata withdrawalType, + bytes[][] calldata validatorAmountsSignatures, + uint256 deadline + ) internal returns (bytes[] memory pubkeys) { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + pubkeys = new bytes[](indices.length); - // validate pubkeys belong to that node and are active for (uint256 i = 0; i < indices.length; ++i) { Validator memory validator = $.validators[moduleName][indices[i]]; require(validator.node == msg.sender, InvalidValidator()); pubkeys[i] = validator.pubKey; + uint64 gweiAmount = gweiAmounts[i]; if (withdrawalType[i] == WithdrawalType.EXIT_VALIDATOR) { - require(gweiAmounts[i] == 0, InvalidWithdrawAmount()); + require(gweiAmount == 0, InvalidWithdrawAmount()); } else { if (withdrawalType[i] == WithdrawalType.DOWNSIZE) { - uint256 batches = gweiAmounts[i] / _32_ETH_GWEI; - require( - batches > validator.numBatches && gweiAmounts[i] % _32_ETH_GWEI == 0, InvalidWithdrawAmount() - ); + uint256 batches = gweiAmount / _32_ETH_GWEI; + require(batches > validator.numBatches && gweiAmount % _32_ETH_GWEI == 0, InvalidWithdrawAmount()); } - // If downsize or rewards withdrawal, backend needs to validate the amount - bytes32 messageHash = keccak256( abi.encode( msg.sender, pubkeys[i], - gweiAmounts[i], + gweiAmount, _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), deadline ) ).toEthSignedMessageHash(); - bool validSignatures = - _GUARDIAN_MODULE.validateGuardiansEOASignatures(validatorAmountsSignatures[i], messageHash); - if (!validSignatures) { - revert Unauthorized(); - } + _validateSignatures(messageHash, validatorAmountsSignatures[i]); } } - - _PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); } /** @@ -442,18 +440,14 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad function _setValidatorLimitPerModule(bytes32 moduleName, uint128 limit) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); - if (limit < $.moduleLimits[moduleName].numberOfRegisteredValidators) { - revert ValidatorLimitForModuleReached(); - } + require($.moduleLimits[moduleName].numberOfRegisteredValidators <= limit, ValidatorLimitForModuleReached()); emit ValidatorLimitPerModuleChanged($.moduleLimits[moduleName].allowedLimit, limit); $.moduleLimits[moduleName].allowedLimit = limit; } function _setVTPenalty(uint256 newPenaltyAmount) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); - if (newPenaltyAmount > $.minimumVtAmount) { - revert InvalidVTAmount(); - } + require(newPenaltyAmount <= $.minimumVtAmount, InvalidVTAmount()); emit VTPenaltyChanged($.vtPenaltyEpochs, newPenaltyAmount); $.vtPenaltyEpochs = newPenaltyAmount; } @@ -466,9 +460,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad function _createPufferModule(bytes32 moduleName) internal returns (address) { ProtocolStorage storage $ = _getPufferProtocolStorage(); - if (address($.modules[moduleName]) != address(0)) { - revert ModuleAlreadyExists(); - } + require(address($.modules[moduleName]) == address(0), ModuleAlreadyExists()); PufferModule module = _PUFFER_MODULE_MANAGER.createNewPufferModule(moduleName); $.modules[moduleName] = module; $.moduleWeights.push(moduleName); diff --git a/mainnet-contracts/src/PufferProtocolBase.sol b/mainnet-contracts/src/PufferProtocolBase.sol index 138d3faa..40a3d8fe 100644 --- a/mainnet-contracts/src/PufferProtocolBase.sol +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0 <0.9.0; import { Status } from "./struct/Status.sol"; +import { Unauthorized } from "./Errors.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; import { IGuardianModule } from "./interface/IGuardianModule.sol"; @@ -193,4 +194,9 @@ abstract contract PufferProtocolBase is PufferProtocolStorage, ProtocolSignature _BEACON_DEPOSIT_CONTRACT = IBeaconDepositContract(beaconDepositContract); _PUFFER_REVENUE_DISTRIBUTOR = pufferRevenueDistributor; } + + function _validateSignatures(bytes32 messageHash, bytes[] memory guardianEOASignatures) internal view { + bool validSignatures = _GUARDIAN_MODULE.validateGuardiansEOASignatures(guardianEOASignatures, messageHash); + require(validSignatures, Unauthorized()); + } } diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index 69de54dd..ffaf68c6 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -18,7 +18,7 @@ import { IGuardianModule } from "./interface/IGuardianModule.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { EpochsValidatedSignature } from "./struct/Signatures.sol"; -import { InvalidAddress, Unauthorized, InvalidAmount } from "./Errors.sol"; +import { InvalidAddress, InvalidAmount } from "./Errors.sol"; /** * @title PufferProtocolLogic @@ -113,12 +113,11 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { // Node operator can only withdraw if they have no active or pending validators // In the future, we plan to allow node operators to withdraw VTs even if they have active/pending validators. - if ( + require( $.nodeOperatorInfo[msg.sender].activeValidatorCount + $.nodeOperatorInfo[msg.sender].pendingValidatorCount - != 0 - ) { - revert ActiveOrPendingValidatorsExist(); - } + == 0, + ActiveOrPendingValidatorsExist() + ); // Reverts if insufficient balance // nosemgrep basic-arithmetic-underflow @@ -154,10 +153,10 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { // The node operator must deposit 1.5 ETH (per batch) or more + minimum validation time for ~30 days // At the moment that's roughly 30 days * 225 (there is roughly 225 epochs per day) - uint256 minimumETHRequired = - bondAmountEth + (numBatches * _MINIMUM_EPOCHS_VALIDATION_REGISTRATION * epochCurrentPrice); - - require(msg.value >= minimumETHRequired, InvalidETHAmount()); + require( + msg.value >= bondAmountEth + (numBatches * _MINIMUM_EPOCHS_VALIDATION_REGISTRATION * epochCurrentPrice), + InvalidETHAmount() + ); emit ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - bondAmountEth) }); @@ -217,12 +216,8 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { payable override { - if (srcIndices.length == 0) { - revert InputArrayLengthZero(); - } - if (srcIndices.length != targetIndices.length) { - revert InputArrayLengthMismatch(); - } + require(srcIndices.length > 0, InputArrayLengthZero()); + require(srcIndices.length == targetIndices.length, InputArrayLengthMismatch()); ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -300,10 +295,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { uint256 deadline ) external payable override validDeadline(deadline) { bytes32 messageHash = keccak256(abi.encode(validatorInfos, deadline)).toEthSignedMessageHash(); - bool validSignatures = _GUARDIAN_MODULE.validateGuardiansEOASignatures(guardianEOASignatures, messageHash); - if (!validSignatures) { - revert Unauthorized(); - } + _validateSignatures(messageHash, guardianEOASignatures); ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -318,9 +310,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { Validator storage validator = $.validators[validatorInfos[i].moduleName][validatorInfos[i].pufferModuleIndex]; - if (validator.status != Status.ACTIVE) { - revert InvalidValidatorState(validator.status); - } + require(validator.status == Status.ACTIVE, InvalidValidatorState(validator.status)); // Save the Node address for the bond transfer bondWithdrawals[i].node = validator.node; @@ -406,12 +396,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { ) ).toEthSignedMessageHash(); - bool validSignatures = - _GUARDIAN_MODULE.validateGuardiansEOASignatures(epochsValidatedSignature.signatures, messageHash); - - if (!validSignatures) { - revert Unauthorized(); - } + _validateSignatures(messageHash, epochsValidatedSignature.signatures); uint256 epochCurrentPrice = _PUFFER_ORACLE.getValidatorTicketPrice(); @@ -529,9 +514,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { //solhint-disable-next-line avoid-low-level-calls (bool success,) = PufferModule(payable(validatorInfos[i].module)).call(address(_PUFFER_VAULT), transferAmount, ""); - if (!success) { - revert Failed(); - } + require(success, Failed()); // Skip the empty transfer (validator got slashed) if (bondWithdrawals[i].pufETHAmount == 0) { From 3599a63d4271f53d4daa9cadd3bea9a5dd90e34e Mon Sep 17 00:00:00 2001 From: Eladio Date: Wed, 16 Jul 2025 17:57:12 +0200 Subject: [PATCH 82/82] Added restricted for the Logic contract and improved natspec --- mainnet-contracts/src/PufferProtocol.sol | 4 +++- mainnet-contracts/src/PufferProtocolLogic.sol | 16 +++++++++++----- mainnet-contracts/test/unit/PufferProtocol.t.sol | 15 +++++++++++++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index f2901e1b..45a51c1a 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -61,8 +61,10 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad * @dev If a function selector is not found in this contract, it will delegatecall the Puffer Protocol Logic. * This is done to be able to call functions from the Puffer Protocol Logic contract without having to * declare them in this contract as well, manually forwarding them to the Puffer Protocol Logic contract. + * @dev This function is restricted, so it checks if the caller can call the function in the PufferProtocolLogic + * contract. This is using the AccessManager from the PufferProtocol contract. */ - fallback() external payable { + fallback() external payable restricted { (bool success, bytes memory returnData) = _getPufferProtocolStorage().pufferProtocolLogic.delegatecall(msg.data); if (success) { diff --git a/mainnet-contracts/src/PufferProtocolLogic.sol b/mainnet-contracts/src/PufferProtocolLogic.sol index ffaf68c6..b56669bd 100644 --- a/mainnet-contracts/src/PufferProtocolLogic.sol +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -71,8 +71,9 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { { } /** - * @notice Check IPufferProtocol.depositValidationTime + * @inheritdoc IPufferProtocolLogic * @dev This function should only be called by the PufferProtocol contract through a delegatecall + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ function depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) external @@ -103,8 +104,9 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { } /** - * @notice Check IPufferProtocol.withdrawValidationTime + * @inheritdoc IPufferProtocolLogic * @dev This function should only be called by the PufferProtocol contract through a delegatecall + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ function withdrawValidationTime(uint96 amount, address recipient) external override { require(recipient != address(0), InvalidAddress()); @@ -133,8 +135,9 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { } /** - * @notice Check IPufferProtocol.registerValidatorKey + * @inheritdoc IPufferProtocolLogic * @dev This function should only be called by the PufferProtocol contract through a delegatecall + * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol */ function registerValidatorKey( ValidatorKeyData calldata data, @@ -209,6 +212,7 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { } /** + * @inheritdoc IPufferProtocolLogic * @dev This function should only be called by the PufferProtocol contract through a delegatecall */ function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) @@ -248,8 +252,9 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { } /** - * @notice Check IPufferProtocol.skipProvisioning + * @inheritdoc IPufferProtocolLogic * @dev This function should only be called by the PufferProtocol contract through a delegatecall + * @dev Restricted to Puffer Paymaster */ function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external override { ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -286,8 +291,9 @@ contract PufferProtocolLogic is PufferProtocolBase, IPufferProtocolLogic { } /** - * @notice Check IPufferProtocol.batchHandleWithdrawals + * @inheritdoc IPufferProtocolLogic * @dev This function should only be called by the PufferProtocol contract through a delegatecall + * @dev Restricted to Puffer Paymaster */ function batchHandleWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index b4f43621..3e88c036 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.0 <0.9.0; import { PufferProtocolMockUpgrade } from "../mocks/PufferProtocolMockUpgrade.sol"; import { UnitTestHelper } from "../helpers/UnitTestHelper.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { IAccessManaged } from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; import { IPufferProtocol } from "../../src/interface/IPufferProtocol.sol"; import { IPufferProtocolLogic } from "../../src/interface/IPufferProtocolLogic.sol"; import { IPufferProtocolFull } from "../../src/interface/IPufferProtocolFull.sol"; @@ -630,8 +631,6 @@ contract PufferProtocolTest is UnitTestHelper { } function test_claim_bond_for_single_withdrawal() external { - uint256 startTimestamp = 1707411226; // TODO Remove this if not used - // Alice registers one validator and we provision it vm.deal(alice, 3 ether); vm.deal(NoRestakingModule, 200 ether); @@ -1842,6 +1841,18 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(validatorTicket.balanceOf(bob), 50 ether, "bob got the VT"); } + function test_batchHandleWithdrawals_restricted() public { + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, alice)); + pufferProtocol.batchHandleWithdrawals(new StoppedValidatorInfo[](0), new bytes[](0), block.timestamp); + } + + function test_skipProvisioning_restricted() public { + vm.startPrank(alice); + vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, alice)); + pufferProtocol.skipProvisioning(0, new bytes[](0)); + } + function _getGuardianSignaturesForSkipping() internal view returns (bytes[] memory) { (bytes32 moduleName, uint256 pendingIdx) = pufferProtocol.getNextValidatorToProvision();