From ddf6531a72f61bc9179ebacdd317560faf7c6f60 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Wed, 19 Mar 2025 17:36:33 +0100 Subject: [PATCH 1/2] inhouse wip --- mainnet-contracts/script/DeployPuffer.s.sol | 19 +- .../DeployPufferModuleImplementation.s.sol | 2 +- .../DeployPufferProtocolImplementation.s.sol | 4 +- mainnet-contracts/script/DeployerHelper.s.sol | 21 ++ ...GenerateBLSKeysAndRegisterValidators.s.sol | 4 +- mainnet-contracts/script/UpgradePufETH.s.sol | 2 +- mainnet-contracts/src/Errors.sol | 6 + .../src/PufferNoRestakingValidator.sol | 148 ++++++++++++++ mainnet-contracts/src/PufferProtocol.sol | 183 +++++++++++++----- .../interface/IPufferNoRestakingValidator.sol | 17 ++ .../src/interface/IPufferProtocol.sol | 8 + .../test/Integration/PufferVaultV2.fork.t.sol | 1 - .../ValidatorTicketMainnetTest.fork.t.sol | 5 +- .../mocks/MockWithdrawalRequestPredeploy.sol | 10 + .../test/mocks/PufferProtocolMockUpgrade.sol | 15 +- .../test/unit/ValidatorTicket.t.sol | 9 +- 16 files changed, 379 insertions(+), 75 deletions(-) create mode 100644 mainnet-contracts/src/PufferNoRestakingValidator.sol create mode 100644 mainnet-contracts/src/interface/IPufferNoRestakingValidator.sol create mode 100644 mainnet-contracts/test/mocks/MockWithdrawalRequestPredeploy.sol diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index e4123abd..65896157 100644 --- a/mainnet-contracts/script/DeployPuffer.s.sol +++ b/mainnet-contracts/script/DeployPuffer.s.sol @@ -27,6 +27,8 @@ import { IPufferOracleV2 } from "../src/interface/IPufferOracleV2.sol"; import { IRewardsCoordinator } from "../src/interface/EigenLayer/IRewardsCoordinator.sol"; import { AVSContractsRegistry } from "../src/AVSContractsRegistry.sol"; import { RewardsCoordinatorMock } from "../test/mocks/RewardsCoordinatorMock.sol"; +import { PufferNoRestakingValidator } from "../src/PufferNoRestakingValidator.sol"; +import { MockWithdrawalRequestPredeploy } from "../test/mocks/MockWithdrawalRequestPredeploy.sol"; /** * @title DeployPuffer @@ -56,6 +58,9 @@ contract DeployPuffer is BaseScript { OperationsCoordinator operationsCoordinator; ValidatorTicketPricer validatorTicketPricer; AVSContractsRegistry aVSContractsRegistry; + PufferNoRestakingValidator noRestakingETHRecipient; + MockWithdrawalRequestPredeploy withdrawalRequestPredeploy; + ValidatorTicket validatorTicketImplementation; address eigenPodManager; address delegationManager; @@ -101,7 +106,7 @@ contract DeployPuffer is BaseScript { validatorTicketPricer = new ValidatorTicketPricer(PufferOracleV2(oracle), address(accessManager)); validatorTicketProxy = new ERC1967Proxy(address(new NoImplementation()), ""); - ValidatorTicket validatorTicketImplementation = new ValidatorTicket({ + validatorTicketImplementation = new ValidatorTicket({ guardianModule: payable(guardiansDeployment.guardianModule), treasury: payable(treasury), pufferVault: payable(pufferVault), @@ -146,6 +151,15 @@ contract DeployPuffer is BaseScript { aVSContractsRegistry = new AVSContractsRegistry(address(accessManager)); + withdrawalRequestPredeploy = new MockWithdrawalRequestPredeploy(); + + noRestakingETHRecipient = new PufferNoRestakingValidator({ + protocol: address(proxy), + accessManager: address(accessManager), + beaconDepositContract: getStakingContract(), + withdrawalRequestPredeploy: address(withdrawalRequestPredeploy) + }); + // Puffer Service implementation pufferProtocolImpl = new PufferProtocol({ pufferVault: PufferVaultV2(payable(pufferVault)), @@ -153,7 +167,8 @@ contract DeployPuffer is BaseScript { guardianModule: GuardianModule(payable(guardiansDeployment.guardianModule)), moduleManager: address(moduleManagerProxy), oracle: IPufferOracleV2(oracle), - beaconDepositContract: getStakingContract() + beaconDepositContract: getStakingContract(), + noRestakingETHRecipient: noRestakingETHRecipient }); } diff --git a/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol b/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol index 120918c0..83eaf415 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())), diff --git a/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol b/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol index 7459b524..2d956eb3 100644 --- a/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol +++ b/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol @@ -14,6 +14,7 @@ import { stdJson } from "forge-std/StdJson.sol"; import { IPufferOracleV2 } from "../src/interface/IPufferOracleV2.sol"; import { GuardianModule } from "../src/GuardianModule.sol"; import { DeployerHelper } from "./DeployerHelper.s.sol"; +import { PufferNoRestakingValidator } from "../src/PufferNoRestakingValidator.sol"; /** * forge script script/DeployPufferProtocolImplementation.s.sol:DeployPufferProtocolImplementation --rpc-url=$RPC_URL --private-key $PK @@ -29,7 +30,8 @@ contract DeployPufferProtocolImplementation is DeployerHelper { guardianModule: GuardianModule(payable(_getGuardianModule())), moduleManager: _getPufferModuleManager(), oracle: IPufferOracleV2(_getPufferOracle()), - beaconDepositContract: _getBeaconDepositContract() + beaconDepositContract: _getBeaconDepositContract(), + noRestakingETHRecipient: PufferNoRestakingValidator(payable(_getPufferNoRestakingETHRecipient())) }) ); diff --git a/mainnet-contracts/script/DeployerHelper.s.sol b/mainnet-contracts/script/DeployerHelper.s.sol index 50f5d62f..0ce8c0b6 100644 --- a/mainnet-contracts/script/DeployerHelper.s.sol +++ b/mainnet-contracts/script/DeployerHelper.s.sol @@ -41,6 +41,27 @@ abstract contract DeployerHelper is Script { revert("PufferDeployer not available for this chain"); } + function _getPufferNoRestakingETHRecipient() internal view returns (address) { + if (block.chainid == mainnet) { + //@todo update + return address(55); + } + + revert("PufferNoRestakingETHRecipient not available for this chain"); + } + + function _getWithdrawalRequestPredeploy() internal view returns (address) { + if (block.chainid == mainnet) { + // https://etherscan.io/address/0x00000961Ef480Eb55e80D19ad83579A64c007002 + return 0x00000961Ef480Eb55e80D19ad83579A64c007002; + } else if (block.chainid == holesky) { + // https://holesky.etherscan.io/address/0x00000961Ef480Eb55e80D19ad83579A64c007002 + return 0x00000961Ef480Eb55e80D19ad83579A64c007002; + } + + revert("WithdrawalRequestPredeploy not available for this chain"); + } + function _getDAO() internal view returns (address) { // ATM Ops multisig is the DAO return _getOPSMultisig(); diff --git a/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index d333d3f7..006697fe 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/script/UpgradePufETH.s.sol b/mainnet-contracts/script/UpgradePufETH.s.sol index f07ddbcd..a2c2cc6a 100644 --- a/mainnet-contracts/script/UpgradePufETH.s.sol +++ b/mainnet-contracts/script/UpgradePufETH.s.sol @@ -54,7 +54,7 @@ contract UpgradePufETH is BaseScript { function run( PufferDeployment memory deployment, - BridgingDeployment memory bridgingDeployment, + BridgingDeployment memory, address pufferOracle, address revenueDepositor ) public broadcast { diff --git a/mainnet-contracts/src/Errors.sol b/mainnet-contracts/src/Errors.sol index c7fc0c7a..182cf621 100644 --- a/mainnet-contracts/src/Errors.sol +++ b/mainnet-contracts/src/Errors.sol @@ -24,3 +24,9 @@ error InvalidAmount(); * @dev Signature "0x90b8ec18" */ error TransferFailed(); + +/** + * @notice Thrown when the input is invalid + * @dev Signature "0xb4fa3fb3" + */ +error InvalidInput(); diff --git a/mainnet-contracts/src/PufferNoRestakingValidator.sol b/mainnet-contracts/src/PufferNoRestakingValidator.sol new file mode 100644 index 00000000..5db66047 --- /dev/null +++ b/mainnet-contracts/src/PufferNoRestakingValidator.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import { InvalidAddress, Unauthorized, InvalidInput } from "./Errors.sol"; +import { IBeaconDepositContract } from "./interface/IBeaconDepositContract.sol"; +import { PufferProtocol } from "./PufferProtocol.sol"; +import { AccessManaged } from "@openzeppelin/contracts/access/manager/AccessManaged.sol"; +import { IPufferNoRestakingValidator } from "./interface/IPufferNoRestakingValidator.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title PufferNoRestakingValidator + * @author Puffer Finance + * @custom:security-contact security@puffer.fi + */ +contract PufferNoRestakingValidator is IPufferNoRestakingValidator, AccessManaged { + using Address for address payable; + + address public immutable BEACON_DEPOSIT_CONTRACT; + address public immutable WITHDRAWAL_REQUEST_PREDEPLOY; + + PufferProtocol public immutable PUFFER_PROTOCOL; + + /** + * The amount of ETH deposited to the Beacon Chain through this contract + */ + uint256 public depositedToBeaconChain; + + /** + * The amount of ETH queued for withdrawal from the Beacon Chain through this contract + */ + uint256 public queuedWithdrawals; + + constructor( + address protocol, + address accessManager, + address beaconDepositContract, + address withdrawalRequestPredeploy + ) AccessManaged(accessManager) { + require(protocol != address(0), InvalidAddress()); + require(beaconDepositContract != address(0), InvalidAddress()); + require(withdrawalRequestPredeploy != address(0), InvalidAddress()); + PUFFER_PROTOCOL = PufferProtocol(payable(protocol)); + BEACON_DEPOSIT_CONTRACT = beaconDepositContract; + WITHDRAWAL_REQUEST_PREDEPLOY = withdrawalRequestPredeploy; + } + + modifier onlyPufferProtocol() { + require(msg.sender == address(PUFFER_PROTOCOL), Unauthorized()); + _; + } + + /** + * @notice Receive function to allow the contract to receive ETH + */ + receive() external payable { } + + /** + * @notice Start non restaking compounding validator + * @param pubKey The public key of the validator + * @param signature The signature of the validator + * @param depositDataRoot The deposit data root of the validator + */ + function startNonRestakingValidators(bytes calldata pubKey, bytes calldata signature, bytes32 depositDataRoot) + external + payable + onlyPufferProtocol + { + IBeaconDepositContract(BEACON_DEPOSIT_CONTRACT).deposit{ value: msg.value }({ + pubkey: pubKey, + withdrawal_credentials: getWithdrawalCredentialsCompounding(), + signature: signature, + deposit_data_root: depositDataRoot + }); + + depositedToBeaconChain += msg.value; + + emit ValidatorDeposited(pubKey, msg.value); + } + + /** + * @notice Creates withdrawal requests for 0x02 validators + * @param pubkeys The public keys of the validators + * @param withdrawalAmounts The amounts of ETH to withdraw (in gwei) 0 = validator exit + * @dev Restricted to Puffer Paymaster + */ + function createWithdrawalRequest(bytes[] calldata pubkeys, uint256[] calldata withdrawalAmounts) + external + payable + restricted + { + require(pubkeys.length == withdrawalAmounts.length, InvalidInput()); + require(pubkeys.length > 0, InvalidInput()); + + // Ensure withdrawalAmount is in gwei (1 gwei = 10^9 wei) + for (uint256 i = 0; i < withdrawalAmounts.length; i++) { + if (withdrawalAmounts[i] % 1 gwei != 0) { + revert WithdrawalAmountNotInGwei(); + } + } + + // Read the withdrawal fee from the withdrawal request predeploy + (bool feeReadOk, bytes memory feeData) = WITHDRAWAL_REQUEST_PREDEPLOY.staticcall(""); + require(feeReadOk, FailedToReadWithdrawalFee()); + + // Create the withdrawal request + for (uint256 i = 0; i < pubkeys.length; i++) { + (bool withdrawalRequestOk,) = WITHDRAWAL_REQUEST_PREDEPLOY.call{ + value: uint256(bytes32(feeData)) * pubkeys.length + }(abi.encodePacked(pubkeys[i], withdrawalAmounts[i])); + require(withdrawalRequestOk, FailedToCreateWithdrawalRequest()); + // Convert from gwei to wei (1 gwei = 10^9 wei) + queuedWithdrawals += withdrawalAmounts[i] * 1 gwei; + + emit WithdrawalRequested(pubkeys[i], withdrawalAmounts[i]); + } + } + + /** + * @notice Exit the validators and send the ETH to the Puffer Vault + * @param amount The amount of ETH to return to the Puffer Vault + */ + function exitValidators(uint256 amount) external onlyPufferProtocol { + queuedWithdrawals -= amount; + payable(PUFFER_PROTOCOL.PUFFER_VAULT()).sendValue(amount); + } + + /** + * @notice Transfer ETH to an address + * This function is used to transfer the earned rewards from this contract + * restricted to Puffer Team + * @param to The address to transfer the ETH to + * @param amount The amount of ETH to transfer + */ + function transferETH(address payable to, uint256 amount) external restricted { + // Only allow transfers of the Rewards to `to` address + require(amount > (address(this).balance - queuedWithdrawals), InvalidAmount()); + payable(to).sendValue(amount); + } + + /** + * @notice Get the withdrawal credentials for the compounding validators (this contract address) + * @return The withdrawal credentials + */ + function getWithdrawalCredentialsCompounding() public view returns (bytes memory) { + return abi.encodePacked(bytes1(uint8(2)), bytes11(0), address(this)); + } +} diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 34abea19..3efc3e97 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0 <0.9.0; +import { PufferProtocolStorage } from "./PufferProtocolStorage.sol"; 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 { IPufferModuleManager } from "./interface/IPufferModuleManager.sol"; import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; import { IGuardianModule } from "./interface/IGuardianModule.sol"; @@ -22,6 +22,7 @@ import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { PufferVaultV2 } from "./PufferVaultV2.sol"; import { ValidatorTicket } from "./ValidatorTicket.sol"; import { InvalidAddress } from "./Errors.sol"; +import { PufferNoRestakingValidator } from "./PufferNoRestakingValidator.sol"; import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; /** @@ -55,50 +56,18 @@ 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 - */ - 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; - /** - * @dev Default "PUFFER_MODULE_0" module - */ - bytes32 internal constant _PUFFER_MODULE_0 = bytes32("PUFFER_MODULE_0"); - - /** - * @inheritdoc IPufferProtocol - */ IGuardianModule public immutable override GUARDIAN_MODULE; - - /** - * @inheritdoc IPufferProtocol - */ ValidatorTicket public immutable override VALIDATOR_TICKET; - - /** - * @inheritdoc IPufferProtocol - */ PufferVaultV2 public immutable override PUFFER_VAULT; - - /** - * @inheritdoc IPufferProtocol - */ IPufferModuleManager public immutable override PUFFER_MODULE_MANAGER; - - /** - * @inheritdoc IPufferProtocol - */ IPufferOracleV2 public immutable override PUFFER_ORACLE; - - /** - * @inheritdoc IPufferProtocol - */ IBeaconDepositContract public immutable override BEACON_DEPOSIT_CONTRACT; + PufferNoRestakingValidator public immutable PUFFER_NO_RESTAKING_RECIPIENT; constructor( PufferVaultV2 pufferVault, @@ -106,29 +75,30 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad address moduleManager, ValidatorTicket validatorTicket, IPufferOracleV2 oracle, - address beaconDepositContract + address beaconDepositContract, + PufferNoRestakingValidator noRestakingETHRecipient ) { + require(address(pufferVault) != address(0), InvalidAddress()); + require(address(guardianModule) != address(0), InvalidAddress()); + require(address(moduleManager) != address(0), InvalidAddress()); + require(address(validatorTicket) != address(0), InvalidAddress()); + require(address(oracle) != address(0), InvalidAddress()); + require(address(beaconDepositContract) != address(0), InvalidAddress()); + require(address(noRestakingETHRecipient) != address(0), InvalidAddress()); GUARDIAN_MODULE = guardianModule; PUFFER_VAULT = PufferVaultV2(payable(address(pufferVault))); PUFFER_MODULE_MANAGER = IPufferModuleManager(moduleManager); VALIDATOR_TICKET = validatorTicket; PUFFER_ORACLE = oracle; BEACON_DEPOSIT_CONTRACT = IBeaconDepositContract(beaconDepositContract); + PUFFER_NO_RESTAKING_RECIPIENT = noRestakingETHRecipient; _disableInitializers(); } /** - * @notice Initializes the contract + * @notice Receive function to allow the contract to receive ETH */ - function initialize(address accessManager) external initializer { - if (address(accessManager) == address(0)) { - revert InvalidAddress(); - } - __AccessManaged_init(accessManager); - _createPufferModule(_PUFFER_MODULE_0); - _changeMinimumVTAmount(28 ether); // 28 Validator Tickets - _setVTPenalty(10 ether); // 10 Validator Tickets - } + receive() external payable { } /** * @inheritdoc IPufferProtocol @@ -198,7 +168,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); - uint256 validatorBondInETH = data.raveEvidence.length > 0 ? _ENCLAVE_VALIDATOR_BOND : _NO_ENCLAVE_VALIDATOR_BOND; + uint256 validatorBondInETH = _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) { @@ -288,6 +258,98 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad $.validators[moduleName][index].status = Status.ACTIVE; } + /** + * @notice Start non restaking Validator by directly depositing into the Beacon Deposit Contract + * @dev 1 Validator = 32 ETH = 1 VT per day + * If we run a validator that has 1024 ETH Balance, that Validator is consuming 32 VT per day, and we need to purchase 160 VT + * for that validator and keep purchasing and redepositing the VT. + * + * msg.sender is the Node Operator in this case. + * msg.sender is a trusted account. + * restricted to Puffer + * + * @param pubKey The public key of the validator + * @param signature The signature of the validator + * @param numberOfValidators The number of validators to start + * @param depositDataRoot The deposit data root of the validator + */ + function startNonRestakingValidator( + bytes calldata pubKey, + bytes calldata signature, + uint256 numberOfValidators, + bytes32 depositDataRoot + ) external payable restricted { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + // slither-disable-next-line unchecked-transfer + uint256 receivedVtAmount = VALIDATOR_TICKET.purchaseValidatorTicket{ value: msg.value }(address(this)); + + // We are required to deposit minimum 5 VT per validator + // If we are provisioning one validator with 5 * 32 ETH balance, we need to deposit 25 VT minimum + // That validator consumes 5 VT per day + // This 5 VT buffer is low because we are running the validators ourself, and they are the first ones to be exited in case of liquidity requirements + if (receivedVtAmount < (numberOfValidators * 5)) { + revert InvalidVTAmount(); + } + + // Pending validator count is always 0 for Puffer + $.nodeOperatorInfo[msg.sender].activeValidatorCount += SafeCast.toUint64(numberOfValidators); + $.nodeOperatorInfo[msg.sender].vtBalance += SafeCast.toUint96(receivedVtAmount); + + PUFFER_NO_RESTAKING_RECIPIENT.startNonRestakingValidators{ value: 32 ether * numberOfValidators }( + pubKey, signature, depositDataRoot + ); + + // This is meant to provision 0x02 validators. Those validators have a compounding balance. + // It is possible to deposit batches of 32 ETH for the validator. + for (uint256 i = 0; i < numberOfValidators; i++) { + PUFFER_ORACLE.provisionNode(); + } + + emit StartedNonRestakingValidator(pubKey, numberOfValidators); + } + + /** + * @notice Return non restaked ETH from the PufferNoRestakingValidator to the PufferVault + * @param withdrawalAmount The amount of ETH to withdraw + * @param startEpoch The start epoch of the validator + * @param endEpoch The end epoch of the validator + * @param pufferNodeOperator The address of the Node Operator + * restricted to Puffer Paymaster + */ + function returnNonRestakedETH( + uint256 withdrawalAmount, + uint256 startEpoch, + uint256 endEpoch, + address pufferNodeOperator + ) external restricted { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + uint256 vtBurnAmount = _getVTBurnAmount({ + node: pufferNodeOperator, + withdrawalAmount: withdrawalAmount, + startEpoch: startEpoch, + endEpoch: endEpoch + }); + + // Decrease the active validator count for the Node Operator + --$.nodeOperatorInfo[pufferNodeOperator].activeValidatorCount; + // Decrease the VT balance for the Node Operator + $.nodeOperatorInfo[pufferNodeOperator].vtBalance -= SafeCast.toUint96(vtBurnAmount); + + // Burn the VT from the Node Operator + VALIDATOR_TICKET.burn(vtBurnAmount); + + // Calculate the number of validators that are being exited + uint256 validatorCount = withdrawalAmount / 32 ether; + + // Return ETH to the PufferVault + PUFFER_NO_RESTAKING_RECIPIENT.exitValidators(validatorCount); + + // Update the Oracle + PUFFER_ORACLE.exitValidators(validatorCount); + } + /** * @inheritdoc IPufferProtocol * @dev Restricted to Puffer Paymaster @@ -323,7 +385,12 @@ 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({ + node: bondWithdrawals[i].node, + withdrawalAmount: validatorInfos[i].withdrawalAmount, + startEpoch: validatorInfos[i].startEpoch, + endEpoch: validatorInfos[i].endEpoch + }); // Update the burnAmounts burnAmounts.pufETH += burnAmount; @@ -626,7 +693,7 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad 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 validatorBond = _NO_ENCLAVE_VALIDATOR_BOND; uint256 ethAmount = validatorBond + ($.minimumVtAmount * PUFFER_ORACLE.getValidatorTicketPrice()) / 1 ether; return (pubKeys, withdrawalCredentials, threshold, ethAmount); @@ -807,15 +874,27 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad module.callStake({ pubKey: validatorPubKey, signature: validatorSignature, depositDataRoot: depositDataRoot }); } - function _getVTBurnAmount(ProtocolStorage storage $, address node, StoppedValidatorInfo calldata validatorInfo) + function _getVTBurnAmount(address node, uint256 withdrawalAmount, uint256 startEpoch, uint256 endEpoch) internal view returns (uint256) { - uint256 validatedEpochs = validatorInfo.endEpoch - validatorInfo.startEpoch; + ProtocolStorage storage $ = _getPufferProtocolStorage(); + + // Because of the rounding down, we get the validator size. + // We measure the size in 32 ETH chunks + // This is related to Pectra (2048 ETH Validators, 0x02 validator credentials) + uint256 validatorSize = withdrawalAmount / 32 ether; + + // If the validator size is 0, it rounded down, meaning 1 validator was active + if (validatorSize == 0) { + validatorSize = 1; + } + + 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 - uint256 vtBurnAmount = validatedEpochs * 4444444444444445; + // The formula is validatedEpochs * validatorSize * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up + uint256 vtBurnAmount = validatedEpochs * validatorSize * 4444444444444445; uint256 minimumVTAmount = $.minimumVtAmount; uint256 nodeVTBalance = $.nodeOperatorInfo[node].vtBalance; diff --git a/mainnet-contracts/src/interface/IPufferNoRestakingValidator.sol b/mainnet-contracts/src/interface/IPufferNoRestakingValidator.sol new file mode 100644 index 00000000..25798ac9 --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferNoRestakingValidator.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * @title IPufferNoRestakingValidator + * @author Puffer Finance + * @custom:security-contact security@puffer.fi + */ +interface IPufferNoRestakingValidator { + error WithdrawalAmountNotInGwei(); + error FailedToCreateWithdrawalRequest(); + error FailedToReadWithdrawalFee(); + error InvalidAmount(); + + event ValidatorDeposited(bytes pubKey, uint256 ethAmount); + event WithdrawalRequested(bytes pubKey, uint256 ethAmount); +} diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index b17a48f0..a767b251 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -177,6 +177,14 @@ interface IPufferProtocol { */ event SuccessfullyProvisioned(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName); + /** + * @notice Emitted when the non-restaking validator is started + * These validators are ran by Puffer. Their balance is not restaked and there is no bond for them. They act as a way to get fast liquidity for the protocol. + * @param pubKey is the validator public key + * @param numberOfValidators is the number of validators started + */ + event StartedNonRestakingValidator(bytes pubKey, uint256 numberOfValidators); + /** * @notice Returns validator information * @param moduleName is the staking Module diff --git a/mainnet-contracts/test/Integration/PufferVaultV2.fork.t.sol b/mainnet-contracts/test/Integration/PufferVaultV2.fork.t.sol index 4d945312..063cdbd3 100644 --- a/mainnet-contracts/test/Integration/PufferVaultV2.fork.t.sol +++ b/mainnet-contracts/test/Integration/PufferVaultV2.fork.t.sol @@ -380,7 +380,6 @@ contract PufferVaultV2ForkTest is MainnetForkTestHelper { pufferVault.deposit(100 ether + 1, alice); vm.expectRevert(); - pufferVault.depositETH{ value: type(uint256).max }(alice); vm.expectRevert(); pufferVault.depositStETH(100 ether + 1, alice); diff --git a/mainnet-contracts/test/fork-tests/ValidatorTicketMainnetTest.fork.t.sol b/mainnet-contracts/test/fork-tests/ValidatorTicketMainnetTest.fork.t.sol index 059aafbe..34469a66 100644 --- a/mainnet-contracts/test/fork-tests/ValidatorTicketMainnetTest.fork.t.sol +++ b/mainnet-contracts/test/fork-tests/ValidatorTicketMainnetTest.fork.t.sol @@ -10,7 +10,6 @@ import { PufferVaultV3 } from "../../src/PufferVaultV3.sol"; import { IPufferOracle } from "../../src/interface/IPufferOracle.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; -import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; contract ValidatorTicketMainnetTest is MainnetForkTestHelper { using Math for uint256; @@ -49,7 +48,7 @@ contract ValidatorTicketMainnetTest is MainnetForkTestHelper { vm.stopPrank(); } - function test_initial_state() public { + function test_initial_state() public view { assertEq(validatorTicket.name(), "Puffer Validator Ticket"); assertEq(validatorTicket.symbol(), "VT"); assertEq(validatorTicket.getProtocolFeeRate(), INITIAL_PROTOCOL_FEE); @@ -184,7 +183,7 @@ contract ValidatorTicketMainnetTest is MainnetForkTestHelper { uint256 initialTreasuryBalance, uint256 initialGuardianBalance, uint256 initialVaultBalance - ) internal { + ) internal view { address treasury = validatorTicket.TREASURY(); address guardianModule = validatorTicket.GUARDIAN_MODULE(); address vault = validatorTicket.PUFFER_VAULT(); diff --git a/mainnet-contracts/test/mocks/MockWithdrawalRequestPredeploy.sol b/mainnet-contracts/test/mocks/MockWithdrawalRequestPredeploy.sol new file mode 100644 index 00000000..824a6610 --- /dev/null +++ b/mainnet-contracts/test/mocks/MockWithdrawalRequestPredeploy.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract MockWithdrawalRequestPredeploy { + event MockWithdrawalRequestPredeployFallback(uint256 amount); + + fallback() external payable { + emit MockWithdrawalRequestPredeployFallback(msg.value); + } +} diff --git a/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol b/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol index 735f9287..dff61100 100644 --- a/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol +++ b/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol @@ -6,20 +6,23 @@ import { GuardianModule } from "../../src/GuardianModule.sol"; import { PufferVaultV2 } from "../../src/PufferVaultV2.sol"; import { ValidatorTicket } from "../../src/ValidatorTicket.sol"; import { IPufferOracleV2 } from "../../src/interface/IPufferOracleV2.sol"; +import { PufferNoRestakingValidator } from "../../src/PufferNoRestakingValidator.sol"; contract PufferProtocolMockUpgrade is PufferProtocol { function returnSomething() external pure returns (uint256) { return 1337; } + // Addresses can't be 0, you get weird compilation error constructor(address beacon) PufferProtocol( - PufferVaultV2(payable(address(0))), - GuardianModule(payable(address(0))), - address(0), - ValidatorTicket(address(0)), - IPufferOracleV2(address(0)), - address(0) + PufferVaultV2(payable(address(1))), + GuardianModule(payable(address(1))), + address(1), + ValidatorTicket(address(1)), + IPufferOracleV2(address(1)), + address(1), + PufferNoRestakingValidator(payable(address(1))) ) { } } diff --git a/mainnet-contracts/test/unit/ValidatorTicket.t.sol b/mainnet-contracts/test/unit/ValidatorTicket.t.sol index 4f9447e7..66e9274b 100644 --- a/mainnet-contracts/test/unit/ValidatorTicket.t.sol +++ b/mainnet-contracts/test/unit/ValidatorTicket.t.sol @@ -8,18 +8,15 @@ import { ValidatorTicket } from "../../src/ValidatorTicket.sol"; import { IValidatorTicket } from "../../src/interface/IValidatorTicket.sol"; import { PufferOracle } from "../../src/PufferOracle.sol"; import { PufferOracleV2 } from "../../src/PufferOracleV2.sol"; -import { IPufferVault } from "../../src/interface/IPufferVault.sol"; import { PufferVaultV2 } from "../../src/PufferVaultV2.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import { PUBLIC_ROLE, ROLE_ID_PUFETH_BURNER, ROLE_ID_VAULT_WITHDRAWER } from "../../script/Roles.sol"; +import { PUBLIC_ROLE, ROLE_ID_PUFETH_BURNER } from "../../script/Roles.sol"; import { Permit } from "../../src/structs/Permit.sol"; import "forge-std/console.sol"; import { Math } from "@openzeppelin/contracts/utils/math/Math.sol"; + /** * @dev This test is for the ValidatorTicket smart contract with `src/PufferOracle.sol` */ - contract ValidatorTicketTest is UnitTestHelper { using ECDSA for bytes32; using Address for address; @@ -243,7 +240,7 @@ contract ValidatorTicketTest is UnitTestHelper { deal(address(pufferVault), recipient, pufEthAmount); } - function _signPermit(bytes32 structHash, bytes32 domainSeparator) internal view returns (Permit memory permit) { + function _signPermit() internal view returns (Permit memory permit) { // TODO: Implement signing logic here permit = Permit({ amount: 10 ether, deadline: block.timestamp + 1 hours, v: 27, r: bytes32(0), s: bytes32(0) }); } From fcf3851ae11706615cf2a53714116c2fd50007e1 Mon Sep 17 00:00:00 2001 From: bxmmm1 <28648109+bxmmm1@users.noreply.github.com> Date: Wed, 19 Mar 2025 16:38:05 +0000 Subject: [PATCH 2/2] forge fmt --- mainnet-contracts/src/PufferProtocol.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 3efc3e97..5f1ada85 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -261,13 +261,13 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad /** * @notice Start non restaking Validator by directly depositing into the Beacon Deposit Contract * @dev 1 Validator = 32 ETH = 1 VT per day - * If we run a validator that has 1024 ETH Balance, that Validator is consuming 32 VT per day, and we need to purchase 160 VT + * If we run a validator that has 1024 ETH Balance, that Validator is consuming 32 VT per day, and we need to purchase 160 VT * for that validator and keep purchasing and redepositing the VT. - * + * * msg.sender is the Node Operator in this case. * msg.sender is a trusted account. * restricted to Puffer - * + * * @param pubKey The public key of the validator * @param signature The signature of the validator * @param numberOfValidators The number of validators to start