diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index c534db05..e02f9c9e 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,21 @@ contract DeployPuffer is BaseScript { address(moduleManager), abi.encodeCall(moduleManager.initialize, (address(accessManager))) ); + 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({ accessManager: address(accessManager) }); + pufferProtocol.initialize({ + accessManager: address(accessManager), + pufferProtocolLogic: address(pufferProtocolLogic) + }); vm.label(address(accessManager), "AccessManager"); vm.label(address(operationsCoordinator), "OperationsCoordinator"); @@ -216,8 +230,9 @@ 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) + }); } function getStakingContract() internal returns (address) { 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/script/GenerateBLSKeysAndRegisterValidators.s.sol b/mainnet-contracts/script/GenerateBLSKeysAndRegisterValidators.s.sol index 0d11cb53..1cbd59bf 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,9 @@ 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/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/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); - } - } } diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index 608e2bf0..45a51c1a 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -5,28 +5,22 @@ 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 { 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"; 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 { ProtocolSignatureNonces } from "./ProtocolSignatureNonces.sol"; -import { EpochsValidatedSignature } from "./struct/Signatures.sol"; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { PufferProtocolBase } from "./PufferProtocolBase.sol"; /** * @title PufferProtocol @@ -35,117 +29,8 @@ import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ -contract PufferProtocol is - IPufferProtocol, - AccessManagedUpgradeable, - UUPSUpgradeable, - PufferProtocolStorage, - ProtocolSignatureNonces -{ - /** - * @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; - } - - /** - * @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 - */ - 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; +contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolBase { + using MessageHashUtils for bytes32; constructor( PufferVaultV5 pufferVault, @@ -155,30 +40,54 @@ 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; + ) + PufferProtocolBase( + pufferVault, + guardianModule, + moduleManager, + validatorTicket, + oracle, + beaconDepositContract, + pufferRevenueDistributor + ) + { _disableInitializers(); } receive() external payable { } /** - * @notice Initializes the contract + * @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. + * @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. */ - function initialize(address accessManager) external initializer { - if (address(accessManager) == address(0)) { - revert InvalidAddress(); + fallback() external payable restricted { + (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 + */ + function initialize(address accessManager, address pufferProtocolLogic) external initializer { + 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 _setVTPenalty(10 * _EPOCHS_PER_DAY); // 10 days worth of ETH is the VT penalty + _setPufferProtocolLogic(pufferProtocolLogic); } /** @@ -186,58 +95,15 @@ contract PufferProtocol is * @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)) { - 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); + function depositValidatorTickets(address node, uint256 amount) external restricted { + require(node != address(0), InvalidAddress()); // 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); - } - - /** - * @inheritdoc IPufferProtocol - * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol - */ - function depositValidationTime(EpochsValidatedSignature memory epochsValidatedSignature) - external - 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 }); + $.nodeOperatorInfo[node].deprecated_vtBalance += SafeCast.toUint96(amount); + emit ValidatorTicketsDeposited(node, msg.sender, amount); } /** @@ -250,140 +116,28 @@ contract PufferProtocol is // 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 $.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); } - /** - * @inheritdoc IPufferProtocol - * @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); - } - - /** - * @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 { - 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 - }); - } - /** * @inheritdoc IPufferProtocol * @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(); @@ -419,253 +173,59 @@ contract PufferProtocol is * @inheritdoc IPufferProtocol * @dev Restricted to Node Operators */ - function requestConsolidation(bytes32 moduleName, uint256[] calldata srcIndices, uint256[] calldata targetIndices) - external - payable - restricted - { - 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); + function requestWithdrawal( + bytes32 moduleName, + uint256[] calldata indices, + uint64[] calldata gweiAmounts, + WithdrawalType[] calldata withdrawalType, + bytes[][] calldata validatorAmountsSignatures, + uint256 deadline + ) external payable restricted validDeadline(deadline) { + // Using internal function to avoid stack too deep + bytes[] memory pubkeys = _processWithdrawalValidation( + moduleName, indices, gweiAmounts, withdrawalType, validatorAmountsSignatures, deadline + ); - emit ConsolidationRequested(moduleName, srcPubkeys, targetPubkeys); + _PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); } - /** - * @inheritdoc IPufferProtocol - * @dev Restricted to Node Operators - */ - function requestWithdrawal( + function _processWithdrawalValidation( bytes32 moduleName, uint256[] calldata indices, uint64[] calldata gweiAmounts, WithdrawalType[] calldata withdrawalType, bytes[][] calldata validatorAmountsSignatures, uint256 deadline - ) external payable restricted { - if (block.timestamp > deadline) { - revert DeadlineExceeded(); - } - + ) internal returns (bytes[] memory pubkeys) { ProtocolStorage storage $ = _getPufferProtocolStorage(); + pubkeys = new bytes[](indices.length); - bytes[] memory 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], - _useNonce(_FUNCTION_SELECTOR_REQUEST_WITHDRAWAL, msg.sender), + gweiAmount, + _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), deadline ) - ); - - GUARDIAN_MODULE.validateWithdrawalRequest({ - eoaSignatures: validatorAmountsSignatures[i], - messageHash: messageHash - }); - } - } - - 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 - */ - function batchHandleWithdrawals( - StoppedValidatorInfo[] calldata validatorInfos, - 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); + ).toEthSignedMessageHash(); + _validateSignatures(messageHash, validatorAmountsSignatures[i]); } - - // 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); - } - - /** - * @inheritdoc IPufferProtocol - * @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); } /** @@ -703,6 +263,13 @@ contract PufferProtocol is _setVTPenalty(newPenaltyAmount); } + /** + * @dev Restricted to the DAO + */ + function setPufferProtocolLogic(address newPufferProtocolLogic) external restricted { + _setPufferProtocolLogic(newPufferProtocolLogic); + } + /** * @inheritdoc IPufferProtocol */ @@ -873,53 +440,16 @@ 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) { - 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; } @@ -932,10 +462,8 @@ contract PufferProtocol is function _createPufferModule(bytes32 moduleName) internal returns (address) { ProtocolStorage storage $ = _getPufferProtocolStorage(); - if (address($.modules[moduleName]) != address(0)) { - revert ModuleAlreadyExists(); - } - PufferModule module = PUFFER_MODULE_MANAGER.createNewPufferModule(moduleName); + require(address($.modules[moduleName]) == address(0), ModuleAlreadyExists()); + PufferModule module = _PUFFER_MODULE_MANAGER.createNewPufferModule(moduleName); $.modules[moduleName] = module; $.moduleWeights.push(moduleName); bytes32 withdrawalCredentials = bytes32(module.getWithdrawalCredentials()); @@ -944,24 +472,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) { @@ -971,29 +481,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, @@ -1011,255 +498,57 @@ 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 ); } - /** - * @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) - { - 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; - } - - // 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 - }); + function _setPufferProtocolLogic(address newPufferProtocolLogic) internal { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + emit PufferProtocolLogicSet($.pufferProtocolLogic, newPufferProtocolLogic); + $.pufferProtocolLogic = newPufferProtocolLogic; } - /** - * @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 { - 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, - epochsValidatedSignature.totalEpochsValidated, - _useNonce(epochsValidatedSignature.functionSelector, node), - epochsValidatedSignature.deadline - ) - ); - - GUARDIAN_MODULE.validateTotalEpochsValidated({ - eoaSignatures: epochsValidatedSignature.signatures, - messageHash: messageHash - }); - - 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 }(""); + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } - // Transfer WETH to the Revenue Distributor, it will be slow released to the PufferVault - ERC20(weth).transfer(PUFFER_REVENUE_DISTRIBUTOR, validationTimeToConsume); + function getPufferProtocolLogic() external view override returns (address) { + return _getPufferProtocolStorage().pufferProtocolLogic; } - /** - * @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; + function GUARDIAN_MODULE() external view override returns (IGuardianModule) { + return _GUARDIAN_MODULE; } - 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 VALIDATOR_TICKET() external view override returns (ValidatorTicket) { + return _VALIDATOR_TICKET; } - function _decreaseNumberOfRegisteredValidators(ProtocolStorage storage $, bytes32 moduleName) internal { - --$.moduleLimits[moduleName].numberOfRegisteredValidators; - emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); + function PUFFER_VAULT() external view override returns (PufferVaultV5) { + return _PUFFER_VAULT; } - 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 PUFFER_MODULE_MANAGER() external view override returns (PufferModuleManager) { + return _PUFFER_MODULE_MANAGER; } - 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; + function PUFFER_ORACLE() external view override returns (IPufferOracleV2) { + return _PUFFER_ORACLE; + } - delete $.validators[validatorInfo.moduleName][ - validatorInfo.pufferModuleIndex - ]; - // nosemgrep basic-arithmetic-underflow - return (bondBurnAmount, bondAmount - bondBurnAmount, numBatches); + function BEACON_DEPOSIT_CONTRACT() external view override returns (IBeaconDepositContract) { + return _BEACON_DEPOSIT_CONTRACT; } - function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } + function PUFFER_REVENUE_DISTRIBUTOR() external view override returns (address payable) { + return _PUFFER_REVENUE_DISTRIBUTOR; + } } diff --git a/mainnet-contracts/src/PufferProtocolBase.sol b/mainnet-contracts/src/PufferProtocolBase.sol new file mode 100644 index 00000000..40a3d8fe --- /dev/null +++ b/mainnet-contracts/src/PufferProtocolBase.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: GPL-3.0 +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"; +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"; +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 + */ + 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; + + 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; + + modifier validDeadline(uint256 deadline) { + require(block.timestamp <= deadline, DeadlineExceeded()); + _; + } + + 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; + } + + 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 new file mode 100644 index 00000000..b56669bd --- /dev/null +++ b/mainnet-contracts/src/PufferProtocolLogic.sol @@ -0,0 +1,659 @@ +// 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 { 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"; +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 { PufferModule } from "./PufferModule.sol"; +import { IPufferOracleV2 } from "./interface/IPufferOracleV2.sol"; +import { IGuardianModule } from "./interface/IGuardianModule.sol"; +import { ValidatorTicket } from "./ValidatorTicket.sol"; +import { PufferVaultV5 } from "./PufferVaultV5.sol"; +import { EpochsValidatedSignature } from "./struct/Signatures.sol"; +import { InvalidAddress, 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 + */ +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 + */ + 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, + address moduleManager, + ValidatorTicket validatorTicket, + IPufferOracleV2 oracle, + address beaconDepositContract, + address payable pufferRevenueDistributor + ) + PufferProtocolBase( + pufferVault, + guardianModule, + moduleManager, + validatorTicket, + oracle, + beaconDepositContract, + pufferRevenueDistributor + ) + { } + + /** + * @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 + payable + override + validDeadline(epochsValidatedSignature.deadline) + { + 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 = IPufferProtocolLogic.depositValidationTime.selector; + + uint256 burnAmount = _useVTOrValidationTime($, epochsValidatedSignature); + + if (burnAmount > 0) { + _VALIDATOR_TICKET.burn(burnAmount); + } + + $.nodeOperatorInfo[epochsValidatedSignature.nodeOperator].validationTime += SafeCast.toUint96(msg.value); + emit ValidationTimeDeposited({ node: epochsValidatedSignature.nodeOperator, ethAmount: msg.value }); + } + + /** + * @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()); + require(amount > 0, InvalidAmount()); + 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. + require( + $.nodeOperatorInfo[msg.sender].activeValidatorCount + $.nodeOperatorInfo[msg.sender].pendingValidatorCount + == 0, + 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); + } + + /** + * @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, + bytes32 moduleName, + uint256 totalEpochsValidated, + bytes[] calldata vtConsumptionSignature, + uint256 deadline + ) external payable override validDeadline(deadline) { + 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) + require( + msg.value >= bondAmountEth + (numBatches * _MINIMUM_EPOCHS_VALIDATION_REGISTRATION * epochCurrentPrice), + InvalidETHAmount() + ); + + emit ValidationTimeDeposited({ node: msg.sender, ethAmount: (msg.value - bondAmountEth) }); + + _settleVTAccounting({ + $: $, + epochsValidatedSignature: EpochsValidatedSignature({ + nodeOperator: msg.sender, + totalEpochsValidated: totalEpochsValidated, + functionSelector: IPufferProtocolLogic.registerValidatorKey.selector, + 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 + }); + } + + /** + * @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) + external + payable + override + { + require(srcIndices.length > 0, InputArrayLengthZero()); + require(srcIndices.length == targetIndices.length, 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); + } + + /** + * @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(); + + 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); + } + + /** + * @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, + bytes[] calldata guardianEOASignatures, + uint256 deadline + ) external payable override validDeadline(deadline) { + bytes32 messageHash = keccak256(abi.encode(validatorInfos, deadline)).toEthSignedMessageHash(); + _validateSignatures(messageHash, guardianEOASignatures); + + 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]; + + require(validator.status == Status.ACTIVE, 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: IPufferProtocolLogic.batchHandleWithdrawals.selector, + 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); + } + + /** + * @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( + 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) { + return; + } + + bytes32 messageHash = keccak256( + abi.encode( + node, + epochsValidatedSignature.totalEpochsValidated, + _useNonce(epochsValidatedSignature.functionSelector, node), + epochsValidatedSignature.deadline + ) + ).toEthSignedMessageHash(); + + _validateSignatures(messageHash, 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); + } + + /** + * @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 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) + * @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; + } + + 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, ""); + require(success, 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 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 _decreaseNumberOfRegisteredValidators(ProtocolStorage storage $, bytes32 moduleName) internal { + --$.moduleLimits[moduleName].numberOfRegisteredValidators; + emit 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/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index 918dbab1..3b20287f 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -23,247 +23,6 @@ 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 - * @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 Returns validator information * @param moduleName is the staking Module @@ -291,26 +50,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; - - /** - * @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; + function depositValidatorTickets(address node, uint256 vtAmount) external; /** * @notice Withdraws the `amount` of Validator Tickers from the `msg.sender` to the `recipient` @@ -319,19 +59,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. @@ -361,32 +88,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 */ @@ -477,23 +178,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` */ @@ -529,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 new file mode 100644 index 00000000..b52ddb86 --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferProtocolEvents.sol @@ -0,0 +1,161 @@ +// 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 + * @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..6a59b7a4 --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferProtocolFull.sol @@ -0,0 +1,31 @@ +// 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"; +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, + IPufferProtocolEvents, + 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 new file mode 100644 index 00000000..548f9bcb --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferProtocolLogic.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0 +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"; + +/** + * @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`). + * 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; + + /** + * @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; + + /** + * @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( + ValidatorKeyData calldata data, + bytes32 moduleName, + uint256 totalEpochsValidated, + bytes[] calldata vtConsumptionSignature, + uint256 deadline + ) external payable; + + /** + * @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) + external + payable; + + /** + * @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; + + /** + * @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( + StoppedValidatorInfo[] calldata validatorInfos, + bytes[] calldata guardianEOASignatures, + uint256 deadline + ) external payable; +} diff --git a/mainnet-contracts/src/interface/IPufferProtocolManagement.sol b/mainnet-contracts/src/interface/IPufferProtocolManagement.sol new file mode 100644 index 00000000..a85a9dba --- /dev/null +++ b/mainnet-contracts/src/interface/IPufferProtocolManagement.sol @@ -0,0 +1,34 @@ +// 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 + */ + 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/src/struct/ProtocolStorage.sol b/mainnet-contracts/src/struct/ProtocolStorage.sol index 97815ca8..a1671f09 100644 --- a/mainnet-contracts/src/struct/ProtocolStorage.sol +++ b/mainnet-contracts/src/struct/ProtocolStorage.sol @@ -66,6 +66,11 @@ struct ProtocolStorage { * Slot 9 */ uint256 vtPenaltyEpochs; + /** + * @dev Address of the PufferProtocolLogic contract + * Slot 10 + */ + address pufferProtocolLogic; } struct ModuleLimit { diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index 10cab22d..49184ae1 100644 --- a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol +++ b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol @@ -1,7 +1,8 @@ // 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 { 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); @@ -582,7 +583,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/helpers/UnitTestHelper.sol b/mainnet-contracts/test/helpers/UnitTestHelper.sol index cc8ed660..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; @@ -110,7 +111,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"); @@ -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); @@ -220,7 +221,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/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 92d823cc..3e88c036 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -4,11 +4,16 @@ 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"; +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"; import { PufferProtocol } from "../../src/PufferProtocol.sol"; +import { PufferProtocolBase } from "../../src/PufferProtocolBase.sol"; import { PufferModule } from "../../src/PufferModule.sol"; import { PufferRevenueDepositor } from "../../src/PufferRevenueDepositor.sol"; import { @@ -19,14 +24,15 @@ 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"; 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 @@ -51,8 +57,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 % @@ -119,6 +123,28 @@ 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); @@ -158,7 +184,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); @@ -177,7 +203,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"); @@ -186,7 +212,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(PufferProtocolBase.ModuleAlreadyExists.selector); pufferProtocol.createPufferModule(PUFFER_MODULE_0); } @@ -195,7 +221,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(PufferProtocolBase.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( validatorKeyData, bytes32("imaginary module"), 0, new bytes[](0), block.timestamp + 1 days ); @@ -246,14 +272,14 @@ contract PufferProtocolTest is UnitTestHelper { numBatches: 0 }); - vm.expectRevert(IPufferProtocol.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(IPufferProtocol.InvalidNumberOfBatches.selector); + vm.expectRevert(PufferProtocolBase.InvalidNumberOfBatches.selector); pufferProtocol.registerValidatorKey{ value: vtPrice }( validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -277,7 +303,7 @@ contract PufferProtocolTest is UnitTestHelper { numBatches: 1 }); - vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); + vm.expectRevert(PufferProtocolBase.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -298,7 +324,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory validatorSignature = _validatorSignature(); - vm.expectRevert(IPufferProtocol.InvalidDepositRootHash.selector); + vm.expectRevert(PufferProtocolBase.InvalidDepositRootHash.selector); pufferProtocol.provisionNode(validatorSignature, bytes32("badDepositRoot")); // "depositRoot" is hardcoded in the mock // now it works @@ -332,21 +358,21 @@ 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"); // 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); @@ -355,7 +381,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 @@ -384,7 +410,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); @@ -404,7 +430,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(); @@ -414,7 +440,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(); @@ -454,7 +480,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); } @@ -471,7 +497,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(); @@ -505,7 +531,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 ); @@ -522,6 +548,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); @@ -530,10 +557,8 @@ 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); - pufferProtocol.registerValidatorKey{ value: amount }( - data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days - ); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); + pufferProtocol.registerValidatorKey{ value: amount }(data, PUFFER_MODULE_0, 0, new bytes[](0), deadline); vm.stopPrank(); assertApproxEqAbs( @@ -547,27 +572,28 @@ 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, IPufferProtocolLogic.depositValidationTime.selector + ); // 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); // 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: 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 }) ); @@ -590,7 +616,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 @@ -598,15 +624,13 @@ 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(PufferProtocolBase.ValidatorLimitForModuleReached.selector); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + BOND) }( validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); } function test_claim_bond_for_single_withdrawal() external { - uint256 startTimestamp = 1707411226; - // Alice registers one validator and we provision it vm.deal(alice, 3 ether); vm.deal(NoRestakingModule, 200 ether); @@ -615,6 +639,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, @@ -640,13 +666,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, IPufferProtocolLogic.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"); @@ -670,16 +698,15 @@ 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); // Deposit for herself vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsDeposited(alice, alice, 200 ether); - pufferProtocol.depositValidatorTickets(vtPermit, alice); + emit IPufferProtocolEvents.ValidatorTicketsDeposited(alice, alice, 200 ether); + pufferProtocol.depositValidatorTickets(alice, vtAmount); assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 200 ether, "protocol got 200 VT"); assertEq(validatorTicket.balanceOf(address(alice)), 0, "alice got 0"); @@ -699,101 +726,28 @@ 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); // Deposit for herself vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsDeposited(alice, alice, 200 ether); - pufferProtocol.depositValidatorTickets(vtPermit, alice); + emit IPufferProtocolEvents.ValidatorTicketsDeposited(alice, alice, 200 ether); + 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"); @@ -812,7 +766,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(PufferProtocolBase.ActiveOrPendingValidatorsExist.selector); pufferProtocol.withdrawValidatorTickets(30 ether, alice); } @@ -833,7 +787,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( @@ -853,7 +807,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"); @@ -861,13 +815,13 @@ contract PufferProtocolTest is UnitTestHelper { function test_setVTPenalty_bigger_than_minimum_VT_amount() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.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(IPufferProtocol.InvalidVTAmount.selector); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); pufferProtocol.changeMinimumVTAmount(9 * EPOCHS_PER_DAY); } @@ -915,7 +869,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); @@ -946,14 +900,16 @@ 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); - 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, @@ -961,10 +917,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false - }) + }), + deadline ); // 2 days are leftover from 30 (30 is minimum for registration) @@ -984,10 +943,12 @@ 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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ); // 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(PufferProtocolBase.InvalidValidatorState.selector, 0)); _executeFullWithdrawal( StoppedValidatorInfo({ module: NoRestakingModule, @@ -998,7 +959,8 @@ contract PufferProtocolTest is UnitTestHelper { vtConsumptionSignature: vtConsumptionSignature, wasSlashed: false, isDownsize: false - }) + }), + deadline ); } @@ -1014,7 +976,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"); @@ -1027,7 +989,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"); @@ -1043,13 +1005,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1060,7 +1026,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferModuleIndex: 1, withdrawalAmount: 32 ether, totalEpochsValidated: epochsValidated, - vtConsumptionSignature: _getGuardianSignaturesForRegistration(alice, epochsValidated), + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + bob, epochsValidated, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1070,20 +1038,20 @@ 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), block.timestamp + 1 days + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline ); assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); @@ -1107,13 +1075,14 @@ 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; StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](5); stopInfos[0] = StoppedValidatorInfo({ @@ -1122,7 +1091,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1132,7 +1103,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1142,7 +1115,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: true, isDownsize: false }); @@ -1152,7 +1127,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1162,24 +1139,26 @@ 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, 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, @@ -1187,18 +1166,18 @@ 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( - stopInfos, _getHandleBatchWithdrawalMessage(stopInfos), block.timestamp + 1 days + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline ); assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); @@ -1231,9 +1210,10 @@ 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; assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 100 ether, "100 VT in the protocol"); @@ -1251,7 +1231,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1260,7 +1242,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 @@ -1298,14 +1280,15 @@ 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" ); - 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"); @@ -1321,7 +1304,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1330,7 +1315,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 @@ -1369,13 +1354,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -1386,19 +1375,21 @@ 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, 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); - _executeFullWithdrawal(aliceInfo); + 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); - _executeFullWithdrawal(bobInfo); + 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( _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" @@ -1430,15 +1421,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 }); } @@ -1459,6 +1450,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); @@ -1471,14 +1464,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, IPufferProtocolLogic.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"); @@ -1511,6 +1506,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 @@ -1521,14 +1518,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, IPufferProtocolLogic.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"); @@ -1569,6 +1568,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 @@ -1579,14 +1580,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, IPufferProtocolLogic.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"); @@ -1629,6 +1632,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 @@ -1639,14 +1644,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), withdrawalAmount: 31.9 ether, wasSlashed: false, isDownsize: false }); // Burns one whole bond - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); revenueDepositor.depositRevenue(); @@ -1682,6 +1689,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 @@ -1690,14 +1699,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), withdrawalAmount: 32.1 ether, wasSlashed: false, isDownsize: false }); // Burns one whole bond - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); revenueDepositor.depositRevenue(); @@ -1724,6 +1735,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 @@ -1734,10 +1747,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), // penalty is 10 wasSlashed: false, isDownsize: false - }) + }), + deadline ); uint256 leftOverValidationTime = 20 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(); @@ -1763,6 +1779,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.changeMinimumVTAmount(35 ether); vm.stopPrank(); + uint256 deadline = block.timestamp + 1 days; + // Alice exited after 1 day _executeFullWithdrawal( StoppedValidatorInfo({ @@ -1771,10 +1789,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false - }) + }), + deadline ); assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0 ether, "alice got 0 VT left in the protocol"); @@ -1788,13 +1809,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 - ); + 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 assertEq(address(pufferProtocol).balance, 7.5 ether, "7.5 ETH in the protocol"); @@ -1807,13 +1828,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 IPufferProtocolEvents.ValidatorTicketsDeposited(bob, alice, vtAmount); + pufferProtocol.depositValidatorTickets(bob, vtAmount); vm.startPrank(bob); pufferProtocol.withdrawValidatorTickets(50 ether, bob); @@ -1821,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(); @@ -1847,16 +1879,18 @@ 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 = _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. @@ -1875,12 +1909,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. @@ -1977,13 +2011,16 @@ 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, IPufferProtocolLogic.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); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: amount }( - validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures, block.timestamp + 1 days + validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures, deadline ); } @@ -2034,7 +2071,7 @@ contract PufferProtocolTest is UnitTestHelper { function test_setVTPenalty_invalid_amount() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); pufferProtocol.setVTPenalty(type(uint256).max); } @@ -2042,7 +2079,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(PufferProtocolBase.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: 3 ether }( data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); @@ -2050,11 +2087,13 @@ contract PufferProtocolTest is UnitTestHelper { function test_changeMinimumVTAmount_invalid_amount() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); pufferProtocol.changeMinimumVTAmount(0); } function test_panic_batch_withdrawals() public { + uint256 deadline = block.timestamp + 1 days; + // Test with zero epochs StoppedValidatorInfo memory info = StoppedValidatorInfo({ module: NoRestakingModule, @@ -2062,7 +2101,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, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), wasSlashed: false, isDownsize: false }); @@ -2075,7 +2116,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 ); } @@ -2088,7 +2129,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(); } @@ -2101,9 +2142,18 @@ 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(); } + + 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 {