diff --git a/mainnet-contracts/docs/PufferProtocol.md b/mainnet-contracts/docs/PufferProtocol.md index 900dc575..8bfafb72 100644 --- a/mainnet-contracts/docs/PufferProtocol.md +++ b/mainnet-contracts/docs/PufferProtocol.md @@ -23,13 +23,11 @@ The `PufferProtocol` serves as the central contract and fulfills three key funct 9. **Completion and Restaking**: Once the Beacon chain recognizes the validator, a withdrawal credentials merkle proof is submitted back to EigenLayer to enable the restaking of the validator's ETH. - ## Registering a validator ### 1. Prepare Bond and VTs -NoOps are required to deposit pufETH and Validator Tickets (VTs) to register a validator. The amount of pufETH depends on the use of an anti-slasher enclave: +NoOps are required to send enough ETH to cover for the validator bond and the validation time. -- **With an enclave**: 1 ETH worth of pufETH is required. -- **Without an enclave**: 2 ETH worth of pufETH is required. +1.5 ETH is the bond amount required. + 30 days of validation time. @@ -41,11 +39,47 @@ The `PufferProtocol` contract mandates a minimum number of VTs at registration, > function registerValidatorKey( > ValidatorKeyData calldata data, > bytes32 moduleName, -> Permit calldata pufETHPermit, -> Permit calldata vtPermit +> uint256 totalEpochsValidated, +> bytes[] calldata vtConsumptionSignature > ) > ``` +Before calling `registerValidatorKey`, Node Operators must first obtain their VT consumption data from the Puffer Backend API. This data includes: +- Total epochs validated by their active validators +- A signature from the Guardians verifying this data + +```mermaid +sequenceDiagram + participant NodeOperator + participant PufferBackend + participant PufferProtocol + participant Paymaster + participant PufferVault + participant PufferModule + + rect rgb(0, 208, 255) + NodeOperator->>PufferBackend: Request VT consumption data (API CALL) + PufferBackend-->>NodeOperator: Return VT consumption & signatures + end + + rect rgb(25, 202, 84) + NodeOperator->>PufferProtocol: registerValidatorKey(data, moduleName, totalEpochsValidated, vtConsumptionSignature) + PufferProtocol->>PufferProtocol: _checkValidatorRegistrationInputs() + PufferProtocol->>PufferProtocol: _settleVTAccounting() + PufferProtocol->>PufferVault: depositETH() - converts ETH bond to pufETH for the NoOp + PufferProtocol->>PufferProtocol: Store validator data + end + + Note over Paymaster: If there is liquidity and the registration is valid + + rect rgb(25, 202, 199) + Paymaster->>PufferProtocol: provisionNode() + PufferProtocol->>PufferVault: transferETH(32 ETH, pufferModule) + PufferProtocol->>PufferModule: callStake() + PufferModule->>BeaconChain: 32 ETH + end +``` + The NoOp must supply the following `ValidatorKeyData` struct created off-chain: - **`bytes blsPubKey`**: [BLS public key](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/keys/) generated by the NoOp. @@ -70,7 +104,6 @@ This function allows flexibility in how pufETH and VTs are supplied. Options inc - Transferring pufETH and VTs using Permit messages or prior approve transactions. - Combining both methods (e.g., minting pufETH and transferring VTs). - #### Registration side effects Successful registration adds the validator to the `PufferModule` queue. Guardians verify the data and manage keyshare custody. Once verified, they provision the validator with 32 ETH from the `PufferVault` to deploy to the `PufferModule's` `EigenPod`. @@ -90,7 +123,7 @@ The `provisionNode` function executes several critical steps in one atomic trans - It updates the `_numberOfActivePufferValidators` counter on the `PufferOracleV2` contract, reflecting the addition of a new validator on the beacon chain. #### Impact on pufETH Conversion Rate -- The provisioning of a validator involves transferring 32 ETH out of the PufferVault, which could negatively affect the pufETH:ETH conversion rate. To prevent this, the vault calculation includes the ETH amount locked as reported by `PUFFER_ORACLE.getLockedEthAmount()`. When provisioning occurs, the oracle contract’s reported locked ETH amount is increased by 32 ETH. This adjustment ensures that the vault's exchange rate remains unchanged, despite the outflow of funds. For a more detailed explanation, refer to the [`PufferOracleV2`](./PufferOracleV2.md) documentation. +- The provisioning of a validator involves transferring 32 ETH out of the PufferVault, which could negatively affect the pufETH:ETH conversion rate. To prevent this, the vault calculation includes the ETH amount locked as reported by `PUFFER_ORACLE.getLockedEthAmount()`. When provisioning occurs, the oracle contract's reported locked ETH amount is increased by 32 ETH. This adjustment ensures that the vault's exchange rate remains unchanged, despite the outflow of funds. For a more detailed explanation, refer to the [`PufferOracleV2`](./PufferOracleV2.md) documentation. ## Restaking a validator Once a validator is onboarded into a `PufferModule` and their validator is observable from the Beacon chain, their Beacon chain ETH is restaked. This process involves delegating the validator's ETH to a [`RestakingOperator`](./RestakingOperator.md), as determined by the DAO. @@ -110,7 +143,7 @@ Guardians, utilizing their enclaves, are authorized to sign and broadcast volunt - If the Node Operator fails to replenish their VTs after their locked amount has expired. #### After exiting -Post-exit, the exited validator’s ETH will be redirected to the `PufferModule's` `EigenPod`. The [`PufferModuleManager`](./PufferModuleManager.md) oversees the full withdrawal process on EigenLayer, which involves Merkle proofs and queueing, to transfer the ETH back to the `PufferModule`. +Post-exit, the exited validator's ETH will be redirected to the `PufferModule's` `EigenPod`. The [`PufferModuleManager`](./PufferModuleManager.md) oversees the full withdrawal process on EigenLayer, which involves Merkle proofs and queueing, to transfer the ETH back to the `PufferModule`. The Guardians then execute `batchHandleWithdrawals()` on the `PufferProtocol`, which returns pufETH bonds to NoOps, burns their consumed VTs, and performs necessary [accounting](./PufferOracleV2.md). The process addresses three potential scenarios: @@ -121,7 +154,7 @@ The Guardians then execute `batchHandleWithdrawals()` on the `PufferProtocol`, w **Insufficient Withdrawal** (*withdrawalAmount* < 32 ETH): - The entire *withdrawalAmount* is transferred back to the `PufferVault`. -- The missing ETH (32 ETH - *withdrawalAmount*) is burned from the NoOp’s bond. +- The missing ETH (32 ETH - *withdrawalAmount*) is burned from the NoOp's bond. - The NoOp receives the remainder of their bond, assuming losses due to inactivity penalties do not exceed the bond itself. **Validator Was Slashed**: @@ -130,9 +163,37 @@ The Guardians then execute `batchHandleWithdrawals()` on the `PufferProtocol`, w In all scenarios, the `_numberOfActivePufferValidators` count on the `PufferOracleV2` contract is decremented atomically, reducing the locked ETH amount by 32 ETH per exited validator to maintain the pufETH conversion rate. +### Depositing Validation Time + +Valiadtion time gives the node operator ability to keep operating their validator. If they run out of validation time, they will be ejected from the beacon chain. To prevent that from happening, they can deposit validation time. In the previous iteration of the protocol, validation time was represented as ERC20 token Validator Tickets (VTs). This is no longer the case, and the protocol now uses native ETH to represent validation time. The drawback of the previous design was that the node operator was left with VT tokens if ejected early because of the liquidity need for the protocol, and that forced them to sell their VT tokens on secondary markets for a lower price. The new design returns the ETH they deposit to the protocol, if they are ejected early. If they want to keep operating their validator, they can deposit more validation time. To do that, they can call the `depositValidationTime` function and send some amount of ETH to the protocol. That ETH is accounted for in the `PufferProtocol` contract, and the node operator can withdraw if when they exit all of their validators. + +```mermaid +sequenceDiagram + participant NodeOperator + participant PufferBackend + participant PufferProtocol + + rect rgb(0, 208, 255) + NodeOperator->>PufferBackend: Request VT consumption data (API CALL) + PufferBackend-->>NodeOperator: Return VT consumption & signatures + end + + rect rgb(25, 202, 84) + NodeOperator->>PufferProtocol: depositValidationTime(node, vtConsumptionAmount, vtConsumptionSignature) + PufferProtocol->>PufferProtocol: _settleVTAccounting() + end +``` + ## Managing Validator Tickets (VT) #### Understanding VT Consumption -Each validator operated by a NoOp consumes one VT per day. While the `getValidatorTicketsBalance()` function returns the total amount of VTs initially deposited by the NoOp, it does not reflect the real-time balance of VTs. This is due to the prohibitive gas costs associated with continually updating VT balances on-chain. + +Each validator operated by a NoOp consumes one VT per day. The VT consumption is tracked off-chain by the Guardians and verified through signatures. When registering a new validator, the NoOp must: + +1. Query the Puffer Backend API to obtain: + - Total epochs validated by their active validators + - Guardian signatures verifying this data + +This off-chain tracking approach is used to minimize gas costs while maintaining accurate VT consumption records. The `depositValidationTime` function is used to deposit validation time to the protocol. It is important to note that the calculation of the legaxy VT / new Validation Time is done off-chain, and per epoch. Currently, 1 day is equivalent to 225 epochs. #### Off-Chain Tracking and Visualizing VTs To efficiently manage VT consumption without incurring high on-chain costs, Guardians track VT usage off-chain. NoOps can access up-to-date VT consumption information through frontend interfaces, which provide a clear view of their current VT status. @@ -141,12 +202,12 @@ To efficiently manage VT consumption without incurring high on-chain costs, Guar Maintaining active validators requires more than just the initial deposit of a minimum of 28 VTs at registration. To ensure continuous operation and prevent ejection from the network, NoOp must periodically top up their VT balance. This is crucial as running out of VTs could lead to a validator being deactivated. #### Depositing Additional VTs -NoOps can replenish their VT supply by executing the `depositValidatorTickets(permit, nodeOperator)` function. This allows them to add VTs to their account, ensuring their validators can continue to operate without interruption. Note that the `PufferProtocol` tracks validators by wallet address, so only one function call is needed to top up VTs across all of your validators. +NoOps can replenish their VT supply by executing the `depositValidatorTickets(permit, nodeOperator)` function (legacy function) or the `depositValidationTime(node, vtConsumptionAmount, vtConsumptionSignature)`. This allows them to add VTs/Validation Time to their account, ensuring their validators can continue to operate without interruption. Note that the `PufferProtocol` tracks validators by wallet address, so only one function call is needed to top up VTs across all of your validators. It's important for NoOps to monitor their VT consumption regularly and respond proactively to avoid disruptions in their validator operations. #### Withdrawing VTs -Since VT consumption is tracked off-chain, withdrawing excess VTs, `withdrawValidatorTickets` can only be called when the NoOp has no active or pending validators. In future protocol upgrades, ZKPs will be used to allow VTs to be withdrawn while validators are still active. +Since VT consumption is tracked off-chain, withdrawing excess VTs, `withdrawValidatorTickets` can only be called when the NoOp has no active or pending validators (legacy function), the same logic applies to `withdrawValidationTime`. In future protocol upgrades, ZKPs will be used to allow VTs to be withdrawn while validators are still active. ## Validator Rewards in Puffer #### Overview of Rewards @@ -156,7 +217,7 @@ In the Puffer protocol, NoOps receive 100% of the consensus and execution reward When NoOps employ tools like MEV-Boost, execution rewards are directly sent to their designated wallet addresses. This is specified through the `fee recipient` parameter. Unlike other protocols there is no need to share these rewards with the protocol. #### Consensus Rewards -Consensus rewards are directed to the validators’ withdrawal credentials, which are linked to `EigenPods`. These rewards accumulate and, following an upcoming EigenLayer upgrade that improves partial withdrawal gas-efficiency, will be accessible for claiming through the [PufferModules](./PufferModule.md#consensus-rewards). +Consensus rewards are directed to the validators' withdrawal credentials, which are linked to `EigenPods`. These rewards accumulate and, following an upcoming EigenLayer upgrade that improves partial withdrawal gas-efficiency, will be accessible for claiming through the [PufferModules](./PufferModule.md#consensus-rewards). #### Restaking Rewards -Beyond the direct rewards from consensus and execution, Puffer validators also benefit from a share of the protocol's restaking rewards. Similar to consensus rewards, these restaking rewards are set to become claimable in future updates to EigenLayer, enhancing the overall profitability and incentive for NoOps within the Puffer ecosystem. \ No newline at end of file +Restaking rewards are claimed by the Puffer, they are periodically converted to ETH and then deposited to PufferVault. That is how Puffer is able to achieve higher yield compared to other protocols. \ No newline at end of file diff --git a/mainnet-contracts/foundry.toml b/mainnet-contracts/foundry.toml index bae8f562..59c2091a 100644 --- a/mainnet-contracts/foundry.toml +++ b/mainnet-contracts/foundry.toml @@ -29,7 +29,7 @@ optimizer = true optimizer_runs = 200 evm_version = "cancun" # is live on mainnet seed = "0x1337" -solc = "0.8.28" +solc = "0.8.30" # via_ir = true [fmt] diff --git a/mainnet-contracts/script/DeployEverything.s.sol b/mainnet-contracts/script/DeployEverything.s.sol index a32ea88e..4ec8cdda 100644 --- a/mainnet-contracts/script/DeployEverything.s.sol +++ b/mainnet-contracts/script/DeployEverything.s.sol @@ -51,8 +51,11 @@ contract DeployEverything is BaseScript { puffETHDeployment.accessManager, guardiansDeployment.guardianModule, puffETHDeployment.pufferVault ); - PufferProtocolDeployment memory pufferDeployment = - new DeployPuffer().run(guardiansDeployment, puffETHDeployment.pufferVault, pufferOracle); + address revenueDepositor = _deployRevenueDepositor(puffETHDeployment); + + PufferProtocolDeployment memory pufferDeployment = new DeployPuffer().run( + guardiansDeployment, puffETHDeployment.pufferVault, pufferOracle, payable(revenueDepositor) + ); pufferDeployment.pufferDepositor = puffETHDeployment.pufferDepositor; pufferDeployment.pufferVault = puffETHDeployment.pufferVault; @@ -61,7 +64,6 @@ contract DeployEverything is BaseScript { pufferDeployment.timelock = puffETHDeployment.timelock; BridgingDeployment memory bridgingDeployment = new DeployPufETHBridging().run(puffETHDeployment); - address revenueDepositor = _deployRevenueDepositor(puffETHDeployment); pufferDeployment.revenueDepositor = revenueDepositor; new UpgradePufETH().run(puffETHDeployment, pufferOracle, revenueDepositor); diff --git a/mainnet-contracts/script/DeployPuffer.s.sol b/mainnet-contracts/script/DeployPuffer.s.sol index ea294c78..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 @@ -68,11 +69,12 @@ contract DeployPuffer is BaseScript { address treasury; address operationsMultisig; - function run(GuardiansDeployment calldata guardiansDeployment, address pufferVault, address oracle) - public - broadcast - returns (PufferProtocolDeployment memory) - { + function run( + GuardiansDeployment calldata guardiansDeployment, + address pufferVault, + address oracle, + address payable revenueDepositor + ) public broadcast returns (PufferProtocolDeployment memory) { accessManager = AccessManager(guardiansDeployment.accessManager); if (isMainnet()) { @@ -161,7 +163,8 @@ contract DeployPuffer is BaseScript { guardianModule: GuardianModule(payable(guardiansDeployment.guardianModule)), moduleManager: address(moduleManagerProxy), oracle: IPufferOracleV2(oracle), - beaconDepositContract: getStakingContract() + beaconDepositContract: getStakingContract(), + pufferRevenueDistributor: payable(revenueDepositor) }); } @@ -179,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"); @@ -214,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/DeployPufferModuleImplementation.s.sol b/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol index ab1a0ce9..44297610 100644 --- a/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol +++ b/mainnet-contracts/script/DeployPufferModuleImplementation.s.sol @@ -26,7 +26,7 @@ contract DeployPufferModuleImplementation is DeployerHelper { vm.startBroadcast(); PufferModule newImpl = new PufferModule({ - protocol: PufferProtocol(_getPufferProtocol()), + protocol: PufferProtocol(payable(_getPufferProtocol())), eigenPodManager: _getEigenPodManager(), delegationManager: IDelegationManager(_getDelegationManager()), moduleManager: PufferModuleManager(payable(_getPufferModuleManager())), @@ -51,7 +51,7 @@ contract DeployPufferModuleImplementation is DeployerHelper { vm.startPrank(_getPaymaster()); PufferModule newImpl = new PufferModule({ - protocol: PufferProtocol(_getPufferProtocol()), + protocol: PufferProtocol(payable(_getPufferProtocol())), eigenPodManager: _getEigenPodManager(), delegationManager: IDelegationManager(_getDelegationManager()), moduleManager: PufferModuleManager(payable(_getPufferModuleManager())), diff --git a/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol b/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol index d7fba15d..e4c8eb4a 100644 --- a/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol +++ b/mainnet-contracts/script/DeployPufferProtocolImplementation.s.sol @@ -29,7 +29,8 @@ contract DeployPufferProtocolImplementation is DeployerHelper { guardianModule: GuardianModule(payable(_getGuardianModule())), moduleManager: _getPufferModuleManager(), oracle: IPufferOracleV2(_getPufferOracle()), - beaconDepositContract: _getBeaconDepositContract() + beaconDepositContract: _getBeaconDepositContract(), + pufferRevenueDistributor: payable(_getRevenueDepositor()) }) ); diff --git a/mainnet-contracts/script/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 649ac165..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"; @@ -37,16 +37,18 @@ contract GenerateBLSKeysAndRegisterValidators is Script { bytes32 private constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + uint256 private constant SIGNATURE_VALIDITY_PERIOD = 1 days; // TODO: Check this value with team + function setUp() public { if (block.chainid == 17000) { // Holesky protocolAddress = 0xE00c79408B9De5BaD2FDEbB1688997a68eC988CD; - pufferProtocol = PufferProtocol(protocolAddress); + pufferProtocol = PufferProtocol(payable(protocolAddress)); forkVersion = "0x01017000"; } else if (block.chainid == 1) { // Mainnet protocolAddress = 0xf7b6B32492c2e13799D921E84202450131bd238B; - pufferProtocol = PufferProtocol(protocolAddress); + pufferProtocol = PufferProtocol(payable(protocolAddress)); forkVersion = "0x00000000"; } @@ -102,28 +104,12 @@ contract GenerateBLSKeysAndRegisterValidators is Script { blsPubKey: stdJson.readBytes(registrationJson, ".bls_pub_key"), signature: stdJson.readBytes(registrationJson, ".signature"), depositDataRoot: stdJson.readBytes32(registrationJson, ".deposit_data_root"), - blsEncryptedPrivKeyShares: blsEncryptedPrivKeyShares, - blsPubKeySet: stdJson.readBytes(registrationJson, ".bls_pub_key_set"), - raveEvidence: "" - }); - - Permit memory pufETHPermit = _signPermit({ - to: protocolAddress, - amount: 2 ether, // Hardcoded to 2 pufETH - nonce: pufETH.nonces(msg.sender), - deadline: block.timestamp + 12 hours, - domainSeparator: pufETH.DOMAIN_SEPARATOR() + numBatches: 1 }); - Permit memory vtPermit = _signPermit({ - to: protocolAddress, - amount: vtAmount * 1 ether, // Upscale to 10**18 - nonce: validatorTicket.nonces(msg.sender), - deadline: block.timestamp + 12 hours, - domainSeparator: validatorTicket.DOMAIN_SEPARATOR() - }); - - IPufferProtocol(protocolAddress).registerValidatorKey(validatorData, moduleName, pufETHPermit, vtPermit); + IPufferProtocolFull(protocolAddress).registerValidatorKey( + validatorData, moduleName, 0, new bytes[](0), block.timestamp + SIGNATURE_VALIDITY_PERIOD + ); registeredPubKeys.push(validatorData.blsPubKey); } @@ -170,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/Roles.sol b/mainnet-contracts/script/Roles.sol index f969b0b8..dfcd3df1 100644 --- a/mainnet-contracts/script/Roles.sol +++ b/mainnet-contracts/script/Roles.sol @@ -13,6 +13,7 @@ uint64 constant ROLE_ID_OPERATIONS_PAYMASTER = 23; uint64 constant ROLE_ID_OPERATIONS_COORDINATOR = 24; uint64 constant ROLE_ID_WITHDRAWAL_FINALIZER = 25; uint64 constant ROLE_ID_REVENUE_DEPOSITOR = 26; +uint64 constant ROLE_ID_VALIDATOR_EXITOR = 27; // Role assigned to validator ticket price setter uint64 constant ROLE_ID_VT_PRICER = 25; diff --git a/mainnet-contracts/script/SetupAccess.s.sol b/mainnet-contracts/script/SetupAccess.s.sol index 00d6907f..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, @@ -28,7 +29,8 @@ import { ROLE_ID_PUFFER_PROTOCOL, ROLE_ID_DAO, ROLE_ID_OPERATIONS_COORDINATOR, - ROLE_ID_VT_PRICER + ROLE_ID_VT_PRICER, + ROLE_ID_VALIDATOR_EXITOR } from "../script/Roles.sol"; contract SetupAccess is BaseScript { @@ -153,7 +155,7 @@ contract SetupAccess is BaseScript { } function _setupPufferModuleManagerAccess() internal view returns (bytes[] memory) { - bytes[] memory calldatas = new bytes[](2); + bytes[] memory calldatas = new bytes[](4); // Dao selectors bytes4[] memory selectors = new bytes4[](7); @@ -181,6 +183,23 @@ contract SetupAccess is BaseScript { ROLE_ID_OPERATIONS_PAYMASTER ); + // ValidatorExitor selectors + bytes4[] memory requestWithdrawalSelector = new bytes4[](1); + requestWithdrawalSelector[0] = PufferModuleManager.requestWithdrawal.selector; + + calldatas[2] = abi.encodeWithSelector( + AccessManager.setTargetFunctionRole.selector, + pufferDeployment.moduleManager, + requestWithdrawalSelector, + ROLE_ID_VALIDATOR_EXITOR + ); + + calldatas[3] = abi.encodeWithSelector( + AccessManager.setTargetFunctionRole.selector, + pufferDeployment.moduleManager, + requestWithdrawalSelector, + ROLE_ID_PUFFER_PROTOCOL + ); return calldatas; } @@ -304,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, @@ -314,11 +333,13 @@ contract SetupAccess is BaseScript { ROLE_ID_OPERATIONS_PAYMASTER ); - bytes4[] memory publicSelectors = new bytes4[](4); - publicSelectors[0] = PufferProtocol.registerValidatorKey.selector; + bytes4[] memory publicSelectors = new bytes4[](6); + publicSelectors[0] = IPufferProtocolLogic.registerValidatorKey.selector; publicSelectors[1] = PufferProtocol.depositValidatorTickets.selector; publicSelectors[2] = PufferProtocol.withdrawValidatorTickets.selector; publicSelectors[3] = PufferProtocol.revertIfPaused.selector; + publicSelectors[4] = IPufferProtocolLogic.depositValidationTime.selector; + publicSelectors[5] = IPufferProtocolLogic.withdrawValidationTime.selector; calldatas[2] = abi.encodeWithSelector( AccessManager.setTargetFunctionRole.selector, diff --git a/mainnet-contracts/src/GuardianModule.sol b/mainnet-contracts/src/GuardianModule.sol index fec650c9..c0233a97 100644 --- a/mainnet-contracts/src/GuardianModule.sol +++ b/mainnet-contracts/src/GuardianModule.sol @@ -17,6 +17,7 @@ import { StoppedValidatorInfo } from "./struct/StoppedValidatorInfo.sol"; * @title Guardian module * @author Puffer Finance * @dev This contract is responsible for storing enclave keys and validation of guardian's EOA/Enclave signatures + * * @dev Some of these functions are no longer used since enclaves have been deprecated * @custom:security-contact security@puffer.fi */ contract GuardianModule is AccessManaged, IGuardianModule { @@ -38,6 +39,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @notice Enclave Verifier smart contract + * @dev DEPRECATED */ IEnclaveVerifier public immutable ENCLAVE_VERIFIER; @@ -53,11 +55,13 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @dev MRSIGNER value for SGX + * @dev DEPRECATED */ bytes32 internal _mrsigner; /** * @dev MRENCLAVE value for SGX + * @dev DEPRECATED */ bytes32 internal _mrenclave; @@ -77,6 +81,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @dev Mapping of a Guardian's EOA to enclave data + * @dev DEPRECATED */ mapping(address guardian => GuardianData data) internal _guardianEnclaves; @@ -139,6 +144,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function validateProvisionNode( uint256 pufferModuleIndex, @@ -171,11 +177,12 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule */ - function validateBatchWithdrawals(StoppedValidatorInfo[] calldata validatorInfos, bytes[] calldata eoaSignatures) - external - view - { - bytes32 signedMessageHash = LibGuardianMessages._getHandleBatchWithdrawalMessage(validatorInfos); + function validateBatchWithdrawals( + StoppedValidatorInfo[] calldata validatorInfos, + bytes[] calldata eoaSignatures, + uint256 deadline + ) external view { + bytes32 signedMessageHash = LibGuardianMessages._getHandleBatchWithdrawalMessage(validatorInfos, deadline); // Check the signatures bool validSignatures = @@ -207,6 +214,36 @@ contract GuardianModule is AccessManaged, IGuardianModule { } } + /** + * @inheritdoc IGuardianModule + */ + function validateWithdrawalRequest(bytes[] calldata eoaSignatures, bytes32 messageHash) external view { + // Recreate the message hash + bytes32 signedMessageHash = LibGuardianMessages._getAnyHashedMessage(messageHash); + + bool validSignatures = + validateGuardiansEOASignatures({ eoaSignatures: eoaSignatures, signedMessageHash: signedMessageHash }); + + if (!validSignatures) { + revert Unauthorized(); + } + } + + /** + * @inheritdoc IGuardianModule + */ + function validateTotalEpochsValidated(bytes[] calldata eoaSignatures, bytes32 messageHash) external view { + // Recreate the message hash + bytes32 signedMessageHash = LibGuardianMessages._getAnyHashedMessage(messageHash); + + bool validSignatures = + validateGuardiansEOASignatures({ eoaSignatures: eoaSignatures, signedMessageHash: signedMessageHash }); + + if (!validSignatures) { + revert Unauthorized(); + } + } + /** * @inheritdoc IGuardianModule */ @@ -220,6 +257,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function validateGuardiansEnclaveSignatures(bytes[] calldata enclaveSignatures, bytes32 signedMessageHash) public @@ -240,6 +278,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule * @dev Restricted to the DAO + * @dev DEPRECATED */ function setGuardianEnclaveMeasurements(bytes32 newMrEnclave, bytes32 newMrSigner) external restricted { emit MrEnclaveChanged(_mrenclave, newMrEnclave); @@ -298,6 +337,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function rotateGuardianKey(uint256 blockNumber, bytes calldata pubKey, RaveEvidence calldata evidence) external { address guardian = msg.sender; @@ -341,6 +381,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getGuardiansEnclaveAddress(address guardian) external view returns (address) { return _guardianEnclaves[guardian].enclaveAddress; @@ -348,6 +389,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getGuardiansEnclaveAddresses() public view returns (address[] memory) { uint256 guardiansLength = _guardians.length(); @@ -367,6 +409,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getGuardiansEnclavePubkeys() external view returns (bytes[] memory) { uint256 guardiansLength = _guardians.length(); @@ -381,6 +424,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getMrenclave() external view returns (bytes32) { return _mrenclave; @@ -388,6 +432,7 @@ contract GuardianModule is AccessManaged, IGuardianModule { /** * @inheritdoc IGuardianModule + * @dev DEPRECATED */ function getMrsigner() external view returns (bytes32) { return _mrsigner; diff --git a/mainnet-contracts/src/LibGuardianMessages.sol b/mainnet-contracts/src/LibGuardianMessages.sol index 59874181..76112907 100644 --- a/mainnet-contracts/src/LibGuardianMessages.sol +++ b/mainnet-contracts/src/LibGuardianMessages.sol @@ -47,14 +47,15 @@ library LibGuardianMessages { /** * @notice Returns the message to be signed for handling the batch withdrawal * @param validatorInfos is an array of validator information + * @param deadline is the deadline for the signature * @return the message to be signed */ - function _getHandleBatchWithdrawalMessage(StoppedValidatorInfo[] memory validatorInfos) + function _getHandleBatchWithdrawalMessage(StoppedValidatorInfo[] memory validatorInfos, uint256 deadline) internal pure returns (bytes32) { - return keccak256(abi.encode(validatorInfos)).toEthSignedMessageHash(); + return keccak256(abi.encode(validatorInfos, deadline)).toEthSignedMessageHash(); } /** @@ -85,5 +86,14 @@ library LibGuardianMessages { { return keccak256(abi.encode(moduleName, root, blockNumber)).toEthSignedMessageHash(); } + + /** + * @notice Returns the message to be signed for any message + * @param hashedMessage is the hashed message to be signed + * @return the message to be signed + */ + function _getAnyHashedMessage(bytes32 hashedMessage) internal pure returns (bytes32) { + return hashedMessage.toEthSignedMessageHash(); + } } /* solhint-disable func-named-parameters */ diff --git a/mainnet-contracts/src/ProtocolSignatureNonces.sol b/mainnet-contracts/src/ProtocolSignatureNonces.sol new file mode 100644 index 00000000..401f3740 --- /dev/null +++ b/mainnet-contracts/src/ProtocolSignatureNonces.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * @title ProtocolSignatureNonces + * @author Puffer Finance + * @custom:security-contact security@puffer.fi + * @dev Abstract contract for managing protocol signatures with selector-based nonces and deadline support. + * + * This contract implements a selector-based nonce system to prevent DOS attacks through nonce manipulation. + * Each function can have its own nonce space using a unique selector, preventing cross-function nonce conflicts. + * + * Key security features: + * - Selector-based nonces prevent DOS attacks between different operations + * - Deadline support for signature expiration (recommended implementation) + * - Nonce validation to ensure proper signature ordering + */ +abstract contract ProtocolSignatureNonces { + /** + * @dev The nonce used for an `account` is not the expected current nonce. + * @param selector The function selector that determines the nonce space + * @param account The account whose nonce was invalid + * @param currentNonce The current expected nonce for the account + */ + error InvalidAccountNonce(bytes32 selector, address account, uint256 currentNonce); + + struct ProtocolSignatureNoncesStorage { + /** + * @dev Mapping from function selector to account to nonce value. + * This creates separate nonce spaces for different operations, + * preventing cross-function nonce manipulation attacks. + */ + mapping(bytes32 selector => mapping(address account => uint256)) _nonces; + } + + // keccak256(abi.encode(uint256(keccak256("ProtocolSignatureNoncesStorageLocation")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant ProtocolSignatureNoncesStorageLocation = + 0xbaa308cee87141dd88d1ecc2d7cbf7f5fef8a56b897e48c821339feb34e04200; + + /** + * @dev Returns the storage pointer for nonces. + * @return $ The storage pointer to ProtocolSignatureNoncesStorage + */ + function _getProtocolSignatureNoncesStorage() private pure returns (ProtocolSignatureNoncesStorage storage $) { + assembly { + $.slot := ProtocolSignatureNoncesStorageLocation + } + } + + /** + * @dev Returns the next unused nonce for an address in a specific function context. + * @param selector The function selector that determines the nonce space + * @param owner The address to get the nonce for + * @return The current nonce value for the owner in the specified function context + */ + function nonces(bytes32 selector, address owner) public view virtual returns (uint256) { + ProtocolSignatureNoncesStorage storage $ = _getProtocolSignatureNoncesStorage(); + return $._nonces[selector][owner]; + } + + /** + * @dev Consumes a nonce for a specific function context. + * Returns the current value and increments nonce. + * @param selector The function selector that determines the nonce space + * @param owner The address whose nonce to consume + * @return The current nonce value before incrementing + * + * @dev This function increments the nonce atomically, ensuring + * that each nonce can only be used once per function context. + * The nonce cannot be decremented or reset, preventing replay attacks. + */ + function _useNonce(bytes32 selector, address owner) internal virtual returns (uint256) { + ProtocolSignatureNoncesStorage storage $ = _getProtocolSignatureNoncesStorage(); + // For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be + // decremented or reset. This guarantees that the nonce never overflows. + unchecked { + // It is important to do x++ and not ++x here. + return $._nonces[selector][owner]++; + } + } +} diff --git a/mainnet-contracts/src/PufferModule.sol b/mainnet-contracts/src/PufferModule.sol index 7c2e57cb..5b8b0e5d 100644 --- a/mainnet-contracts/src/PufferModule.sol +++ b/mainnet-contracts/src/PufferModule.sol @@ -8,7 +8,7 @@ import { IEigenPodManager } from "../src/interface/Eigenlayer-Slashing/IEigenPod import { ISignatureUtils } from "../src/interface/Eigenlayer-Slashing/ISignatureUtils.sol"; import { IStrategy } from "../src/interface/Eigenlayer-Slashing/IStrategy.sol"; import { IPufferProtocol } from "./interface/IPufferProtocol.sol"; -import { IEigenPod } from "../src/interface/Eigenlayer-Slashing/IEigenPod.sol"; +import { IEigenPod, IEigenPodTypes } from "../src/interface/Eigenlayer-Slashing/IEigenPod.sol"; import { PufferModuleManager } from "./PufferModuleManager.sol"; import { Unauthorized } from "./Errors.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -96,18 +96,6 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { receive() external payable { } - /** - * @notice Starts the validator - */ - function callStake(bytes calldata pubKey, bytes calldata signature, bytes32 depositDataRoot) - external - payable - onlyPufferProtocol - { - // EigenPod is deployed in this call - EIGEN_POD_MANAGER.stake{ value: 32 ether }(pubKey, signature, depositDataRoot); - } - /** * @notice Sets the proof submitter on the EigenPod */ @@ -195,6 +183,56 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { return EIGEN_DELEGATION_MANAGER.undelegate(address(this)); } + /** + * @notice Requests a consolidation for the given validators. This consolidation consists on merging one validator into another one + * @param srcPubkeys The pubkeys of the validators to consolidate from + * @param targetPubkeys The pubkeys of the validators to consolidate to + * @dev Only callable by the PufferProtocol + * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * to the caller from the EigenPod + */ + function requestConsolidation(bytes[] calldata srcPubkeys, bytes[] calldata targetPubkeys) + external + payable + virtual + onlyPufferProtocol + { + ModuleStorage storage $ = _getPufferModuleStorage(); + + IEigenPod.ConsolidationRequest[] memory requests = new IEigenPodTypes.ConsolidationRequest[](srcPubkeys.length); + for (uint256 i = 0; i < srcPubkeys.length; i++) { + requests[i] = + IEigenPodTypes.ConsolidationRequest({ srcPubkey: srcPubkeys[i], targetPubkey: targetPubkeys[i] }); + } + $.eigenPod.requestConsolidation{ value: msg.value }(requests); + } + + /** + * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. + * If the amount is 0, the withdrawal is total and the validator will be fully exited. + * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. + * @param pubkeys The pubkeys of the validators to withdraw + * @param gweiAmounts The amounts of the validators to withdraw, in Gwei + * @dev Only callable by the PufferModuleManager + * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule + */ + function requestWithdrawal(bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) + external + payable + virtual + onlyPufferModuleManager + { + ModuleStorage storage $ = _getPufferModuleStorage(); + + IEigenPodTypes.WithdrawalRequest[] memory requests = new IEigenPodTypes.WithdrawalRequest[](pubkeys.length); + for (uint256 i = 0; i < pubkeys.length; i++) { + requests[i] = IEigenPodTypes.WithdrawalRequest({ pubkey: pubkeys[i], amountGwei: gweiAmounts[i] }); + } + $.eigenPod.requestWithdrawal{ value: msg.value }(requests); + } + /** * @notice Sets the rewards claimer to `claimer` for the PufferModule */ @@ -208,7 +246,7 @@ contract PufferModule is Initializable, AccessManagedUpgradeable { function getWithdrawalCredentials() public view returns (bytes memory) { // Withdrawal credentials for EigenLayer modules are EigenPods ModuleStorage storage $ = _getPufferModuleStorage(); - return abi.encodePacked(bytes1(uint8(1)), bytes11(0), $.eigenPod); + return abi.encodePacked(bytes1(uint8(2)), bytes11(0), $.eigenPod); } /** diff --git a/mainnet-contracts/src/PufferModuleManager.sol b/mainnet-contracts/src/PufferModuleManager.sol index f7d6f619..87daa1b1 100644 --- a/mainnet-contracts/src/PufferModuleManager.sol +++ b/mainnet-contracts/src/PufferModuleManager.sol @@ -9,6 +9,7 @@ import { PufferVaultV5 } from "./PufferVaultV5.sol"; import { RestakingOperator } from "./RestakingOperator.sol"; import { IPufferModuleManager } from "./interface/IPufferModuleManager.sol"; import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { Create2 } from "@openzeppelin/contracts/utils/Create2.sol"; import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; @@ -26,6 +27,9 @@ import { PufferModule } from "./PufferModule.sol"; * @custom:security-contact security@puffer.fi */ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, UUPSUpgradeable { + using Address for address; + using Address for address payable; + address public immutable PUFFER_MODULE_BEACON; address public immutable RESTAKING_OPERATOR_BEACON; address public immutable PUFFER_PROTOCOL; @@ -239,6 +243,54 @@ contract PufferModuleManager is IPufferModuleManager, AccessManagedUpgradeable, emit PufferModuleUndelegated(moduleName); } + /** + * @notice Upgrades the given validators to consolidating (0x02) + * @param moduleName The name of the module + * @param pubkeys The pubkeys of the validators to upgrade + * @dev The function does not check that the pubkeys belong to the module + * @dev Restricted to the DAO + * @dev According to EIP-7251 there is a fee for each validator consolidation request (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount is refunded + * to the caller from the EigenPod + */ + function upgradeToConsolidating(bytes32 moduleName, bytes[] calldata pubkeys) external payable virtual restricted { + address moduleAddress = IPufferProtocol(PUFFER_PROTOCOL).getModuleAddress(moduleName); + + PufferModule(payable(moduleAddress)).requestConsolidation{ value: msg.value }(pubkeys, pubkeys); + + emit PufferModuleUpgradedToConsolidating(moduleName, pubkeys); + } + + /** + * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. + * If the amount is 0, the withdrawal is total and the validator will be fully exited. + * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. + * @param moduleName The name of the module + * @param pubkeys The pubkeys of the validators to withdraw + * @param gweiAmounts The amounts of the validators to withdraw, in Gwei + * @dev Restricted to the VALIDATOR_EXITOR role and the PufferProtocol + * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule + */ + function requestWithdrawal(bytes32 moduleName, bytes[] calldata pubkeys, uint64[] calldata gweiAmounts) + external + payable + virtual + restricted + { + if (pubkeys.length == 0) { + revert InputArrayLengthZero(); + } + if (pubkeys.length != gweiAmounts.length) { + revert InputArrayLengthMismatch(); + } + address moduleAddress = IPufferProtocol(PUFFER_PROTOCOL).getModuleAddress(moduleName); + + PufferModule(payable(moduleAddress)).requestWithdrawal{ value: msg.value }(pubkeys, gweiAmounts); + + emit WithdrawalRequested(moduleName, pubkeys, gweiAmounts); + } + /** * @notice Calls the callRegisterOperatorToAVS function on the target restaking operator * @param restakingOperator is the address of the restaking operator diff --git a/mainnet-contracts/src/PufferOracleV2.sol b/mainnet-contracts/src/PufferOracleV2.sol index a68e32b1..89367084 100644 --- a/mainnet-contracts/src/PufferOracleV2.sol +++ b/mainnet-contracts/src/PufferOracleV2.sol @@ -58,7 +58,7 @@ contract PufferOracleV2 is IPufferOracleV2, AccessManaged { PUFFER_VAULT = vault; _totalNumberOfValidators = 927122; // Oracle will be updated with the correct value _epochNumber = 268828; // Oracle will be updated with the correct value - _setMintPrice(0.01 ether); + _setMintPrice(9803921568628); // This is now price per epoch, and not per day } /** diff --git a/mainnet-contracts/src/PufferProtocol.sol b/mainnet-contracts/src/PufferProtocol.sol index d910547e..45a51c1a 100644 --- a/mainnet-contracts/src/PufferProtocol.sol +++ b/mainnet-contracts/src/PufferProtocol.sol @@ -5,24 +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 { PufferProtocolBase } from "./PufferProtocolBase.sol"; /** * @title PufferProtocol @@ -31,74 +29,8 @@ import { PufferModule } from "./PufferModule.sol"; * @dev Upgradeable smart contract for the Puffer Protocol * Storage variables are located in PufferProtocolStorage.sol */ -contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolStorage { - /** - * @dev Helper struct for the full withdrawals accounting - * 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; - } - - /** - * @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 if the node operator uses SGX - */ - uint256 internal constant _ENCLAVE_VALIDATOR_BOND = 1 ether; - - /** - * @dev ETH Amount required to be deposited as a bond if the node operator doesn't use SGX - */ - uint256 internal constant _NO_ENCLAVE_VALIDATOR_BOND = 2 ether; - - /** - * @dev Default "PUFFER_MODULE_0" module - */ - bytes32 internal constant _PUFFER_MODULE_0 = bytes32("PUFFER_MODULE_0"); - - /** - * @inheritdoc IPufferProtocol - */ - IGuardianModule public immutable override GUARDIAN_MODULE; - - /** - * @inheritdoc IPufferProtocol - */ - ValidatorTicket public immutable override VALIDATOR_TICKET; - - /** - * @inheritdoc IPufferProtocol - */ - 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; +contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgradeable, PufferProtocolBase { + using MessageHashUtils for bytes32; constructor( PufferVaultV5 pufferVault, @@ -106,159 +38,106 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad address moduleManager, ValidatorTicket validatorTicket, IPufferOracleV2 oracle, - address beaconDepositContract - ) { - 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); + address beaconDepositContract, + address payable 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(28 ether); // 28 Validator Tickets - _setVTPenalty(10 ether); // 10 Validator Tickets + _changeMinimumVTAmount(30 * _EPOCHS_PER_DAY); // 30 days worth of ETH is the minimum VT amount + _setVTPenalty(10 * _EPOCHS_PER_DAY); // 10 days worth of ETH is the VT penalty + _setPufferProtocolLogic(pufferProtocolLogic); } /** * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ - function depositValidatorTickets(Permit calldata permit, address node) external restricted { - if (node == address(0)) { - 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].vtBalance += SafeCast.toUint96(permit.amount); - emit ValidatorTicketsDeposited(node, msg.sender, permit.amount); + $.nodeOperatorInfo[node].deprecated_vtBalance += SafeCast.toUint96(amount); + emit ValidatorTicketsDeposited(node, msg.sender, amount); } /** * @inheritdoc IPufferProtocol * @dev Restricted in this context is like `whenNotPaused` modifier from Pausable.sol + * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function withdrawValidatorTickets(uint96 amount, address recipient) external restricted { ProtocolStorage storage $ = _getPufferProtocolStorage(); // 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].vtBalance -= amount; + $.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 registerValidatorKey( - ValidatorKeyData calldata data, - bytes32 moduleName, - Permit calldata pufETHPermit, - Permit calldata vtPermit - ) external payable restricted { - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - // Revert if the permit amounts are non zero, but the msg.value is also non zero - if (vtPermit.amount != 0 && pufETHPermit.amount != 0 && msg.value > 0) { - revert InvalidETHAmount(); - } - - _checkValidatorRegistrationInputs({ $: $, data: data, moduleName: moduleName }); - - uint256 validatorBondInETH = data.raveEvidence.length > 0 ? _ENCLAVE_VALIDATOR_BOND : _NO_ENCLAVE_VALIDATOR_BOND; - - // If the node operator is paying for the bond in ETH and wants to transfer VT from their wallet, the ETH amount they send must be equal the bond amount - if (vtPermit.amount != 0 && pufETHPermit.amount == 0 && msg.value != validatorBondInETH) { - revert InvalidETHAmount(); - } - - uint256 vtPayment = pufETHPermit.amount == 0 ? msg.value - validatorBondInETH : msg.value; - - uint256 receivedVtAmount; - // If the VT permit amount is zero, that means that the user is paying for VT with ETH - if (vtPermit.amount == 0) { - receivedVtAmount = VALIDATOR_TICKET.purchaseValidatorTicket{ value: vtPayment }(address(this)); - } else { - _callPermit(address(VALIDATOR_TICKET), vtPermit); - receivedVtAmount = vtPermit.amount; - - // slither-disable-next-line unchecked-transfer - VALIDATOR_TICKET.transferFrom(msg.sender, address(this), receivedVtAmount); - } - - if (receivedVtAmount < $.minimumVtAmount) { - revert InvalidVTAmount(); - } - - uint256 bondAmount; - - // If the pufETH permit amount is zero, that means that the user is paying the bond with ETH - if (pufETHPermit.amount == 0) { - // Mint pufETH by depositing ETH and store the bond amount - bondAmount = PUFFER_VAULT.depositETH{ value: validatorBondInETH }(address(this)); - } else { - // Calculate the pufETH amount that we need to transfer from the user - bondAmount = PUFFER_VAULT.convertToShares(validatorBondInETH); - _callPermit(address(PUFFER_VAULT), pufETHPermit); - - // slither-disable-next-line unchecked-transfer - PUFFER_VAULT.transferFrom(msg.sender, address(this), bondAmount); - } - - _storeValidatorInformation({ - $: $, - data: data, - pufETHAmount: bondAmount, - moduleName: moduleName, - vtAmount: receivedVtAmount - }); - } - /** * @inheritdoc IPufferProtocol * @dev Restricted to Puffer Paymaster */ - function provisionNode( - bytes[] calldata guardianEnclaveSignatures, - bytes calldata validatorSignature, - bytes32 depositRootHash - ) external restricted { - if (depositRootHash != BEACON_DEPOSIT_CONTRACT.get_deposit_root()) { - revert InvalidDepositRootHash(); - } + function provisionNode(bytes calldata validatorSignature, bytes32 depositRootHash) external restricted { + require(depositRootHash == _BEACON_DEPOSIT_CONTRACT.get_deposit_root(), InvalidDepositRootHash()); ProtocolStorage storage $ = _getPufferProtocolStorage(); @@ -275,7 +154,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad $: $, moduleName: moduleName, index: index, - guardianEnclaveSignatures: guardianEnclaveSignatures, validatorSignature: validatorSignature }); @@ -284,143 +162,70 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad --$.nodeOperatorInfo[node].pendingValidatorCount; ++$.nodeOperatorInfo[node].activeValidatorCount; + // Update numBatches now that validator becomes active + $.nodeOperatorInfo[node].numBatches += $.validators[moduleName][index].numBatches; + // Mark the validator as active $.validators[moduleName][index].status = Status.ACTIVE; } /** * @inheritdoc IPufferProtocol - * @dev Restricted to Puffer Paymaster + * @dev Restricted to Node Operators */ - function batchHandleWithdrawals( - StoppedValidatorInfo[] calldata validatorInfos, - bytes[] calldata guardianEOASignatures - ) external restricted { - GUARDIAN_MODULE.validateBatchWithdrawals(validatorInfos, guardianEOASignatures); - - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - BurnAmounts memory burnAmounts; - Withdrawals[] memory bondWithdrawals = new Withdrawals[](validatorInfos.length); - - // We MUST NOT do the burning/oracle update/transferring ETH from the PufferModule -> PufferVault - // because it affects pufETH exchange rate - - // First, we do the calculations - // slither-disable-start calls-loop - for (uint256 i = 0; i < validatorInfos.length; ++i) { - Validator storage validator = - $.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; - - uint96 bondAmount = validator.bond; - // Get the burnAmount for the withdrawal at the current exchange rate - uint256 burnAmount = - _getBondBurnAmount({ validatorInfo: validatorInfos[i], validatorBondAmount: bondAmount }); - uint256 vtBurnAmount = _getVTBurnAmount($, bondWithdrawals[i].node, validatorInfos[i]); - - // Update the burnAmounts - burnAmounts.pufETH += burnAmount; - burnAmounts.vt += vtBurnAmount; - - // Store the withdrawal amount for that node operator - // nosemgrep basic-arithmetic-underflow - bondWithdrawals[i].pufETHAmount = (bondAmount - burnAmount); - - emit ValidatorExited({ - pubKey: validator.pubKey, - pufferModuleIndex: validatorInfos[i].pufferModuleIndex, - moduleName: validatorInfos[i].moduleName, - pufETHBurnAmount: burnAmount, - vtBurnAmount: vtBurnAmount - }); - - // Decrease the number of registered validators for that module - _decreaseNumberOfRegisteredValidators($, validatorInfos[i].moduleName); - // Storage VT and the active validator count update for the Node Operator - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[validator.node].vtBalance -= SafeCast.toUint96(vtBurnAmount); - --$.nodeOperatorInfo[validator.node].activeValidatorCount; - - delete validator.node; - delete validator.bond; - delete validator.module; - delete validator.status; - delete validator.pubKey; - } - - VALIDATOR_TICKET.burn(burnAmounts.vt); - // Because we've calculated everything in the previous loop, we can do the burning - PUFFER_VAULT.burn(burnAmounts.pufETH); - // Deduct 32 ETH from the `lockedETHAmount` on the PufferOracle - PUFFER_ORACLE.exitValidators(validatorInfos.length); - - // In this loop, we transfer back the bonds, and do the accounting that affects the exchange rate - for (uint256 i = 0; i < validatorInfos.length; ++i) { - // If the withdrawal amount is bigger than 32 ETH, we cap it to 32 ETH - // The excess is the rewards amount for that Node Operator - uint256 transferAmount = - validatorInfos[i].withdrawalAmount > 32 ether ? 32 ether : validatorInfos[i].withdrawalAmount; - //solhint-disable-next-line avoid-low-level-calls - (bool success,) = - PufferModule(payable(validatorInfos[i].module)).call(address(PUFFER_VAULT), transferAmount, ""); - if (!success) { - revert Failed(); - } + 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 + ); - // 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 + _PUFFER_MODULE_MANAGER.requestWithdrawal{ value: msg.value }(moduleName, pubkeys, gweiAmounts); } - /** - * @inheritdoc IPufferProtocol - * @dev Restricted to Puffer Paymaster - */ - function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external restricted { + function _processWithdrawalValidation( + bytes32 moduleName, + uint256[] calldata indices, + uint64[] calldata gweiAmounts, + WithdrawalType[] calldata withdrawalType, + bytes[][] calldata validatorAmountsSignatures, + uint256 deadline + ) internal returns (bytes[] memory pubkeys) { ProtocolStorage storage $ = _getPufferProtocolStorage(); - - 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 vtPenalty = $.vtPenalty; - // Burn VT penalty amount from the Node Operator - VALIDATOR_TICKET.burn(vtPenalty); - // nosemgrep basic-arithmetic-underflow - $.nodeOperatorInfo[node].vtBalance -= SafeCast.toUint96(vtPenalty); - --$.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]; + pubkeys = new bytes[](indices.length); + + 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(gweiAmount == 0, InvalidWithdrawAmount()); + } else { + if (withdrawalType[i] == WithdrawalType.DOWNSIZE) { + uint256 batches = gweiAmount / _32_ETH_GWEI; + require(batches > validator.numBatches && gweiAmount % _32_ETH_GWEI == 0, InvalidWithdrawAmount()); + } + + bytes32 messageHash = keccak256( + abi.encode( + msg.sender, + pubkeys[i], + gweiAmount, + _useNonce(IPufferProtocol.requestWithdrawal.selector, msg.sender), + deadline + ) + ).toEthSignedMessageHash(); + _validateSignatures(messageHash, validatorAmountsSignatures[i]); + } } - emit ValidatorSkipped($.validators[moduleName][skippedIndex].pubKey, skippedIndex, moduleName); } /** @@ -458,12 +263,19 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad _setVTPenalty(newPenaltyAmount); } + /** + * @dev Restricted to the DAO + */ + function setPufferProtocolLogic(address newPufferProtocolLogic) external restricted { + _setPufferProtocolLogic(newPufferProtocolLogic); + } + /** * @inheritdoc IPufferProtocol */ function getVTPenalty() external view returns (uint256) { ProtocolStorage storage $ = _getPufferProtocolStorage(); - return $.vtPenalty; + return $.vtPenaltyEpochs; } /** @@ -594,11 +406,16 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad /** * @inheritdoc IPufferProtocol + * @dev DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function getValidatorTicketsBalance(address owner) public view returns (uint256) { ProtocolStorage storage $ = _getPufferProtocolStorage(); + return $.nodeOperatorInfo[owner].deprecated_vtBalance; + } - return $.nodeOperatorInfo[owner].vtBalance; + function getValidationTime(address owner) public view returns (uint256) { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + return $.nodeOperatorInfo[owner].validationTime; } /** @@ -609,25 +426,6 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad return $.minimumVtAmount; } - /** - * @notice Returns necessary information to make Guardian's life easier - */ - function getPayload(bytes32 moduleName, bool usingEnclave) - external - view - returns (bytes[] memory, bytes memory, uint256, uint256) - { - ProtocolStorage storage $ = _getPufferProtocolStorage(); - - bytes[] memory pubKeys = GUARDIAN_MODULE.getGuardiansEnclavePubkeys(); - bytes memory withdrawalCredentials = getWithdrawalCredentials(address($.modules[moduleName])); - uint256 threshold = GUARDIAN_MODULE.getThreshold(); - uint256 validatorBond = usingEnclave ? _ENCLAVE_VALIDATOR_BOND : _NO_ENCLAVE_VALIDATOR_BOND; - uint256 ethAmount = validatorBond + ($.minimumVtAmount * PUFFER_ORACLE.getValidatorTicketPrice()) / 1 ether; - - return (pubKeys, withdrawalCredentials, threshold, ethAmount); - } - /** * @inheritdoc IPufferProtocol */ @@ -642,52 +440,18 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad */ function revertIfPaused() external restricted { } - function _storeValidatorInformation( - ProtocolStorage storage $, - ValidatorKeyData calldata data, - uint256 pufETHAmount, - bytes32 moduleName, - uint256 vtAmount - ) internal { - uint256 pufferModuleIndex = $.pendingValidatorIndices[moduleName]; - - // No need for SafeCast - $.validators[moduleName][pufferModuleIndex] = Validator({ - pubKey: data.blsPubKey, - status: Status.PENDING, - module: address($.modules[moduleName]), - bond: uint96(pufETHAmount), - node: msg.sender - }); - - $.nodeOperatorInfo[msg.sender].vtBalance += SafeCast.toUint96(vtAmount); - - // Increment indices for this module and number of validators registered - unchecked { - ++$.nodeOperatorInfo[msg.sender].pendingValidatorCount; - ++$.pendingValidatorIndices[moduleName]; - ++$.moduleLimits[moduleName].numberOfRegisteredValidators; - } - emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); - emit ValidatorKeyRegistered(data.blsPubKey, pufferModuleIndex, moduleName, (data.raveEvidence.length > 0)); - } - 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(); - } - emit VTPenaltyChanged($.vtPenalty, newPenaltyAmount); - $.vtPenalty = newPenaltyAmount; + require(newPenaltyAmount <= $.minimumVtAmount, InvalidVTAmount()); + emit VTPenaltyChanged($.vtPenaltyEpochs, newPenaltyAmount); + $.vtPenaltyEpochs = newPenaltyAmount; } function _setModuleWeights(bytes32[] memory newModuleWeights) internal { @@ -698,10 +462,8 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad 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()); @@ -710,143 +472,83 @@ contract PufferProtocol is IPufferProtocol, AccessManagedUpgradeable, UUPSUpgrad return address(module); } - function _checkValidatorRegistrationInputs( - ProtocolStorage storage $, - ValidatorKeyData calldata data, - bytes32 moduleName - ) internal view { - // This acts as a validation if the module is existent - // +1 is to validate the current transaction registration - if (($.moduleLimits[moduleName].numberOfRegisteredValidators + 1) > $.moduleLimits[moduleName].allowedLimit) { - revert ValidatorLimitForModuleReached(); - } - - if (data.blsPubKey.length != _BLS_PUB_KEY_LENGTH) { - revert InvalidBLSPubKey(); - } - - // Every guardian needs to receive a share - if (data.blsEncryptedPrivKeyShares.length != GUARDIAN_MODULE.getGuardians().length) { - revert InvalidBLSPrivateKeyShares(); - } - - // blsPubKeySet is for a subset of guardians and because of that we use .getThreshold() - if (data.blsPubKeySet.length != (GUARDIAN_MODULE.getThreshold() * _BLS_PUB_KEY_LENGTH)) { - revert InvalidBLSPublicKeySet(); - } - } - function _changeMinimumVTAmount(uint256 newMinimumVtAmount) internal { ProtocolStorage storage $ = _getPufferProtocolStorage(); - if (newMinimumVtAmount < $.vtPenalty) { + if (newMinimumVtAmount < $.vtPenaltyEpochs) { revert InvalidVTAmount(); } emit MinimumVTAmountChanged($.minimumVtAmount, newMinimumVtAmount); $.minimumVtAmount = newMinimumVtAmount; } - function _getBondBurnAmount(StoppedValidatorInfo calldata validatorInfo, uint256 validatorBondAmount) - 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, we burn the difference to cover up the loss for inactivity - if (validatorInfo.withdrawalAmount < 32 ether) { - pufETHBurnAmount = PUFFER_VAULT.convertToSharesUp(32 ether - validatorInfo.withdrawalAmount); - } - // Case 3: - // Withdrawal amount was >= 32 ether, we don't burn anything - return pufETHBurnAmount; - } - function _validateSignaturesAndProvisionValidator( ProtocolStorage storage $, bytes32 moduleName, uint256 index, - bytes[] calldata guardianEnclaveSignatures, bytes calldata validatorSignature ) internal { bytes memory validatorPubKey = $.validators[moduleName][index].pubKey; + uint256 numBatches = $.validators[moduleName][index].numBatches; bytes memory withdrawalCredentials = getWithdrawalCredentials($.validators[moduleName][index].module); bytes32 depositDataRoot = LibBeaconchainContract.getDepositDataRoot(validatorPubKey, validatorSignature, withdrawalCredentials); - // Check the signatures (reverts if invalid) - GUARDIAN_MODULE.validateProvisionNode({ - pufferModuleIndex: index, - pubKey: validatorPubKey, - signature: validatorSignature, - depositDataRoot: depositDataRoot, - withdrawalCredentials: withdrawalCredentials, - guardianEnclaveSignatures: guardianEnclaveSignatures - }); - PufferModule module = $.modules[moduleName]; - // Transfer 32 ETH to the module - PUFFER_VAULT.transferETH(address(module), 32 ether); + // Transfer 32 ETH to this contract for each batch + _PUFFER_VAULT.transferETH(address(this), numBatches * 32 ether); - emit SuccessfullyProvisioned(validatorPubKey, index, moduleName); + emit SuccessfullyProvisioned(validatorPubKey, index, moduleName, numBatches); // Increase lockedETH on Puffer Oracle - PUFFER_ORACLE.provisionNode(); + for (uint256 i = 0; i < numBatches; ++i) { + _PUFFER_ORACLE.provisionNode(); + } - module.callStake({ pubKey: validatorPubKey, signature: validatorSignature, depositDataRoot: depositDataRoot }); + _BEACON_DEPOSIT_CONTRACT.deposit{ value: numBatches * 32 ether }( + validatorPubKey, module.getWithdrawalCredentials(), validatorSignature, depositDataRoot + ); } - function _getVTBurnAmount(ProtocolStorage storage $, address node, StoppedValidatorInfo calldata validatorInfo) - internal - view - returns (uint256) - { - uint256 validatedEpochs = validatorInfo.endEpoch - validatorInfo.startEpoch; - // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day - // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up - uint256 vtBurnAmount = validatedEpochs * 4444444444444445; - - uint256 minimumVTAmount = $.minimumVtAmount; - uint256 nodeVTBalance = $.nodeOperatorInfo[node].vtBalance; - - // If the VT burn amount is less than the minimum VT amount that means that the node operator exited early - // If we don't penalize it, the node operator can exit early and re-register with the same VTs. - // By doing that, they can lower the APY for the pufETH holders - if (minimumVTAmount > vtBurnAmount) { - // Case when the node operator registered the validator but afterwards the DAO increases the minimum VT amount - if (nodeVTBalance < minimumVTAmount) { - return nodeVTBalance; - } + function _setPufferProtocolLogic(address newPufferProtocolLogic) internal { + ProtocolStorage storage $ = _getPufferProtocolStorage(); + emit PufferProtocolLogicSet($.pufferProtocolLogic, newPufferProtocolLogic); + $.pufferProtocolLogic = newPufferProtocolLogic; + } - return minimumVTAmount; - } + function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } - return vtBurnAmount; + function getPufferProtocolLogic() external view override returns (address) { + return _getPufferProtocolStorage().pufferProtocolLogic; } - 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 GUARDIAN_MODULE() external view override returns (IGuardianModule) { + return _GUARDIAN_MODULE; } - function _decreaseNumberOfRegisteredValidators(ProtocolStorage storage $, bytes32 moduleName) internal { - --$.moduleLimits[moduleName].numberOfRegisteredValidators; - emit NumberOfRegisteredValidatorsChanged(moduleName, $.moduleLimits[moduleName].numberOfRegisteredValidators); + function VALIDATOR_TICKET() external view override returns (ValidatorTicket) { + return _VALIDATOR_TICKET; } - function _authorizeUpgrade(address newImplementation) internal virtual override restricted { } + function PUFFER_VAULT() external view override returns (PufferVaultV5) { + return _PUFFER_VAULT; + } + + function PUFFER_MODULE_MANAGER() external view override returns (PufferModuleManager) { + return _PUFFER_MODULE_MANAGER; + } + + function PUFFER_ORACLE() external view override returns (IPufferOracleV2) { + return _PUFFER_ORACLE; + } + + function BEACON_DEPOSIT_CONTRACT() external view override returns (IBeaconDepositContract) { + return _BEACON_DEPOSIT_CONTRACT; + } + + function PUFFER_REVENUE_DISTRIBUTOR() external view override returns (address payable) { + return _PUFFER_REVENUE_DISTRIBUTOR; + } } diff --git a/mainnet-contracts/src/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/Eigenlayer-Slashing/IEigenPod.sol b/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol index b465f711..9473ff6a 100644 --- a/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol +++ b/mainnet-contracts/src/interface/Eigenlayer-Slashing/IEigenPod.sol @@ -4,6 +4,7 @@ pragma solidity >=0.5.0; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../libraries/BeaconChainProofs.sol"; +import "./ISemVerMixin.sol"; import "./IEigenPodManager.sol"; interface IEigenPodErrors { @@ -42,8 +43,6 @@ interface IEigenPodErrors { /// @dev Thrown when amount exceeds `restakedExecutionLayerGwei`. error InsufficientWithdrawableBalance(); - /// @dev Thrown when provided `amountGwei` is not a multiple of gwei. - error AmountMustBeMultipleOfGwei(); /// Validator Status @@ -60,6 +59,17 @@ interface IEigenPodErrors { /// @dev Thrown when a validator has not been slashed on the beacon chain. error ValidatorNotSlashedOnBeaconChain(); + /// Consolidation and Withdrawal Requests + + /// @dev Thrown when a predeploy request is initiated with insufficient msg.value + error InsufficientFunds(); + /// @dev Thrown when refunding excess fees from a predeploy fails + error RefundFailed(); + /// @dev Thrown when calling the predeploy fails + error PredeployFailed(); + /// @dev Thrown when querying a predeploy for its current fee fails + error FeeQueryFailed(); + /// Misc /// @dev Thrown when an invalid block root is returned by the EIP-4788 oracle. @@ -68,24 +78,28 @@ interface IEigenPodErrors { error MsgValueNot32ETH(); /// @dev Thrown when provided `beaconTimestamp` is too far in the past. error BeaconTimestampTooFarInPast(); + /// @dev Thrown when the pectraForkTimestamp returned from the EigenPodManager is zero + error ForkTimestampZero(); } interface IEigenPodTypes { enum VALIDATOR_STATUS { - INACTIVE, // doesn't exist + INACTIVE, // does not exist ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod WITHDRAWN // withdrawn from the Beacon Chain } + /** + * @param validatorIndex index of the validator on the beacon chain + * @param restakedBalanceGwei amount of beacon chain ETH restaked on EigenLayer in gwei + * @param lastCheckpointedAt timestamp of the validator's most recent balance update + * @param status last recorded status of the validator + */ struct ValidatorInfo { - // index of the validator in the beacon chain uint64 validatorIndex; - // amount of beacon chain ETH restaked on EigenLayer in gwei uint64 restakedBalanceGwei; - //timestamp of the validator's most recent balance update uint64 lastCheckpointedAt; - // status of the validator VALIDATOR_STATUS status; } @@ -96,6 +110,30 @@ interface IEigenPodTypes { int64 balanceDeltasGwei; uint64 prevBeaconBalanceGwei; } + + /** + * @param srcPubkey the pubkey of the source validator for the consolidation + * @param targetPubkey the pubkey of the target validator for the consolidation + * @dev Note that if srcPubkey == targetPubkey, this is a "switch request," and will + * change the validator's withdrawal credential type from 0x01 to 0x02. + * For more notes on usage, see `requestConsolidation` + */ + struct ConsolidationRequest { + bytes srcPubkey; + bytes targetPubkey; + } + + /** + * @param pubkey the pubkey of the validator to withdraw from + * @param amountGwei the amount (in gwei) to withdraw from the beacon chain to the pod + * @dev Note that if amountGwei == 0, this is a "full exit request," and will fully exit + * the validator to the pod. + * For more notes on usage, see `requestWithdrawal` + */ + struct WithdrawalRequest { + bytes pubkey; + uint64 amountGwei; + } } interface IEigenPodEvents is IEigenPodTypes { @@ -131,6 +169,18 @@ interface IEigenPodEvents is IEigenPodTypes { /// @notice Emitted when a validaor is proven to have 0 balance at a given checkpoint event ValidatorWithdrawn(uint64 indexed checkpointTimestamp, uint40 indexed validatorIndex); + + /// @notice Emitted when a consolidation request is initiated where source == target + event SwitchToCompoundingRequested(bytes32 indexed validatorPubkeyHash); + + /// @notice Emitted when a standard consolidation request is initiated + event ConsolidationRequested(bytes32 indexed sourcePubkeyHash, bytes32 indexed targetPubkeyHash); + + /// @notice Emitted when a withdrawal request is initiated where request.amountGwei == 0 + event ExitRequested(bytes32 indexed validatorPubkeyHash); + + /// @notice Emitted when a partial withdrawal request is initiated + event WithdrawalRequested(bytes32 indexed validatorPubkeyHash, uint64 withdrawalAmountGwei); } /** @@ -140,19 +190,18 @@ interface IEigenPodEvents is IEigenPodTypes { * @dev Note that all beacon chain balances are stored as gwei within the beacon chain datastructures. We choose * to account balances in terms of gwei in the EigenPod contract and convert to wei when making calls to other contracts */ -interface IEigenPod is IEigenPodErrors, IEigenPodEvents { +interface IEigenPod is IEigenPodErrors, IEigenPodEvents, ISemVerMixin { /// @notice Used to initialize the pointers to contracts crucial to the pod's functionality, in beacon proxy construction from EigenPodManager function initialize(address owner) external; /// @notice Called by EigenPodManager when the owner wants to create another ETH validator. + /// @dev This function only supports staking to a 0x01 validator. For compounding validators, please interact directly with the deposit contract. function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; /** - * @notice Transfers `amountWei` in ether from this contract to the specified `recipient` address - * @notice Called by EigenPodManager to withdrawBeaconChainETH that has been added to the EigenPod's balance due to a withdrawal from the beacon chain. - * @dev The podOwner must have already proved sufficient withdrawals, so that this pod's `restakedExecutionLayerGwei` exceeds the - * `amountWei` input (when converted to GWEI). - * @dev Reverts if `amountWei` is not a whole Gwei amount + * @notice Transfers `amountWei` from this contract to the `recipient`. Only callable by the EigenPodManager as part + * of the DelegationManager's withdrawal flow. + * @dev `amountWei` is not required to be a whole Gwei amount. Amounts less than a Gwei multiple may be unrecoverable due to Gwei conversion. */ function withdrawRestakedBeaconChainETH(address recipient, uint256 amount) external; @@ -243,16 +292,105 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { BeaconChainProofs.ValidatorProof calldata proof ) external; + /// @notice Allows the owner or proof submitter to initiate one or more requests to + /// consolidate their validators on the beacon chain. + /// @param requests An array of requests consisting of the source and target pubkeys + /// of the validators to be consolidated + /// @dev Both the source and target validator MUST have active withdrawal credentials + /// pointed at the pod + /// @dev The consolidation request predeploy requires a fee is sent with each request; + /// this is pulled from msg.value. After submitting all requests, any remaining fee is + /// refunded to the caller by calling its fallback function. + /// @dev This contract exposes `getConsolidationRequestFee` to query the current fee for + /// a single request. If submitting multiple requests in a single block, the total fee + /// is equal to (fee * requests.length). This fee is updated at the end of each block. + /// + /// (See https://eips.ethereum.org/EIPS/eip-7251#fee-calculation for details) + /// + /// @dev Note on beacon chain behavior: + /// - If request.srcPubkey == request.targetPubkey, this is a "switch" consolidation. Once + /// processed on the beacon chain, the validator's withdrawal credentials will be changed + /// to compounding (0x02). + /// - The rest of the notes assume src != target. + /// - The target validator MUST already have 0x02 credentials. The source validator can have either. + /// - Consoldiation sets the source validator's exit_epoch and withdrawable_epoch, similar to an exit. + /// When the exit epoch is reached, an epoch sweep will process the consolidation and transfer balance + /// from the source to the target validator. + /// - Consolidation transfers min(srcValidator.effective_balance, state.balance[srcIndex]) to the target. + /// This may not be the entirety of the source validator's balance; any remainder will be moved to the + /// pod when hit by a subsequent withdrawal sweep. + /// + /// @dev Note that consolidation requests CAN FAIL for a variety of reasons. Failures occur when the request + /// is processed on the beacon chain, and are invisible to the pod. The pod and predeploy cannot guarantee + /// a request will succeed; it's up to the pod owner to determine this for themselves. If your request fails, + /// you can retry by initiating another request via this method. + /// + /// Some requirements that are NOT checked by the pod: + /// - If request.srcPubkey == request.targetPubkey, the validator MUST have 0x01 credentials + /// - If request.srcPubkey != request.targetPubkey, the target validator MUST have 0x02 credentials + /// - Both the source and target validators MUST be active and MUST NOT have initiated exits + /// - The source validator MUST NOT have pending partial withdrawal requests (via `requestWithdrawal`) + /// - If the source validator is slashed after requesting consolidation (but before processing), + /// the consolidation will be skipped. + /// + /// For further reference, see consolidation processing at block and epoch boundaries: + /// - Block: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_consolidation_request + /// - Epoch: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_pending_consolidations + function requestConsolidation(ConsolidationRequest[] calldata requests) external payable; + + /// @notice Allows the owner or proof submitter to initiate one or more requests to + /// withdraw funds from validators on the beacon chain. + /// @param requests An array of requests consisting of the source validator and an + /// amount to withdraw + /// @dev The withdrawal request predeploy requires a fee is sent with each request; + /// this is pulled from msg.value. After submitting all requests, any remaining fee is + /// refunded to the caller by calling its fallback function. + /// @dev This contract exposes `getWithdrawalRequestFee` to query the current fee for + /// a single request. If submitting multiple requests in a single block, the total fee + /// is equal to (fee * requests.length). This fee is updated at the end of each block. + /// + /// (See https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule for details) + /// + /// @dev Note on beacon chain behavior: + /// - Withdrawal requests have two types: full exit requests, and partial exit requests. + /// Partial exit requests will be skipped if the validator has 0x01 withdrawal credentials. + /// If you want your validators to have access to partial exits, use `requestConsolidation` + /// to change their withdrawal credentials to compounding (0x02). + /// - If request.amount == 0, this is a FULL exit request. A full exit request initiates a + /// standard validator exit. + /// - Other amounts are treated as PARTIAL exit requests. A partial exit request will NOT result + /// in a validator with less than 32 ETH balance. Any requested amount above this is ignored. + /// - The actual amount withdrawn for a partial exit is given by the formula: + /// min(request.amount, state.balances[vIdx] - 32 ETH - pending_balance_to_withdraw) + /// (where `pending_balance_to_withdraw` is the sum of any outstanding partial exit requests) + /// (Note that this means you may request more than is actually withdrawn!) + /// + /// @dev Note that withdrawal requests CAN FAIL for a variety of reasons. Failures occur when the request + /// is processed on the beacon chain, and are invisible to the pod. The pod and predeploy cannot guarantee + /// a request will succeed; it's up to the pod owner to determine this for themselves. If your request fails, + /// you can retry by initiating another request via this method. + /// + /// Some requirements that are NOT checked by the pod: + /// - request.pubkey MUST be a valid validator pubkey + /// - request.pubkey MUST belong to a validator whose withdrawal credentials are this pod + /// - If request.amount is for a partial exit, the validator MUST have 0x02 withdrawal credentials + /// - If request.amount is for a full exit, the validator MUST NOT have any pending partial exits + /// - The validator MUST be active and MUST NOT have initiated exit + /// + /// For further reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-process_withdrawal_request + function requestWithdrawal(WithdrawalRequest[] calldata requests) external payable; + /// @notice called by owner of a pod to remove any ERC20s deposited in the pod function recoverTokens(IERC20[] memory tokenList, uint256[] memory amountsToWithdraw, address recipient) external; /// @notice Allows the owner of a pod to update the proof submitter, a permissioned - /// address that can call `startCheckpoint` and `verifyWithdrawalCredentials`. + /// address that can call various EigenPod methods, but cannot trigger asset withdrawals + /// from the DelegationManager. /// @dev Note that EITHER the podOwner OR proofSubmitter can access these methods, /// so it's fine to set your proofSubmitter to 0 if you want the podOwner to be the /// only address that can call these methods. /// @param newProofSubmitter The new proof submitter address. If set to 0, only the - /// pod owner will be able to call `startCheckpoint` and `verifyWithdrawalCredentials` + /// pod owner will be able to call EigenPod methods. function setProofSubmitter(address newProofSubmitter) external; /** @@ -267,7 +405,8 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { /// @dev If this address is NOT set, only the podOwner can call `startCheckpoint` and `verifyWithdrawalCredentials` function proofSubmitter() external view returns (address); - /// @notice the amount of execution layer ETH in this contract that is staked in EigenLayer (i.e. withdrawn from beaconchain but not EigenLayer), + /// @notice Native ETH in the pod that has been accounted for in a checkpoint (denominated in gwei). + /// This amount is withdrawable from the pod via the DelegationManager withdrawal flow. function withdrawableRestakedExecutionLayerGwei() external view returns (uint64); /// @notice The single EigenPodManager for EigenLayer @@ -282,10 +421,10 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { /// @notice Returns the validatorInfo struct for the provided pubkey function validatorPubkeyToInfo(bytes calldata validatorPubkey) external view returns (ValidatorInfo memory); - /// @notice This returns the status of a given validator + /// @notice Returns the validator status for a given validator pubkey hash function validatorStatus(bytes32 pubkeyHash) external view returns (VALIDATOR_STATUS); - /// @notice This returns the status of a given validator pubkey + /// @notice Returns the validator status for a given validator pubkey function validatorStatus(bytes calldata validatorPubkey) external view returns (VALIDATOR_STATUS); /// @notice Number of validators with proven withdrawal credentials, who do not have proven full withdrawals @@ -298,6 +437,8 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { function currentCheckpointTimestamp() external view returns (uint64); /// @notice Returns the currently-active checkpoint + /// To save gas on checkpoint creation, we don't delete checkpoints when they're completed. + /// If there's not an active checkpoint, this method returns an empty Checkpoint. function currentCheckpoint() external view returns (Checkpoint memory); /// @notice For each checkpoint, the total balance attributed to exited validators, in gwei @@ -335,4 +476,14 @@ interface IEigenPod is IEigenPodErrors, IEigenPodEvents { /// to an existing slot within the last 24 hours. If the slot at `timestamp` was skipped, this method /// will revert. function getParentBlockRoot(uint64 timestamp) external view returns (bytes32); + + /// @notice Returns the fee required to add a consolidation request to the EIP-7251 predeploy this block. + /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7251#fee-calculation + /// Consider overestimating the amount sent to ensure the fee does not update before your transaction. + function getConsolidationRequestFee() external view returns (uint256); + + /// @notice Returns the current fee required to add a withdrawal request to the EIP-7002 predeploy. + /// @dev Note that the predeploy updates its fee every block according to https://eips.ethereum.org/EIPS/eip-7002#fee-update-rule + /// Consider overestimating the amount sent to ensure the fee does not update before your transaction. + function getWithdrawalRequestFee() external view returns (uint256); } diff --git a/mainnet-contracts/src/interface/Eigenlayer-Slashing/ISemVerMixin.sol b/mainnet-contracts/src/interface/Eigenlayer-Slashing/ISemVerMixin.sol new file mode 100644 index 00000000..206cf38d --- /dev/null +++ b/mainnet-contracts/src/interface/Eigenlayer-Slashing/ISemVerMixin.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/// @title ISemVerMixin +/// @notice A mixin interface that provides semantic versioning functionality. +/// @dev Follows SemVer 2.0.0 specification (https://semver.org/) +interface ISemVerMixin { + /// @notice Returns the semantic version string of the contract. + /// @return The version string in SemVer format (e.g., "v1.1.1") + function version() external view returns (string memory); +} diff --git a/mainnet-contracts/src/interface/IGuardianModule.sol b/mainnet-contracts/src/interface/IGuardianModule.sol index 7958f090..d68d1f5d 100644 --- a/mainnet-contracts/src/interface/IGuardianModule.sol +++ b/mainnet-contracts/src/interface/IGuardianModule.sol @@ -8,6 +8,7 @@ import { StoppedValidatorInfo } from "../struct/StoppedValidatorInfo.sol"; /** * @title IGuardianModule interface * @author Puffer Finance + * @dev Some of these functions are no longer used since enclaves have been deprecated */ interface IGuardianModule { /** @@ -19,6 +20,7 @@ interface IGuardianModule { /** * @notice Thrown when the RAVE evidence is not valid * @dev Signature "0x2b3c629b" + * @dev DEPRECATED */ error InvalidRAVE(); @@ -64,23 +66,27 @@ interface IGuardianModule { * @param guardianEnclave is the enclave address * @param pubKey is the public key * @dev Signature "0x14720919b20fceff2a396c4973d37c6087e4619d40c8f4003d8e44ee127461a2" + * @dev DEPRECATED */ event RotatedGuardianKey(address guardian, address guardianEnclave, bytes pubKey); /** * @notice Emitted when the mrenclave value is changed * @dev Signature "0x1ff2c57ef9a384cea0c482d61fec8d708967d266f03266e301c6786f7209904a" + * @dev DEPRECATED */ event MrEnclaveChanged(bytes32 oldMrEnclave, bytes32 newMrEnclave); /** * @notice Emitted when the mrsigner value is changed * @dev Signature "0x1a1fe271c5533136fccd1c6df515ca1f227d95822bfe78b9dd93debf3d709ae6" + * @dev DEPRECATED */ event MrSignerChanged(bytes32 oldMrSigner, bytes32 newMrSigner); /** * @notice Returns the enclave address registered to `guardian` + * @dev DEPRECATED */ function getGuardiansEnclaveAddress(address guardian) external view returns (address); @@ -95,6 +101,7 @@ interface IGuardianModule { /** * @notice Sets the values for mrEnclave and mrSigner to `newMrenclave` and `newMrsigner` + * @dev DEPRECATED */ function setGuardianEnclaveMeasurements(bytes32 newMrenclave, bytes32 newMrsigner) external; @@ -109,6 +116,7 @@ interface IGuardianModule { /** * @notice Returns the enclave verifier + * @dev DEPRECATED */ function ENCLAVE_VERIFIER() external view returns (IEnclaveVerifier); @@ -118,16 +126,19 @@ interface IGuardianModule { * The order of the signatures MUST the same as the order of the validators in the validator module * @param validatorInfos The information of the stopped validators * @param guardianEOASignatures The guardian EOA signatures + * @param deadline The deadline for the signature */ function validateBatchWithdrawals( StoppedValidatorInfo[] calldata validatorInfos, - bytes[] calldata guardianEOASignatures + bytes[] calldata guardianEOASignatures, + uint256 deadline ) external; /** * @notice Validates the node provisioning calldata * @dev The order of the signatures is important * The order of the signatures MUST the same as the order of the guardians in the guardian module + * @dev DEPRECATED * @param pufferModuleIndex is the validator index in Puffer * @param pubKey The public key * @param signature The signature @@ -154,6 +165,20 @@ interface IGuardianModule { external view; + /** + * @notice Validates the withdrawal request + * @param eoaSignatures The guardian EOA signatures + * @param messageHash The message hash + */ + function validateWithdrawalRequest(bytes[] calldata eoaSignatures, bytes32 messageHash) external view; + + /** + * @notice Validates the total epochs validated + * @param eoaSignatures The guardian EOA signatures + * @param messageHash The message hash + */ + function validateTotalEpochsValidated(bytes[] calldata eoaSignatures, bytes32 messageHash) external view; + /** * @notice Returns the threshold value for guardian signatures * @dev The threshold value is the minimum number of guardian signatures required for a transaction to be considered valid @@ -198,6 +223,7 @@ interface IGuardianModule { /** * @dev Validates the signatures of the guardians' enclave signatures + * @dev DEPRECATED * @param enclaveSignatures The array of enclave signatures * @param signedMessageHash The hash of the signed message * @return A boolean indicating whether the signatures are valid @@ -221,6 +247,7 @@ interface IGuardianModule { /** * @notice Rotates guardian's key * @dev If he caller is not a valid guardian or if the RAVE evidence is not valid the tx will revert + * @dev DEPRECATED * @param blockNumber is the block number * @param pubKey is the public key of the new signature * @param evidence is the RAVE evidence @@ -229,11 +256,13 @@ interface IGuardianModule { /** * @notice Returns the guardians enclave addresses + * @dev DEPRECATED */ function getGuardiansEnclaveAddresses() external view returns (address[] memory); /** * @notice Returns the guardians enclave public keys + * @dev DEPRECATED */ function getGuardiansEnclavePubkeys() external view returns (bytes[] memory); @@ -246,11 +275,13 @@ interface IGuardianModule { /** * @notice Returns the mrenclave value + * @dev DEPRECATED */ function getMrenclave() external view returns (bytes32); /** * @notice Returns the mrsigner value + * @dev DEPRECATED */ function getMrsigner() external view returns (bytes32); } diff --git a/mainnet-contracts/src/interface/IPufferModuleManager.sol b/mainnet-contracts/src/interface/IPufferModuleManager.sol index fa5b754f..146e99ce 100644 --- a/mainnet-contracts/src/interface/IPufferModuleManager.sol +++ b/mainnet-contracts/src/interface/IPufferModuleManager.sol @@ -14,6 +14,16 @@ interface IPufferModuleManager { */ error ForbiddenModuleName(); + /** + * @notice Thrown if the input array length mismatch + */ + error InputArrayLengthMismatch(); + + /** + * @notice Thrown if the input array length is zero + */ + error InputArrayLengthZero(); + /** * @notice Emitted when the Custom Call from the restakingOperator is successful * @dev Signature "0x80b240e4b7a31d61bdee28b97592a7c0ad486cb27d11ee5c6b90530db4e949ff" @@ -73,6 +83,23 @@ interface IPufferModuleManager { */ event PufferModuleUndelegated(bytes32 indexed moduleName); + /** + * @notice Emitted when validators of a module are upgraded to consolidating (0x02) + * @param moduleName the module name + * @param pubkeys the pubkeys of the validators to uppgrade + * @dev Signature "0x591863087d102c41b3f4d214fefc262505274cf32ef4e08ef20184140796614a" + */ + event PufferModuleUpgradedToConsolidating(bytes32 indexed moduleName, bytes[] pubkeys); + + /** + * @notice Emitted when a withdrawal is requested + * @param moduleName the module name to be undelegated + * @param pubkeys the pubkeys of the validators to exit + * @param gweiAmounts the amounts of the validators to exit, in Gwei + * @dev Signature "0x8de190edd50136636ef6acd43f071508e34713f6dbaf117f74cc92322e32a387" + */ + event WithdrawalRequested(bytes32 indexed moduleName, bytes[] pubkeys, uint64[] gweiAmounts); + /** * @notice Emitted when the restaking operator avs signature proof is updated * @param restakingOperator is the address of the restaking operator diff --git a/mainnet-contracts/src/interface/IPufferProtocol.sol b/mainnet-contracts/src/interface/IPufferProtocol.sol index f6a87a69..3b20287f 100644 --- a/mainnet-contracts/src/interface/IPufferProtocol.sol +++ b/mainnet-contracts/src/interface/IPufferProtocol.sol @@ -8,11 +8,13 @@ import { PufferModuleManager } from "../PufferModuleManager.sol"; import { PufferVaultV5 } from "../PufferVaultV5.sol"; import { IPufferOracleV2 } from "../interface/IPufferOracleV2.sol"; import { Status } from "../struct/Status.sol"; +import { WithdrawalType } from "../struct/WithdrawalType.sol"; import { Permit } from "../structs/Permit.sol"; import { ValidatorTicket } from "../ValidatorTicket.sol"; import { NodeInfo } from "../struct/NodeInfo.sol"; import { ModuleLimit } from "../struct/ProtocolStorage.sol"; import { StoppedValidatorInfo } from "../struct/StoppedValidatorInfo.sol"; +import { EpochsValidatedSignature } from "../struct/Signatures.sol"; import { IBeaconDepositContract } from "../interface/IBeaconDepositContract.sol"; /** @@ -21,162 +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 number of BLS public key shares doesn't match guardians threshold number - * @dev Signature "0x8cdea6a6" - */ - error InvalidBLSPublicKeySet(); - - /** - * @notice Thrown when the node operator tries to withdraw VTs from the PufferProtocol but has active/pending validators - * @dev Signature "0x22242546" - */ - 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 number of BLS private key shares doesn't match guardians number - * @dev Signature "0x2c8f9aa3" - */ - error InvalidBLSPrivateKeyShares(); - - /** - * @notice Thrown when the BLS public key is not valid - * @dev Signature "0x7eef7967" - */ - 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 Emitted when the number of active validators changes - * @dev Signature "0xc06afc2b3c88873a9be580de9bbbcc7fea3027ef0c25fd75d5411ed3195abcec" - */ - event NumberOfRegisteredValidatorsChanged(bytes32 indexed moduleName, uint256 newNumberOfRegisteredValidators); - - /** - * @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 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 usingEnclave is indicating if the validator is using secure enclave - * @dev Signature "0xc73344cf227e056eee8d82aee54078c9b55323b61d17f61587eb570873f8e319" - */ - event ValidatorKeyRegistered( - bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName, bool usingEnclave - ); - - /** - * @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 - * @dev Signature "0xf435da9e3aeccc40d39fece7829f9941965ceee00d31fa7a89d608a273ea906e" - */ - event ValidatorExited( - bytes pubKey, - uint256 indexed pufferModuleIndex, - bytes32 indexed moduleName, - uint256 pufETHBurnAmount, - uint256 vtBurnAmount - ); - - /** - * @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 - * @dev Signature "0x96cbbd073e24b0a7d0cab7dc347c239e52be23c1b44ce240b3b929821fed19a4" - */ - event SuccessfullyProvisioned(bytes pubKey, uint256 indexed pufferModuleIndex, bytes32 indexed moduleName); - /** * @notice Returns validator information * @param moduleName is the staking Module @@ -188,6 +34,7 @@ interface IPufferProtocol { /** * @notice Returns Penalty for submitting a bad validator registration * @dev If the guardians skip a validator, the node operator will be penalized + * @return Number of epochs to burn for a penalty if a validator is skipped. epochs * vtPricePerEpoch = penalty in ETH * /// todo write any possible reasons for skipping a validator, here and in skipValidator method */ function getVTPenalty() external view returns (uint256); @@ -201,36 +48,45 @@ interface IPufferProtocol { /** * @notice Deposits Validator Tickets for the `node` + * DEPRECATED - This method is deprecated and will be removed in the future upgrade */ - function depositValidatorTickets(Permit calldata permit, address node) external; + function depositValidatorTickets(address node, uint256 vtAmount) external; /** * @notice Withdraws the `amount` of Validator Tickers from the `msg.sender` to the `recipient` + * DEPRECATED - This method is deprecated and will be removed in the future upgrade * @dev Each active validator requires node operator to have at least `minimumVtAmount` locked */ function withdrawValidatorTickets(uint96 amount, address recipient) external; /** - * @notice Batch settling of validator withdrawals - * - * @notice Settles a validator withdrawal - * @dev This is one of the most important methods in the protocol - * It has multiple tasks: - * 1. Burn the pufETH from the node operator (if the withdrawal amount was lower than 32 ETH) - * 2. Burn the Validator Tickets from the node operator - * 3. Transfer withdrawal ETH from the PufferModule of the Validator to the PufferVault - * 4. Decrement the `lockedETHAmount` on the PufferOracle to reflect the new amount of locked ETH - */ - function batchHandleWithdrawals( - StoppedValidatorInfo[] calldata validatorInfos, - bytes[] calldata guardianEOASignatures - ) external; - - /** - * @notice Skips the next validator for `moduleName` - * @dev Restricted to Guardians - */ - function skipProvisioning(bytes32 moduleName, bytes[] calldata guardianEOASignatures) external; + * @notice Requests a withdrawal for the given validators. This withdrawal can be total or partial. + * If the amount is 0, the withdrawal is total and the validator will be fully exited. + * If it is a partial withdrawal, the validator should not be below 32 ETH or the request will be ignored. + * @param moduleName The name of the module + * @param indices The indices of the validators to withdraw + * @param gweiAmounts The amounts of the validators to withdraw, in Gwei + * @param withdrawalType The type of withdrawal + * @param validatorAmountsSignatures The signatures of the guardians to validate the amount of the validators to withdraw + * @param deadline The deadline for the signatures + * @dev The pubkeys should be active validators on the same module + * @dev There are 3 types of withdrawal: + * EXIT_VALIDATOR: The validator is fully exited. The gweiAmount needs to be 0 + * DOWNSIZE: The number of batches of the validator is reduced. The gweiAmount needs to be exactly a multiple of a batch size (32 ETH in gwei) + * And the validator should have more than the requested number of batches + * WITHDRAW_REWARDS: The amount cannot be higher than what the protocol provisioned for the validator and must be validated by the guardians via the `validatorAmountsSignatures` + * @dev The validatorAmountsSignatures is only needed when the withdrawal type is DOWNSIZE orWITHDRAW_REWARDS + * @dev According to EIP-7002 there is a fee for each validator withdrawal request (See https://eips.ethereum.org/assets/eip-7002/fee_analysis) + * The fee is paid in the msg.value of this function. Since the fee is not fixed and might change, the excess amount will be kept in the PufferModule + */ + function requestWithdrawal( + bytes32 moduleName, + uint256[] calldata indices, + uint64[] calldata gweiAmounts, + WithdrawalType[] calldata withdrawalType, + bytes[][] calldata validatorAmountsSignatures, + uint256 deadline + ) external payable; /** * @notice Returns the guardian module @@ -239,6 +95,7 @@ interface IPufferProtocol { /** * @notice Returns the Validator ticket ERC20 token + * DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function VALIDATOR_TICKET() external view returns (ValidatorTicket); @@ -262,6 +119,11 @@ interface IPufferProtocol { */ function BEACON_DEPOSIT_CONTRACT() external view returns (IBeaconDepositContract); + /** + * @notice Returns the Puffer Revenue Distributor + */ + function PUFFER_REVENUE_DISTRIBUTOR() external view returns (address payable); + /** * @notice Returns the current module weights */ @@ -278,14 +140,12 @@ interface IPufferProtocol { function getModuleAddress(bytes32 moduleName) external view returns (address); /** - * @notice Provisions the next node that is in line for provisioning if the `guardianEnclaveSignatures` are valid + * @notice Provisions the next node that is in line for provisioning + * @param validatorSignature The signature of the validator to provision + * @param depositRootHash The deposit root hash of the validator * @dev You can check who is next for provisioning by calling `getNextValidatorToProvision` method */ - function provisionNode( - bytes[] calldata guardianEnclaveSignatures, - bytes calldata validatorSignature, - bytes32 depositRootHash - ) external; + function provisionNode(bytes calldata validatorSignature, bytes32 depositRootHash) external; /** * @notice Returns the deposit_data_root @@ -318,25 +178,6 @@ interface IPufferProtocol { */ function createPufferModule(bytes32 moduleName) external returns (address); - /** - * @notice Registers a new validator key in a `moduleName` queue with a permit - * @dev There is a queue per moduleName and it is FIFO - * - * If you are depositing without the permit, make sure to .approve pufETH to PufferProtocol - * and populate permit.amount with the correct amount - * - * @param data The validator key data - * @param moduleName The name of the module - * @param pufETHPermit The permit for the pufETH - * @param vtPermit The permit for the ValidatorTicket - */ - function registerValidatorKey( - ValidatorKeyData calldata data, - bytes32 moduleName, - Permit calldata pufETHPermit, - Permit calldata vtPermit - ) external payable; - /** * @notice Returns the pending validator index for `moduleName` */ @@ -351,6 +192,7 @@ interface IPufferProtocol { * @notice Returns the amount of Validator Tickets locked in the PufferProtocol for the `owner` * The real VT balance may be different from the balance in the PufferProtocol * When the Validator is exited, the VTs are burned and the balance is decreased + * DEPRECATED - This method is deprecated and will be removed in the future upgrade */ function getValidatorTicketsBalance(address owner) external returns (uint256); @@ -367,10 +209,20 @@ interface IPufferProtocol { function getWithdrawalCredentials(address module) external view returns (bytes memory); /** - * @notice Returns the minimum amount of Validator Tokens to run a validator + * @notice Returns the minimum amount of Epochs a validator needs to run */ 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/NodeInfo.sol b/mainnet-contracts/src/struct/NodeInfo.sol index c225c188..da45429a 100644 --- a/mainnet-contracts/src/struct/NodeInfo.sol +++ b/mainnet-contracts/src/struct/NodeInfo.sol @@ -7,5 +7,11 @@ pragma solidity >=0.8.0 <0.9.0; struct NodeInfo { uint64 activeValidatorCount; // Number of active validators uint64 pendingValidatorCount; // Number of pending validators (registered but not yet provisioned) - uint96 vtBalance; // Validator ticket balance + uint96 deprecated_vtBalance; // Validator ticket balance + // @dev The node operators deposit ETH, and that ETH is used to calculate the validation time for the node + uint256 validationTime; + uint256 epochPrice; + uint256 totalEpochsValidated; + uint8 numBatches; // Number of batches + // @todo: Adapt with VT rework to fit a single slot } diff --git a/mainnet-contracts/src/struct/ProtocolStorage.sol b/mainnet-contracts/src/struct/ProtocolStorage.sol index c87d18e2..a1671f09 100644 --- a/mainnet-contracts/src/struct/ProtocolStorage.sol +++ b/mainnet-contracts/src/struct/ProtocolStorage.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.0 <0.9.0; import { Validator } from "../struct/Validator.sol"; import { NodeInfo } from "../struct/NodeInfo.sol"; import { PufferModule } from "../PufferModule.sol"; + /** * @custom:storage-location erc7201:PufferProtocol.storage * @dev +-----------------------------------------------------------+ @@ -12,7 +13,6 @@ import { PufferModule } from "../PufferModule.sol"; * | | * +-----------------------------------------------------------+ */ - struct ProtocolStorage { /** * @dev Module weights @@ -62,11 +62,15 @@ struct ProtocolStorage { */ uint256 minimumVtAmount; /** - * @dev Amount of VT tokens to burn for a validator penalty - * 1 VT = 1e18 + * @dev Amount of epochs to burn for a penalty if a validator is skipped * Slot 9 */ - uint256 vtPenalty; + uint256 vtPenaltyEpochs; + /** + * @dev Address of the PufferProtocolLogic contract + * Slot 10 + */ + address pufferProtocolLogic; } struct ModuleLimit { diff --git a/mainnet-contracts/src/struct/Signatures.sol b/mainnet-contracts/src/struct/Signatures.sol new file mode 100644 index 00000000..fe343b07 --- /dev/null +++ b/mainnet-contracts/src/struct/Signatures.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +struct EpochsValidatedSignature { + bytes32 functionSelector; + uint256 totalEpochsValidated; + address nodeOperator; + uint256 deadline; + bytes[] signatures; +} diff --git a/mainnet-contracts/src/struct/StoppedValidatorInfo.sol b/mainnet-contracts/src/struct/StoppedValidatorInfo.sol index dd1091a1..5766936e 100644 --- a/mainnet-contracts/src/struct/StoppedValidatorInfo.sol +++ b/mainnet-contracts/src/struct/StoppedValidatorInfo.sol @@ -7,10 +7,6 @@ pragma solidity >=0.8.0 <0.9.0; struct StoppedValidatorInfo { ///@dev Module address. address module; - ///@dev Validator start epoch. - uint256 startEpoch; - ///@dev Validator stop epoch. - uint256 endEpoch; /// @dev Indicates whether the validator was slashed before stopping. bool wasSlashed; /// @dev Name of the module where the validator was participating. @@ -19,4 +15,10 @@ struct StoppedValidatorInfo { uint256 pufferModuleIndex; /// @dev Amount of funds withdrawn upon validator stoppage. uint256 withdrawalAmount; + /// @dev Total number of epochs validated by the node operator. + uint256 totalEpochsValidated; + /// @dev The signature of the guardians to validate the number of epochs validated. + bytes[] vtConsumptionSignature; + /// @dev Indicates whether the validator was downsized instead of exited + bool isDownsize; } diff --git a/mainnet-contracts/src/struct/Validator.sol b/mainnet-contracts/src/struct/Validator.sol index f1bddf25..03aefc0d 100644 --- a/mainnet-contracts/src/struct/Validator.sol +++ b/mainnet-contracts/src/struct/Validator.sol @@ -12,4 +12,5 @@ struct Validator { address module; // In which module is the Validator participating Status status; // Validator status bytes pubKey; // Validator public key + uint8 numBatches; // Number of batches } diff --git a/mainnet-contracts/src/struct/ValidatorKeyData.sol b/mainnet-contracts/src/struct/ValidatorKeyData.sol index 78512fbd..b7765484 100644 --- a/mainnet-contracts/src/struct/ValidatorKeyData.sol +++ b/mainnet-contracts/src/struct/ValidatorKeyData.sol @@ -8,7 +8,5 @@ struct ValidatorKeyData { bytes blsPubKey; bytes signature; bytes32 depositDataRoot; - bytes[] blsEncryptedPrivKeyShares; - bytes blsPubKeySet; - bytes raveEvidence; + uint8 numBatches; } diff --git a/mainnet-contracts/src/struct/WithdrawalType.sol b/mainnet-contracts/src/struct/WithdrawalType.sol new file mode 100644 index 00000000..1af0fcff --- /dev/null +++ b/mainnet-contracts/src/struct/WithdrawalType.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * @dev WithdrawalType + */ +enum WithdrawalType { + EXIT_VALIDATOR, + DOWNSIZE, + WITHDRAW_REWARDS +} diff --git a/mainnet-contracts/test/handlers/PufferProtocolHandler.sol b/mainnet-contracts/test/handlers/PufferProtocolHandler.sol index 5c9cc944..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); @@ -499,10 +500,7 @@ contract PufferProtocolHandler is Test { ProvisioningData memory validatorData = _validatorQueue[moduleName][nextIdx]; if (validatorData.status == Status.PENDING) { - bytes memory sig = _getPubKey(validatorData.pubKeypart); - - bytes[] memory signatures = _getGuardianSignatures(sig); - pufferProtocol.provisionNode(signatures, mockValidatorSignature, bytes32(0)); + pufferProtocol.provisionNode(mockValidatorSignature, bytes32(0)); ghost_validators_validating.push(ProvisionedValidator({ moduleName: moduleName, idx: nextIdx })); @@ -552,10 +550,8 @@ contract PufferProtocolHandler is Test { signature: mockValidatorSignature, withdrawalCredentials: withdrawalCredentials }), - blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48), - raveEvidence: new bytes(1) // Guardians are checking it off chain - }); + numBatches: 1 + }); return validatorData; } @@ -587,57 +583,14 @@ contract PufferProtocolHandler is Test { uint256 bond = 1 ether; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorKeyRegistered(pubKey, idx, moduleName, true); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( - validatorKeyData, moduleName, emptyPermit, emptyPermit + validatorKeyData, moduleName, 0, new bytes[](0), block.timestamp + 1 days ); return (smoothingCommitment + bond); } - // Copied from PufferProtocol.t.sol - function _getGuardianSignatures(bytes memory pubKey) internal view returns (bytes[] memory) { - (bytes32 moduleName, uint256 pendingIdx) = pufferProtocol.getNextValidatorToProvision(); - Validator memory validator = pufferProtocol.getValidatorInfo(moduleName, pendingIdx); - // If there is no module return empty byte array - if (validator.module == address(0)) { - return new bytes[](0); - } - bytes memory withdrawalCredentials = pufferProtocol.getWithdrawalCredentials(validator.module); - - bytes32 digest = LibGuardianMessages._getBeaconDepositMessageToBeSigned( - pendingIdx, - pubKey, - mockValidatorSignature, - withdrawalCredentials, - pufferProtocol.getDepositDataRoot({ - pubKey: pubKey, - signature: mockValidatorSignature, - withdrawalCredentials: withdrawalCredentials - }) - ); - - return _getGuardianEnclaveSignatures(digest); - } - - function _getGuardianEnclaveSignatures(bytes32 digest) internal view returns (bytes[] memory) { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SKEnclave, digest); - bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - (v, r, s) = vm.sign(guardian2SKEnclave, digest); - bytes memory signature2 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - (v, r, s) = vm.sign(guardian3SKEnclave, digest); - bytes memory signature3 = abi.encodePacked(r, s, v); // note the order here is different from line above. - - bytes[] memory guardianSignatures = new bytes[](3); - guardianSignatures[0] = signature1; - guardianSignatures[1] = signature2; - guardianSignatures[2] = signature3; - - return guardianSignatures; - } - function _getGuardianEOASignatures(bytes32 digest) internal returns (bytes[] memory) { // Create Guardian wallets (, uint256 guardian1SK) = makeAddrAndKey("guardian1"); diff --git a/mainnet-contracts/test/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/mocks/BeaconMock.sol b/mainnet-contracts/test/mocks/BeaconMock.sol index 2bd35a48..eb4210dd 100644 --- a/mainnet-contracts/test/mocks/BeaconMock.sol +++ b/mainnet-contracts/test/mocks/BeaconMock.sol @@ -4,12 +4,7 @@ pragma solidity >=0.8.0 <0.9.0; contract BeaconMock { event StartedStaking(); - error BadValue(); - function deposit(bytes calldata, bytes calldata, bytes calldata, bytes32) external payable { - if (msg.value != 32 ether) { - revert BadValue(); - } emit StartedStaking(); } diff --git a/mainnet-contracts/test/mocks/EigenPodManagerMock.sol b/mainnet-contracts/test/mocks/EigenPodManagerMock.sol index 67312086..054c9ef7 100644 --- a/mainnet-contracts/test/mocks/EigenPodManagerMock.sol +++ b/mainnet-contracts/test/mocks/EigenPodManagerMock.sol @@ -6,9 +6,20 @@ import "src/interface/Eigenlayer-Slashing/IEigenPodManager.sol"; import "src/interface/Eigenlayer-Slashing/IAllocationManager.sol"; contract EigenPodMock { + uint256 private constant WITHDRAWAL_FEE = 0.0001 ether; + + struct WithdrawalRequest { + bytes pubkey; + uint64 amountGwei; + } + function startCheckpoint(bool) external { } function setProofSubmitter(address) external { } + + function requestWithdrawal(WithdrawalRequest[] calldata requests) external payable { + payable(msg.sender).transfer(msg.value - requests.length * WITHDRAWAL_FEE); + } } contract EigenPodManagerMock is IEigenPodManager, Test { diff --git a/mainnet-contracts/test/mocks/MockPufferOracle.sol b/mainnet-contracts/test/mocks/MockPufferOracle.sol index 7ccc8042..4d4fb393 100644 --- a/mainnet-contracts/test/mocks/MockPufferOracle.sol +++ b/mainnet-contracts/test/mocks/MockPufferOracle.sol @@ -30,6 +30,7 @@ contract MockPufferOracle is IPufferOracleV2 { } function provisionNode() external { } + function exitValidators(uint256) external { } function getValidatorTicketPrice() external view returns (uint256 pricePerVT) { } diff --git a/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol b/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol index 94e11a2b..df62b172 100644 --- a/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol +++ b/mainnet-contracts/test/mocks/PufferProtocolMockUpgrade.sol @@ -19,7 +19,8 @@ contract PufferProtocolMockUpgrade is PufferProtocol { address(0), ValidatorTicket(address(0)), IPufferOracleV2(address(0)), - address(0) + address(0), + payable(address(0)) ) { } } diff --git a/mainnet-contracts/test/unit/PufferModuleManager.t.sol b/mainnet-contracts/test/unit/PufferModuleManager.t.sol index afd284a2..e2e64023 100644 --- a/mainnet-contracts/test/unit/PufferModuleManager.t.sol +++ b/mainnet-contracts/test/unit/PufferModuleManager.t.sol @@ -5,12 +5,13 @@ import { UnitTestHelper } from "../helpers/UnitTestHelper.sol"; import { PufferModule } from "../../src/PufferModule.sol"; import { PufferProtocol } from "../../src/PufferProtocol.sol"; import { IPufferModuleManager } from "../../src/interface/IPufferModuleManager.sol"; +import { PufferModuleManager } from "../../src/PufferModuleManager.sol"; import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { Merkle } from "murky/Merkle.sol"; import { ISignatureUtils } from "src/interface/Eigenlayer-Slashing/ISignatureUtils.sol"; import { Unauthorized } from "../../src/Errors.sol"; -import { ROLE_ID_OPERATIONS_PAYMASTER } from "../../script/Roles.sol"; +import { ROLE_ID_OPERATIONS_PAYMASTER, ROLE_ID_VALIDATOR_EXITOR } from "../../script/Roles.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { IDelegationManager } from "src/interface/Eigenlayer-Slashing/IDelegationManager.sol"; import { IDelegationManagerTypes } from "src/interface/Eigenlayer-Slashing/IDelegationManager.sol"; @@ -22,6 +23,8 @@ import { IAllocationManagerTypes } from "src/interface/Eigenlayer-Slashing/IAllo import { IAllocationManager } from "src/interface/Eigenlayer-Slashing/IAllocationManager.sol"; import { IRewardsCoordinator } from "src/interface/Eigenlayer-Slashing/IRewardsCoordinator.sol"; import { InvalidAddress } from "../../src/Errors.sol"; +import { IAccessManaged } from "@openzeppelin/contracts/access/manager/IAccessManaged.sol"; +import { console } from "forge-std/console.sol"; contract PufferModuleUpgrade { function getMagicValue() external pure returns (uint256) { @@ -37,15 +40,28 @@ contract PufferModuleManagerTest is UnitTestHelper { bytes32 CRAZY_GAINS = bytes32("CRAZY_GAINS"); + bytes32 MOCK_MODULE = bytes32("MOCK_MODULE"); + + address validatorExitor = makeAddr("validatorExitor"); + + uint256 EXIT_FEE = 0.0001 ether; + function setUp() public override { super.setUp(); vm.deal(address(this), 1000 ether); + vm.deal(validatorExitor, 3 ether); + bytes memory cd = new GenerateSlashingELCalldata().run(address(pufferModuleManager)); vm.startPrank(timelock); accessManager.grantRole(ROLE_ID_OPERATIONS_PAYMASTER, address(this), 0); + accessManager.grantRole(ROLE_ID_VALIDATOR_EXITOR, validatorExitor, 0); + bytes4[] memory selectors = new bytes4[](1); + selectors[0] = PufferModuleManager.requestWithdrawal.selector; + accessManager.setTargetFunctionRole(address(pufferModuleManager), selectors, ROLE_ID_VALIDATOR_EXITOR); + (bool success,) = address(accessManager).call(cd); assertTrue(success, "should succeed"); @@ -86,16 +102,6 @@ contract PufferModuleManagerTest is UnitTestHelper { assertEq(PufferModule(payable(module)).NAME(), moduleName, "bad name"); } - function test_pufferModuleAuthorization(bytes32 moduleName) public { - address module = _createPufferModule(moduleName); - - vm.expectRevert(Unauthorized.selector); - PufferModule(payable(module)).callStake("", "", ""); - - vm.expectRevert(Unauthorized.selector); - PufferModule(payable(module)).call(address(0), 0, ""); - } - function test_registerOperatorToAVS() public { vm.startPrank(DAO); RestakingOperator operator = _createRestakingOperator(); @@ -335,6 +341,151 @@ contract PufferModuleManagerTest is UnitTestHelper { vm.stopPrank(); } + function test_requestWithdrawalExactFee1() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + uint64[] memory gweiAmounts = new uint64[](1); + + vm.startPrank(validatorExitor); + + vm.expectEmit(true, true, true, true); + emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); + + pufferModuleManager.requestWithdrawal{ value: EXIT_FEE }(MOCK_MODULE, pubkeys, gweiAmounts); + vm.stopPrank(); + } + + function test_requestWithdrawalExactFee2() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](2); + pubkeys[0] = bytes("0x1234"); + pubkeys[1] = bytes("0x4321"); + uint64[] memory gweiAmounts = new uint64[](2); + + vm.startPrank(validatorExitor); + + vm.expectEmit(true, true, true, true); + emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); + + pufferModuleManager.requestWithdrawal{ value: 2 * EXIT_FEE }(MOCK_MODULE, pubkeys, gweiAmounts); + vm.stopPrank(); + } + + function test_requestWithdrawalExcessFee() public { + address moduleAddress = _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + uint64[] memory gweiAmounts = new uint64[](1); + + vm.startPrank(validatorExitor); + + uint256 initialBalance = moduleAddress.balance; + + vm.expectEmit(true, true, true, true); + emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); + + pufferModuleManager.requestWithdrawal{ value: 1 ether }(MOCK_MODULE, pubkeys, gweiAmounts); + + // Calculate expected balance: initial + amount sent - fee + uint256 expectedBalance = initialBalance + 1 ether - EXIT_FEE; + + // Verify the balance change accounting for gas + assertEq(moduleAddress.balance, expectedBalance, "Module should get the fee back minus gas costs"); + + vm.stopPrank(); + } + + function test_requestWithdrawalExcessFee2() public { + address moduleAddress = _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](2); + pubkeys[0] = bytes("0x1234"); + pubkeys[1] = bytes("0x4321"); + uint64[] memory gweiAmounts = new uint64[](2); + + vm.startPrank(validatorExitor); + + uint256 initialBalance = moduleAddress.balance; + + vm.expectEmit(true, true, true, true); + emit IPufferModuleManager.WithdrawalRequested(MOCK_MODULE, pubkeys, gweiAmounts); + + pufferModuleManager.requestWithdrawal{ value: 1 ether }(MOCK_MODULE, pubkeys, gweiAmounts); + + // Calculate expected balance: initial + amount sent - fee + uint256 expectedBalance = initialBalance + 1 ether - 2 * EXIT_FEE; + + // Verify the balance change accounting for gas + assertEq(moduleAddress.balance, expectedBalance, "Module should get the fee back minus gas costs"); + + vm.stopPrank(); + } + + function test_requestWithdrawalNoFee() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + uint64[] memory gweiAmounts = new uint64[](1); + + vm.startPrank(validatorExitor); + + vm.expectRevert(); // panic underflow when subtracting fee + pufferModuleManager.requestWithdrawal(MOCK_MODULE, pubkeys, gweiAmounts); + vm.stopPrank(); + } + + function test_requestWithdrawalUnauthorized() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + uint64[] memory gweiAmounts = new uint64[](1); + + vm.startPrank(bob); + + vm.expectRevert(abi.encodeWithSelector(IAccessManaged.AccessManagedUnauthorized.selector, bob)); + pufferModuleManager.requestWithdrawal(MOCK_MODULE, pubkeys, gweiAmounts); + + vm.stopPrank(); + } + + function test_requestWithdrawalInputArrayLengthMismatch() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](1); + pubkeys[0] = bytes("0x1234"); + + uint64[] memory gweiAmounts = new uint64[](2); + gweiAmounts[0] = 1 ether; + gweiAmounts[1] = 2 ether; + + vm.startPrank(validatorExitor); + + vm.expectRevert(abi.encodeWithSelector(IPufferModuleManager.InputArrayLengthMismatch.selector)); + pufferModuleManager.requestWithdrawal(MOCK_MODULE, pubkeys, gweiAmounts); + + vm.stopPrank(); + } + + function test_requestWithdrawalInputArrayLengthZero() public { + _createPufferModule(MOCK_MODULE); + + bytes[] memory pubkeys = new bytes[](0); + uint64[] memory gweiAmounts = new uint64[](0); + + vm.startPrank(validatorExitor); + + vm.expectRevert(abi.encodeWithSelector(IPufferModuleManager.InputArrayLengthZero.selector)); + pufferModuleManager.requestWithdrawal(MOCK_MODULE, pubkeys, gweiAmounts); + + vm.stopPrank(); + } + function _createPufferModule(bytes32 moduleName) internal returns (address module) { vm.assume(pufferProtocol.getModuleAddress(moduleName) == address(0)); vm.assume(bytes32("NO_VALIDATORS") != moduleName); diff --git a/mainnet-contracts/test/unit/PufferProtocol.t.sol b/mainnet-contracts/test/unit/PufferProtocol.t.sol index bff105a3..3e88c036 100644 --- a/mainnet-contracts/test/unit/PufferProtocol.t.sol +++ b/mainnet-contracts/test/unit/PufferProtocol.t.sol @@ -4,25 +4,51 @@ 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 { ROLE_ID_DAO, ROLE_ID_OPERATIONS_PAYMASTER, ROLE_ID_OPERATIONS_MULTISIG } from "../../script/Roles.sol"; -import { Unauthorized } from "../../src/Errors.sol"; +import { PufferRevenueDepositor } from "../../src/PufferRevenueDepositor.sol"; +import { + ROLE_ID_DAO, + ROLE_ID_OPERATIONS_PAYMASTER, + ROLE_ID_OPERATIONS_MULTISIG, + ROLE_ID_OPERATIONS_COORDINATOR, + ROLE_ID_REVENUE_DEPOSITOR +} from "../../script/Roles.sol"; import { LibGuardianMessages } from "../../src/LibGuardianMessages.sol"; -import { Permit } from "../../src/structs/Permit.sol"; import { ModuleLimit } from "../../src/struct/ProtocolStorage.sol"; import { StoppedValidatorInfo } from "../../src/struct/StoppedValidatorInfo.sol"; +import { NodeInfo } from "../../src/struct/NodeInfo.sol"; +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; - event ValidatorKeyRegistered(bytes pubKey, uint256 indexed, bytes32 indexed, bool); - event SuccessfullyProvisioned(bytes pubKey, uint256 indexed, bytes32 indexed); - event ModuleWeightsChanged(bytes32[] oldWeights, bytes32[] newWeights); + /** + * @dev New bond is reduced from 2 to 1.5 ETH + */ + uint256 BOND = 1.5 ether; + /** + * @dev Minimum validation time in epochs + * Roughly: 30 days * 225 epochs per day = 6750 epochs + */ + uint256 internal constant MINIMUM_EPOCHS_VALIDATION = 6750; + + // Eth has 225 epochs per day + uint256 internal constant EPOCHS_PER_DAY = 225; + + // 1 VT is burned per 225 epochs + uint256 internal constant BURN_RATE_PER_EPOCH = 4444444444444445; bytes zeroPubKey = new bytes(48); bytes32 zeroPubKeyPart; @@ -31,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 % @@ -52,6 +76,8 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(address(this), 1000 ether); + vm.label(address(revenueDepositor), "RevenueDepositorProxy"); + // Setup roles bytes4[] memory selectors = new bytes4[](3); selectors[0] = PufferProtocol.createPufferModule.selector; @@ -65,8 +91,19 @@ contract PufferProtocolTest is UnitTestHelper { accessManager.grantRole(ROLE_ID_OPERATIONS_MULTISIG, address(this), 0); accessManager.grantRole(ROLE_ID_OPERATIONS_PAYMASTER, address(this), 0); accessManager.grantRole(ROLE_ID_OPERATIONS_MULTISIG, address(this), 0); + accessManager.grantRole(ROLE_ID_OPERATIONS_COORDINATOR, address(this), 0); + + // Grant revenue depositor roles to this contract for simplicity + bytes4[] memory revenueDepositorRole = new bytes4[](2); + revenueDepositorRole[0] = PufferRevenueDepositor.depositRevenue.selector; + revenueDepositorRole[1] = PufferRevenueDepositor.setRewardsDistributionWindow.selector; + accessManager.setTargetFunctionRole(address(revenueDepositor), revenueDepositorRole, ROLE_ID_REVENUE_DEPOSITOR); + accessManager.grantRole(ROLE_ID_REVENUE_DEPOSITOR, address(this), 0); + vm.stopPrank(); + revenueDepositor.setRewardsDistributionWindow(0); + _skipDefaultFuzzAddresses(); fuzzedAddressMapping[address(pufferProtocol)] = true; @@ -79,13 +116,46 @@ contract PufferProtocolTest is UnitTestHelper { // Setup function test_setup() public view { assertTrue(address(pufferProtocol.PUFFER_VAULT()) != address(0), "puffer vault address"); + assertTrue( + address(pufferProtocol.PUFFER_REVENUE_DISTRIBUTOR()) != address(0), "puffer revenue distributor address" + ); address module = pufferProtocol.getModuleAddress(PUFFER_MODULE_0); assertEq(PufferModule(payable(module)).NAME(), PUFFER_MODULE_0, "bad name"); } + 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(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + + NodeInfo memory nodeInfo = pufferProtocol.getNodeInfo(address(this)); + assertEq(nodeInfo.activeValidatorCount, 0); + assertEq(nodeInfo.pendingValidatorCount, 1); + assertEq(nodeInfo.deprecated_vtBalance, 0); + assertEq(nodeInfo.validationTime, (30 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice())); // 30 days of VT + assertEq(nodeInfo.epochPrice, 9803921568628); // VT Price per epoch + assertEq(nodeInfo.totalEpochsValidated, 0); } // Empty queue should return NO_VALIDATORS @@ -97,8 +167,8 @@ contract PufferProtocolTest is UnitTestHelper { // Test Skipping the validator function test_skip_provisioning() public { - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("bob"), PUFFER_MODULE_0); + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("bob"), PUFFER_MODULE_0, 0); (bytes32 moduleName, uint256 idx) = pufferProtocol.getNextValidatorToProvision(); uint256 moduleSelectionIndex = pufferProtocol.getModuleSelectIndex(); @@ -114,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); @@ -132,11 +202,9 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(moduleName, PUFFER_MODULE_0, "module"); assertEq(idx, 1, "idx should be 1"); - bytes[] memory signatures = _getGuardianSignatures(_getPubKey(bytes32("bob"))); - vm.expectEmit(true, true, true, true); - emit SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + 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"); } @@ -144,36 +212,18 @@ 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); } - // Invalid pub key shares length - function test_register_invalid_pubkey_shares_length() public { - ValidatorKeyData memory data = _getMockValidatorKeyData(new bytes(48), PUFFER_MODULE_0); - data.blsPubKeySet = new bytes(22); // Invalid length - - vm.expectRevert(IPufferProtocol.InvalidBLSPublicKeySet.selector); - pufferProtocol.registerValidatorKey{ value: 4 ether }(data, PUFFER_MODULE_0, emptyPermit, emptyPermit); - } - - // Invalid private key shares length - function test_register_invalid_privKey_shares() public { - ValidatorKeyData memory data = _getMockValidatorKeyData(new bytes(48), PUFFER_MODULE_0); - data.blsEncryptedPrivKeyShares = new bytes[](2); // we have 3 guardians, and we try to give 2 priv key shares - - vm.expectRevert(IPufferProtocol.InvalidBLSPrivateKeyShares.selector); - pufferProtocol.registerValidatorKey{ value: 4 ether }(data, PUFFER_MODULE_0, emptyPermit, emptyPermit); - } - // Try registering with invalid module function test_register_to_invalid_module() public { uint256 smoothingCommitment = pufferOracle.getValidatorTicketPrice() * 30; 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"), emptyPermit, emptyPermit + validatorKeyData, bytes32("imaginary module"), 0, new bytes[](0), block.timestamp + 1 days ); } @@ -181,58 +231,29 @@ contract PufferProtocolTest is UnitTestHelper { function test_register_with_non_whole_amount() public { bytes memory pubKey = _getPubKey(bytes32("charlie")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); uint256 amount = 5.11 ether; pufferProtocol.registerValidatorKey{ value: amount }( - validatorKeyData, PUFFER_MODULE_0, emptyPermit, emptyPermit + validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); assertEq( - validatorTicket.balanceOf(address(pufferProtocol)), - ((amount - 1 ether) * 1 ether) / vtPrice, - "VT after for pufferProtocol" - ); - } - - // If we are > burst threshold, treasury gets everything - function test_burst_threshold() external { - vm.roll(50401); - - _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); - _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); - _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); - - pufferOracle.setTotalNumberOfValidators( - 5, - 99999999, - _getGuardianEOASignatures( - LibGuardianMessages._getSetNumberOfValidatorsMessage({ numberOfValidators: 5, epochNumber: 99999999 }) - ) + address(pufferProtocol).balance, + amount - 1.5 ether, + "protocol has the eth amount for VT, the bond is converted to pufETH" ); - - uint256 sc = pufferOracle.getValidatorTicketPrice() * 30; - address treasury = validatorTicket.TREASURY(); - - uint256 balanceBefore = address(treasury).balance; - - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - - uint256 balanceAfter = address(treasury).balance; - - assertEq(balanceAfter, balanceBefore + sc, "treasury gets everything"); } // Set validator limit and try registering that many validators function test_fuzz_register_many_validators(uint8 numberOfValidatorsToProvision) external { for (uint256 i = 0; i < uint256(numberOfValidatorsToProvision); ++i) { - vm.deal(address(this), 2 ether); - _registerValidatorKey(bytes32(i), PUFFER_MODULE_0); + vm.deal(address(this), 3 ether); + _registerValidatorKey(address(this), bytes32(i), PUFFER_MODULE_0, 0); } } - // Try registering without RAVE evidence - function test_register_no_sgx() public { + // Try registering with an invalid number of batches + function test_register_invalid_num_batches() public { uint256 vtPrice = pufferOracle.getValidatorTicketPrice() * 30; bytes memory pubKey = _getPubKey(bytes32("something")); @@ -248,15 +269,19 @@ contract PufferProtocolTest is UnitTestHelper { blsPubKey: pubKey, // key length must be 48 byte signature: new bytes(0), depositDataRoot: bytes32(""), - blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48), - raveEvidence: new bytes(0) // No rave - }); + numBatches: 0 + }); - vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, false); - pufferProtocol.registerValidatorKey{ value: vtPrice + 2 ether }( - validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit + 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(PufferProtocolBase.InvalidNumberOfBatches.selector); + pufferProtocol.registerValidatorKey{ value: vtPrice }( + validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); } @@ -275,53 +300,35 @@ contract PufferProtocolTest is UnitTestHelper { blsPubKey: hex"aeaa", // invalid key signature: new bytes(0), depositDataRoot: bytes32(""), - blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(144), - raveEvidence: new bytes(1) + numBatches: 1 }); - vm.expectRevert(IPufferProtocol.InvalidBLSPubKey.selector); + vm.expectRevert(PufferProtocolBase.InvalidBLSPubKey.selector); pufferProtocol.registerValidatorKey{ value: smoothingCommitment }( - validatorData, PUFFER_MODULE_0, emptyPermit, emptyPermit + validatorData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); } - function test_get_payload() public view { - (bytes[] memory guardianPubKeys,, uint256 threshold,) = pufferProtocol.getPayload(PUFFER_MODULE_0, false); - - assertEq(guardianPubKeys[0], guardian1EnclavePubKey, "guardian1"); - assertEq(guardianPubKeys[1], guardian2EnclavePubKey, "guardian2"); - assertEq(guardianPubKeys[2], guardian3EnclavePubKey, "guardian3"); - - assertEq(guardianPubKeys.length, 3, "pubkeys len"); - assertEq(threshold, 1, "threshold"); - } - // Try to provision a validator when there is nothing to provision function test_provision_reverts() public { (, uint256 idx) = pufferProtocol.getNextValidatorToProvision(); assertEq(type(uint256).max, idx, "module"); - // Invalid signatures - bytes[] memory signatures = - _getGuardianSignatures(hex"0000000000000000000000000000000000000000000000000000000000000000"); - vm.expectRevert(); // panic - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } // If the deposit root is not bytes(0), it must match match the one returned from the beacon contract function test_provision_bad_deposit_hash() public { - _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(address(this), zeroPubKeyPart, PUFFER_MODULE_0, 0); bytes memory validatorSignature = _validatorSignature(); - bytes[] memory guardianSignatures = _getGuardianSignatures(_getPubKey(zeroPubKeyPart)); - vm.expectRevert(IPufferProtocol.InvalidDepositRootHash.selector); - pufferProtocol.provisionNode(guardianSignatures, validatorSignature, bytes32("badDepositRoot")); // "depositRoot" is hardcoded in the mock + vm.expectRevert(PufferProtocolBase.InvalidDepositRootHash.selector); + pufferProtocol.provisionNode(validatorSignature, bytes32("badDepositRoot")); // "depositRoot" is hardcoded in the mock // now it works - pufferProtocol.provisionNode(guardianSignatures, validatorSignature, DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(validatorSignature, DEFAULT_DEPOSIT_ROOT); } function test_register_multiple_validators_and_skipProvisioning(bytes32 alicePubKeyPart, bytes32 bobPubKeyPart) @@ -334,7 +341,7 @@ contract PufferProtocolTest is UnitTestHelper { bytes memory bobPubKey = _getPubKey(bobPubKeyPart); // 1. validator - _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(address(this), zeroPubKeyPart, PUFFER_MODULE_0, 0); Validator memory validator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 0); assertTrue(validator.node == address(this), "node operator"); @@ -342,35 +349,31 @@ contract PufferProtocolTest is UnitTestHelper { // 2. validator vm.startPrank(bob); - _registerValidatorKey(bobPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(bob, bobPubKeyPart, PUFFER_MODULE_0, 0); vm.stopPrank(); // 3. validator vm.startPrank(alice); - _registerValidatorKey(alicePubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(alice, alicePubKeyPart, PUFFER_MODULE_0, 0); vm.stopPrank(); // 4. validator - _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(address(this), zeroPubKeyPart, PUFFER_MODULE_0, 0); // 5. Validator - _registerValidatorKey(zeroPubKeyPart, PUFFER_MODULE_0); + _registerValidatorKey(address(this), zeroPubKeyPart, PUFFER_MODULE_0, 0); assertEq(pufferProtocol.getPendingValidatorIndex(PUFFER_MODULE_0), 5, "next pending validator index"); - bytes[] memory signatures = _getGuardianSignatures(zeroPubKey); - // 1. provision zero key vm.expectEmit(true, true, true, true); - emit SuccessfullyProvisioned(zeroPubKey, 0, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); - - bytes[] memory bobSignatures = _getGuardianSignatures(bobPubKey); + 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 SuccessfullyProvisioned(bobPubKey, 1, PUFFER_MODULE_0); - pufferProtocol.provisionNode(bobSignatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + emit IPufferProtocolEvents.SuccessfullyProvisioned(bobPubKey, 1, PUFFER_MODULE_0, 1); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); Validator memory bobValidator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 1); @@ -378,10 +381,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); - signatures = _getGuardianSignatures(zeroPubKey); - - emit SuccessfullyProvisioned(zeroPubKey, 3, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + emit IPufferProtocolEvents.SuccessfullyProvisioned(zeroPubKey, 3, PUFFER_MODULE_0, 1); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Get validators Validator[] memory registeredValidators = pufferProtocol.getValidators(PUFFER_MODULE_0); @@ -409,30 +410,28 @@ contract PufferProtocolTest is UnitTestHelper { newWeights[3] = CRAZY_GAINS; vm.expectEmit(true, true, true, true); - emit ModuleWeightsChanged(oldWeights, newWeights); + emit IPufferProtocolEvents.ModuleWeightsChanged(oldWeights, newWeights); pufferProtocol.setModuleWeights(newWeights); vm.deal(address(pufferVault), 10000 ether); - _registerValidatorKey(bytes32("bob"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("charlie"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("david"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("emma"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("benjamin"), EIGEN_DA); - _registerValidatorKey(bytes32("rocky"), CRAZY_GAINS); + _registerValidatorKey(address(this), bytes32("bob"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("charlie"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("david"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("emma"), PUFFER_MODULE_0, 0); + _registerValidatorKey(address(this), bytes32("benjamin"), EIGEN_DA, 0); + _registerValidatorKey(address(this), bytes32("rocky"), CRAZY_GAINS, 0); (bytes32 nextModule, uint256 nextId) = pufferProtocol.getNextValidatorToProvision(); assertTrue(nextModule == PUFFER_MODULE_0, "module selection"); assertTrue(nextId == 0, "module selection"); - bytes[] memory signatures = _getGuardianSignatures(_getPubKey(bytes32("bob"))); - // Provision Bob that is not zero pubKey vm.expectEmit(true, true, true, true); - emit SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 0, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + emit IPufferProtocolEvents.SuccessfullyProvisioned(_getPubKey(bytes32("bob")), 0, PUFFER_MODULE_0, 1); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -440,11 +439,9 @@ contract PufferProtocolTest is UnitTestHelper { // Id is zero, because that is the first in this queue assertTrue(nextId == 0, "module id"); - signatures = _getGuardianSignatures(_getPubKey(bytes32("benjamin"))); - vm.expectEmit(true, true, true, true); - emit SuccessfullyProvisioned(_getPubKey(bytes32("benjamin")), 0, EIGEN_DA); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + emit IPufferProtocolEvents.SuccessfullyProvisioned(_getPubKey(bytes32("benjamin")), 0, EIGEN_DA, 1); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -455,7 +452,7 @@ contract PufferProtocolTest is UnitTestHelper { vm.stopPrank(); // Now jason registers to EIGEN_DA - _registerValidatorKey(bytes32("jason"), EIGEN_DA); + _registerValidatorKey(address(this), bytes32("jason"), EIGEN_DA, 0); // If we query next validator, it should switch back to EIGEN_DA (because of the weighted selection) (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -463,24 +460,15 @@ contract PufferProtocolTest is UnitTestHelper { assertTrue(nextModule == EIGEN_DA, "module selection"); assertTrue(nextId == 1, "module id"); - // Provisioning of rocky should fail, because jason is next in line - signatures = _getGuardianSignatures(_getPubKey(bytes32("rocky"))); - vm.expectRevert(Unauthorized.selector); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); - - signatures = _getGuardianSignatures(_getPubKey(bytes32("jason"))); - // Provision Jason - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); - signatures = _getGuardianSignatures(_getPubKey(bytes32("rocky"))); - // Rocky is now in line assertTrue(nextModule == CRAZY_GAINS, "module selection"); assertTrue(nextId == 0, "module id"); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); (nextModule, nextId) = pufferProtocol.getNextValidatorToProvision(); @@ -491,11 +479,9 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.getNextValidatorToBeProvisionedIndex(PUFFER_MODULE_0), 1, "next idx for no restaking module" ); - signatures = _getGuardianSignatures(_getPubKey(bytes32("alice"))); - vm.expectEmit(true, true, true, true); - emit SuccessfullyProvisioned(_getPubKey(bytes32("alice")), 1, PUFFER_MODULE_0); - pufferProtocol.provisionNode(signatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + emit IPufferProtocolEvents.SuccessfullyProvisioned(_getPubKey(bytes32("alice")), 1, PUFFER_MODULE_0, 1); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } function test_create_puffer_module() public { @@ -511,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(); @@ -527,326 +513,175 @@ contract PufferProtocolTest is UnitTestHelper { vm.expectRevert(); pufferProtocol.registerValidatorKey{ value: type(uint256).max }( - validatorKeyData, PUFFER_MODULE_0, emptyPermit, emptyPermit + validatorKeyData, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days ); } - // Node operator can deposit Bond in pufETH - function test_register_pufETH_approve_buy_VT() external { + function test_register_validator_key_new_flow() external { bytes memory pubKey = _getPubKey(bytes32("alice")); + vm.deal(alice, 10 ether); - uint256 expectedMint = pufferVault.previewDeposit(1 ether); - assertGt(expectedMint, 0, "should expect more pufETH"); + uint256 amount = BOND + (pufferOracle.getValidatorTicketPrice() * MINIMUM_EPOCHS_VALIDATION); - // Alice mints 1 ETH of pufETH vm.startPrank(alice); - uint256 minted = pufferVault.depositETH{ value: 1 ether }(alice); - assertGt(minted, 0, "should mint pufETH"); - // approve pufETH to pufferProtocol - pufferVault.approve(address(pufferProtocol), type(uint256).max); - - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 1 ether, "1 pufETH before for alice"); + assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before registration"); - // In this case, the only important data on permit is the amount - // Permit call will fail, but the amount is reused ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - Permit memory permit; - permit.amount = pufferVault.balanceOf(alice); - // Get the smoothing commitment amount for 180 days - uint256 sc = pufferOracle.getValidatorTicketPrice() * 180; - - // Register validator key by paying SC in ETH and depositing bond in pufETH vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); - pufferProtocol.registerValidatorKey{ value: sc }(data, PUFFER_MODULE_0, permit, emptyPermit); - // Alice has some dust in her wallet, because VT purchase changes the exchange rate, meaning pufETH is worth more - assertEq(pufferVault.balanceOf(alice), 1696417975049392, "1696417975049392 pufETH after for alice"); - uint256 protocolPufETHBalance = pufferVault.balanceOf(address(pufferProtocol)); - assertApproxEqRel(protocolPufETHBalance, 0.998303582024950608 ether, pointZeroZeroTwo, "~0.998 pufETH after"); - assertApproxEqRel( - pufferVault.convertToAssets(protocolPufETHBalance), 1 ether, pointZeroZeroOne, "1 ETH worth of pufETH after" - ); - } - - // Node operator can deposit Bond with Permit and pay for the VT in ETH - function test_register_pufETH_permit_pay_VT() external { - bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); - - // Alice mints 1 ETH of pufETH - vm.startPrank(alice); - pufferVault.depositETH{ value: 1 ether }(alice); - - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 1 ether, "1 pufETH before for alice"); - - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - // Generate Permit data for 2 pufETH to the protocol - Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), 2 ether, block.timestamp), pufferVault.DOMAIN_SEPARATOR() + 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 ); - uint256 numberOfDays = 180; - // Get the smoothing commitment amount for 6 months - uint256 sc = pufferOracle.getValidatorTicketPrice() * numberOfDays; - - // Register validator key by paying SC in ETH and depositing bond in pufETH - vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); - pufferProtocol.registerValidatorKey{ value: sc }(data, PUFFER_MODULE_0, permit, emptyPermit); - - // Alice has some dust in her wallet, because VT purchase changes the exchange rate, meaning pufETH is worth more - assertEq(pufferVault.balanceOf(alice), 1696417975049392, "1696417975049392 pufETH after for alice"); - - uint256 protocolPufETHBalance = pufferVault.balanceOf(address(pufferProtocol)); - assertEq(protocolPufETHBalance, 0.998303582024950608 ether, "~0.99 pufETH after"); - assertApproxEqRel( - pufferVault.convertToAssets(protocolPufETHBalance), 1 ether, pointZeroZeroOne, "1 ETH worth of pufETH after" + assertApproxEqAbs( + pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), BOND, 1, "1 pufETH after" ); + assertEq(address(pufferProtocol).balance, amount - BOND, "amount locked in the protocol"); } - // Node operator can deposit both VT and pufETH with Permit - function test_register_both_permit() external { + function test_register_validator_key_new_flow_with_eth_deposit() external { bytes memory pubKey = _getPubKey(bytes32("alice")); + vm.deal(alice, 10 ether); - uint256 numberOfDays = 200; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; + uint256 amount = BOND + (pufferOracle.getValidatorTicketPrice() * MINIMUM_EPOCHS_VALIDATION); + uint256 deadline = block.timestamp + 1 days; - // Alice mints 1 ETH of pufETH vm.startPrank(alice); - // Purchase pufETH - pufferVault.depositETH{ value: 1 ether }(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - - // Because Alice purchased a lot of VT's, it changed the conversion rate - // Because of that the registerValidatorKey will .transferFrom a smaller amount of pufETH - uint256 leftOverPufETH = pufferVault.balanceOf(alice) - pufferVault.convertToShares(1 ether); - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 1 ether, "1 pufETH before for alice"); - assertEq(validatorTicket.balanceOf(alice), _upscaleTo18Decimals(numberOfDays), "VT before for alice"); + assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before registration"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 bond = 1 ether; - Permit memory pufETHPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), bond, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); - Permit memory vtPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(numberOfDays), block.timestamp), - validatorTicket.DOMAIN_SEPARATOR() - ); - vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); - pufferProtocol.registerValidatorKey(data, PUFFER_MODULE_0, pufETHPermit, vtPermit); - - assertEq(pufferVault.balanceOf(alice), leftOverPufETH, "alice should have some leftover pufETH"); - assertEq(validatorTicket.balanceOf(alice), 0, "0 vt after for alice"); - assertApproxEqRel(pufferVault.balanceOf(address(pufferProtocol)), bond, 0.002e18, "1 pufETH after"); - } - - // Node operator can deposit both VT and pufETH with .approve - function test_register_both_approve() external { - bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); - - uint256 numberOfDays = 200; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; - - vm.startPrank(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - // Alice mints 1 ETH of pufETH - pufferVault.depositETH{ value: 1 ether }(alice); + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, 1); + pufferProtocol.registerValidatorKey{ value: amount }(data, PUFFER_MODULE_0, 0, new bytes[](0), deadline); + vm.stopPrank(); - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - // 1 wei diff assertApproxEqAbs( - pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1 ether, 1, "1 pufETH before for alice" + pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), BOND, 1, "1 pufETH after" ); - assertEq(validatorTicket.balanceOf(alice), _upscaleTo18Decimals(numberOfDays), "VT before for alice"); - - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); + assertEq(address(pufferProtocol).balance, amount - BOND, "amount locked in the protocol"); - uint256 bond = 1 ether; + // Provision a newly registered validator + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); - pufferVault.approve(address(pufferProtocol), type(uint256).max); - validatorTicket.approve(address(pufferProtocol), type(uint256).max); - - Permit memory vtPermit = emptyPermit; - vtPermit.amount = _upscaleTo18Decimals(numberOfDays); // upscale to 18 decimals - - Permit memory pufETHPermit = emptyPermit; - pufETHPermit.amount = pufferVault.convertToShares(bond); - - vm.expectEmit(true, true, true, true); - emit ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); - pufferProtocol.registerValidatorKey(data, PUFFER_MODULE_0, pufETHPermit, vtPermit); + // alice validated for 20 days * 225 epochs = 4500 epochs with 1 validator + uint256 validatedEpochs = 4500; - assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH after for alice"); - assertEq(validatorTicket.balanceOf(alice), 0, "0 vt after for alice"); - // 1 wei diff - assertApproxEqAbs( - pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), bond, 1, "1 pufETH after" + bytes[] memory vtConsumptionSignatures = _getTotalEpochsValidatedSignatures( + alice, validatedEpochs, deadline, IPufferProtocolLogic.depositValidationTime.selector ); - } - // Node operator can pay for pufETH with ETH and use Permit for VT - function test_register_pufETH_pay_vt_approve() external { - bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); - - uint256 numberOfDays = 30; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; + // We deposit 10 VT for alice (legacy VT) + deal(address(validatorTicket), address(this), 10 ether); + validatorTicket.approve(address(pufferProtocol), 10 ether); + pufferProtocol.depositValidatorTickets(alice, 10 ether); vm.startPrank(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - - assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH before for alice"); - assertEq(validatorTicket.balanceOf(alice), _upscaleTo18Decimals(numberOfDays), "VT before for alice"); - - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(numberOfDays), block.timestamp), - validatorTicket.DOMAIN_SEPARATOR() - ); - - // Alice is using SGX - uint256 bond = 1 ether; + // 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 ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); - pufferProtocol.registerValidatorKey{ value: bond }(data, PUFFER_MODULE_0, emptyPermit, permit); - - assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH after for alice"); - assertApproxEqRel(pufferVault.balanceOf(address(pufferProtocol)), 1 ether, pointZeroFive, "~1 pufETH after"); + emit IPufferProtocolEvents.ValidationTimeConsumed( + alice, 10 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 10 ether + ); // 10 Legacy VTs got burned + pufferProtocol.depositValidationTime{ value: 0.1 ether }( + EpochsValidatedSignature({ + nodeOperator: alice, + totalEpochsValidated: validatedEpochs, + functionSelector: 0, + deadline: deadline, + signatures: vtConsumptionSignatures + }) + ); } - // Node operator can deposit Bond in pufETH - function test_register_validator_key_with_permit_reverts_invalid_vt_amount() external { + function testRevert_invalidETHPayment() external { bytes memory pubKey = _getPubKey(bytes32("alice")); vm.deal(alice, 100 ether); - // Alice mints 2 ETH of pufETH - vm.startPrank(alice); - pufferVault.depositETH{ value: 2 ether }(alice); - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - // Generate Permit data for 10 pufETH to the protocol - Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), 0.5 ether, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); // Underpay VT vm.expectRevert(); - pufferProtocol.registerValidatorKey{ value: 0.1 ether }(data, PUFFER_MODULE_0, permit, emptyPermit); - } - - function test_validator_griefing_attack() external { - vm.deal(address(pufferVault), 100 ether); - - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - bytes[] memory guardianSignatures = _getGuardianSignatures(_getPubKey(bytes32("alice"))); - // Register and provision Alice - // Alice may be an active validator or it can be exited, doesn't matter - pufferProtocol.provisionNode(guardianSignatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); - - // Register another validator with using the same data - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - - // Try to provision it with the original message (replay attack) - // It should revert - vm.expectRevert(Unauthorized.selector); - pufferProtocol.provisionNode(guardianSignatures, _validatorSignature(), DEFAULT_DEPOSIT_ROOT); + pufferProtocol.registerValidatorKey{ value: 0.1 ether }( + data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); } function test_validator_limit_per_module() external { - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorLimitPerModuleChanged(500, 1); + emit IPufferProtocolEvents.ValidatorLimitPerModuleChanged(500, 1); pufferProtocol.setValidatorLimitPerModule(PUFFER_MODULE_0, 1); // Revert if the registration will be over the limit uint256 smoothingCommitment = pufferOracle.getValidatorTicketPrice(); bytes memory pubKey = _getPubKey(bytes32("bob")); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - uint256 bond = 1 ether; - vm.expectRevert(IPufferProtocol.ValidatorLimitForModuleReached.selector); - pufferProtocol.registerValidatorKey{ value: (smoothingCommitment + bond) }( - validatorKeyData, PUFFER_MODULE_0, emptyPermit, emptyPermit + 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, 2 ether); + vm.deal(alice, 3 ether); vm.deal(NoRestakingModule, 200 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); + uint256 deadline = block.timestamp + 1 days; + assertApproxEqAbs( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, + 1.5 ether, 1, - "~1 pufETH in protocol" + "~1.5 pufETH in protocol" ); // bond + something for the validator registration - assertEq(address(pufferVault).balance, 1001.2835 ether, "vault eth balance"); + assertEq(address(pufferVault).balance, 1001.5 ether, "vault eth balance"); Validator memory validator = pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 0); assertEq(validator.bond, pufferVault.balanceOf(address(pufferProtocol)), "alice bond is in the protocol"); - vm.warp(startTimestamp); - - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); // Didn't claim the bond yet assertEq(pufferVault.balanceOf(alice), 0, "alice has zero pufETH"); - // 15 days later (+16 is because 1 day is the start offset) - vm.warp(startTimestamp + 16 days); - StoppedValidatorInfo memory validatorInfo = StoppedValidatorInfo({ module: NoRestakingModule, moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(16 days, 100), - wasSlashed: false + totalEpochsValidated: 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"); // 1 wei diff assertApproxEqAbs( - pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1 ether, 1, "assets owned by alice" + pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1.5 ether, 1, "assets owned by alice" ); - - // Alice doesn't withdraw her VT's right away - vm.warp(startTimestamp + 50 days); } // Alice deposits VT for herself @@ -863,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"); @@ -892,108 +726,35 @@ 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(), 28 ether, "initial value"); + assertEq(pufferProtocol.getMinimumVtAmount(), 30 * EPOCHS_PER_DAY, "initial value"); vm.startPrank(DAO); - pufferProtocol.changeMinimumVTAmount(50 ether); + pufferProtocol.changeMinimumVTAmount(50 * EPOCHS_PER_DAY); - assertEq(pufferProtocol.getMinimumVtAmount(), 50 ether, "value after change"); + assertEq(pufferProtocol.getMinimumVtAmount(), 50 * EPOCHS_PER_DAY, "value after change"); } // Alice tries to withdraw all VT before provisioning @@ -1003,9 +764,9 @@ contract PufferProtocolTest is UnitTestHelper { vm.startPrank(alice); // Register Validator key registers validator with 30 VTs - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); - vm.expectRevert(IPufferProtocol.ActiveOrPendingValidatorsExist.selector); + vm.expectRevert(PufferProtocolBase.ActiveOrPendingValidatorsExist.selector); pufferProtocol.withdrawValidatorTickets(30 ether, alice); } @@ -1013,51 +774,55 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + + uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), 30 ether, pointZeroZeroOne, "alice should have ~30 VTS" + pufferProtocol.getValidationTime(alice), + 30 * EPOCHS_PER_DAY * vtPrice, + pointZeroZeroOne, + "alice should have ~30 VTS" ); vm.stopPrank(); 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( - pufferProtocol.getValidatorTicketsBalance(alice), - 20 ether, + pufferProtocol.getValidationTime(alice), + 20 * EPOCHS_PER_DAY * vtPrice, pointZeroZeroOne, "alice should have ~20 VTS -10 penalty" ); - - vm.startPrank(alice); - pufferProtocol.withdrawValidatorTickets(uint96(20 ether), alice); - - assertEq(validatorTicket.balanceOf(alice), 20 ether, "alice got her VT"); } function test_setVTPenalty() public { - assertEq(pufferProtocol.getVTPenalty(), 10 ether, "initial value"); + // 10 days of VT penalty + uint256 penaltyETHAmount = 10 * EPOCHS_PER_DAY; + assertEq(pufferProtocol.getVTPenalty(), penaltyETHAmount, "initial value"); + + uint256 newPenaltyAmount = 20 * EPOCHS_PER_DAY; vm.startPrank(DAO); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.VTPenaltyChanged(10 ether, 20 ether); - pufferProtocol.setVTPenalty(20 ether); + emit IPufferProtocolEvents.VTPenaltyChanged(penaltyETHAmount, newPenaltyAmount); + pufferProtocol.setVTPenalty(newPenaltyAmount); - assertEq(pufferProtocol.getVTPenalty(), 20 ether, "value after change"); + assertEq(pufferProtocol.getVTPenalty(), newPenaltyAmount, "value after change"); } function test_setVTPenalty_bigger_than_minimum_VT_amount() public { vm.startPrank(DAO); - vm.expectRevert(IPufferProtocol.InvalidVTAmount.selector); - pufferProtocol.setVTPenalty(50 ether); + 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); - pufferProtocol.changeMinimumVTAmount(9 ether); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); + pufferProtocol.changeMinimumVTAmount(9 * EPOCHS_PER_DAY); } function test_new_vtPenalty_works() public { @@ -1067,42 +832,53 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); + uint256 vtPricePerEpoch = pufferOracle.getValidatorTicketPrice(); + assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), 30 ether, pointZeroZeroOne, "alice should have ~30 VTS" + pufferProtocol.getValidationTime(alice), + 30 * EPOCHS_PER_DAY * vtPricePerEpoch, + pointZeroZeroOne, + "alice should have ~30 VTS" ); pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); // Alice loses 20 VT's assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), 10 ether, pointZeroZeroOne, "alice should have ~20 VTS" + pufferProtocol.getValidationTime(alice), + 10 * EPOCHS_PER_DAY * vtPricePerEpoch, + pointZeroZeroOne, + "alice should have ~10 VTS" ); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Alice is not provisioned assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), 40 ether, pointZeroZeroOne, "alice should have ~40 VTS" + pufferProtocol.getValidationTime(alice), + 40 * EPOCHS_PER_DAY * vtPricePerEpoch, + pointZeroZeroOne, + "alice should have ~40 VTS" ); // Set penalty to 0 vm.startPrank(DAO); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.VTPenaltyChanged(20 ether, 0); + emit IPufferProtocolEvents.VTPenaltyChanged(20 * EPOCHS_PER_DAY, 0); pufferProtocol.setVTPenalty(0); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), - 70 ether, + pufferProtocol.getValidationTime(alice), + 70 * EPOCHS_PER_DAY * vtPricePerEpoch, pointZeroZeroOne, "alice should have ~70 VTS register" ); @@ -1110,8 +886,8 @@ contract PufferProtocolTest is UnitTestHelper { pufferProtocol.skipProvisioning(PUFFER_MODULE_0, _getGuardianSignaturesForSkipping()); assertApproxEqRel( - pufferProtocol.getValidatorTicketsBalance(alice), - 70 ether, + pufferProtocol.getValidationTime(alice), + 70 * EPOCHS_PER_DAY * vtPricePerEpoch, pointZeroZeroOne, "alice should have ~70 VTS end" ); @@ -1120,52 +896,71 @@ contract PufferProtocolTest is UnitTestHelper { function test_double_withdrawal_reverts() public { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); - assertEq(validatorTicket.balanceOf(address(pufferProtocol)), 30 ether, "protocol has 30 VT"); assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 1 ether, 1, "protocol should have ~1 eth bond" + _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.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + emit IPufferProtocolEvents.ValidationTimeConsumed( + alice, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); + vm.expectEmit(true, true, true, true); + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); _executeFullWithdrawal( StoppedValidatorInfo({ module: NoRestakingModule, moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false - }) + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false + }), + deadline ); - // 28 got burned from Alice - assertApproxEqRel( - validatorTicket.balanceOf(address(pufferProtocol)), 2 ether, pointZeroZeroOne, "Protocol has 2 VT" - ); + // 2 days are leftover from 30 (30 is minimum for registration) + uint256 leftOverTime = 2 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(); - assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" - ); + uint256 unusedValidationTime = pufferProtocol.getValidationTime(alice); + + assertEq(unusedValidationTime, leftOverTime, "unused validation time"); + + // VTS got burned from puffer protocol + assertEq(address(pufferProtocol).balance, unusedValidationTime, "Protocol has some leftower ETH - unused VT"); + + vm.startPrank(alice); + pufferProtocol.withdrawValidationTime(uint96(unusedValidationTime), address(55)); + + assertEq(weth.balanceOf(address(55)), unusedValidationTime, "recipient got the validation time ETH"); + + assertApproxEqAbs(_getUnderlyingETHAmount(address(alice)), 1.5 ether, 1, "alice got back the bond"); - assertApproxEqAbs(_getUnderlyingETHAmount(address(alice)), 1 ether, 1, "alice got back the bond"); + 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, moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false - }) + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: vtConsumptionSignature, + wasSlashed: false, + isDownsize: false + }), + deadline ); } @@ -1177,28 +972,27 @@ contract PufferProtocolTest is UnitTestHelper { uint256 aliceVTBalance = pufferProtocol.getValidatorTicketsBalance(alice); - assertApproxEqRel(aliceVTBalance, 2 ether, pointZeroZeroOne, "2 vt balance after"); + assertEq(aliceVTBalance, 0, "0 vt token balance after"); vm.startPrank(alice); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorTicketsWithdrawn(alice, alice, aliceVTBalance); + emit IPufferProtocolEvents.ValidatorTicketsWithdrawn(alice, alice, aliceVTBalance); pufferProtocol.withdrawValidatorTickets(uint96(aliceVTBalance), alice); - assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "0 vt balance after"); + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "0 vt token balance after"); assertEq(validatorTicket.balanceOf(alice), aliceVTBalance, "~20 vt alice before"); uint256 bobVTBalance = pufferProtocol.getValidatorTicketsBalance(bob); - assertApproxEqRel(bobVTBalance, 2 ether, pointZeroZeroOne, "2 vt balance before bob"); + assertEq(bobVTBalance, 0, "2 vt balance before bob"); vm.startPrank(bob); 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 balance after bob"); - assertApproxEqRel(validatorTicket.balanceOf(alice), 4 ether, pointZeroZeroOne, "4 vt alice after bobs gift"); + assertEq(pufferProtocol.getValidatorTicketsBalance(bob), 0, "0 vt token balance after bob"); } // Batch claim 32 ETH withdrawals @@ -1206,14 +1000,24 @@ contract PufferProtocolTest is UnitTestHelper { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); + assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 3 ether, "protocol should have 3 eth bond"); + + // 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, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + totalEpochsValidated: epochsValidated, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, epochsValidated, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false }); StoppedValidatorInfo memory bobInfo = StoppedValidatorInfo({ @@ -1221,9 +1025,12 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 1, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + totalEpochsValidated: epochsValidated, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + bob, epochsValidated, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false }); StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](2); @@ -1231,26 +1038,31 @@ contract PufferProtocolTest is UnitTestHelper { stopInfos[1] = bobInfo; vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + emit IPufferProtocolEvents.ValidationTimeConsumed( + alice, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); + vm.expectEmit(true, true, true, true); + emit IPufferProtocolEvents.ValidationTimeConsumed( + bob, 28 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(), 0 ); - pufferProtocol.batchHandleWithdrawals(stopInfos, _getHandleBatchWithdrawalMessage(stopInfos)); + vm.expectEmit(true, true, true, true); + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, 0, 1); - assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" + pufferProtocol.batchHandleWithdrawals( + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline ); - // Alice got more because she earned the rewards from Bob's registration - assertGe(_getUnderlyingETHAmount(address(alice)), 1 ether, "alice got back the bond gt"); + assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); - assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 1 ether, 1, "bob got back the bond"); + assertEq(_getUnderlyingETHAmount(address(alice)), 1.5 ether, "alice got back the bond gt"); + + assertEq(_getUnderlyingETHAmount(address(bob)), 1.5 ether, "bob got back the bond"); } // Batch claim of different amounts + // This one uses old validator tickets instead of new VT model function test_different_amounts_batch_claim() public { // Buy and approve VT validatorTicket.purchaseValidatorTicket{ value: 10 ether }(address(this)); @@ -1263,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({ @@ -1277,96 +1090,105 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(35 days, 100), - wasSlashed: false + totalEpochsValidated: 35 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 35 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false }); stopInfos[1] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 1, withdrawalAmount: 31.9 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + bob, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false }); stopInfos[2] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 2, withdrawalAmount: 31 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(34 days, 100), - wasSlashed: true + totalEpochsValidated: 34 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + charlie, 34 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: true, + isDownsize: false }); stopInfos[3] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 3, withdrawalAmount: 31.8 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(48 days, 100), - wasSlashed: false + totalEpochsValidated: 48 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + dianna, 48 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false }); stopInfos[4] = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 4, withdrawalAmount: 31.5 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(2 days, 100), - wasSlashed: true + totalEpochsValidated: 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.ValidatorExited( - _getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, _getVTBurnAmount(100, _getEpochNumber(35 days, 100)) - ); + emit IPufferProtocolEvents.ValidationTimeConsumed(alice, 0, 35 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("bob")), - 1, - PUFFER_MODULE_0, - pufferVault.convertToSharesUp(0.1 ether), - _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) + emit IPufferProtocolEvents.ValidatorExited(_getPubKey(bytes32("alice")), 0, PUFFER_MODULE_0, 0, 1); + vm.expectEmit(true, true, true, true); + emit IPufferProtocolEvents.ValidationTimeConsumed(bob, 0, 28 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + vm.expectEmit(true, true, true, true); + emit IPufferProtocolEvents.ValidatorExited( + _getPubKey(bytes32("bob")), 1, PUFFER_MODULE_0, pufferVault.convertToSharesUp(0.1 ether), 1 ); vm.expectEmit(true, true, true, true); - 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, pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 2).bond, - _getVTBurnAmount(100, _getEpochNumber(34 days, 100)) + 1 ); // got slashed vm.expectEmit(true, true, true, true); - emit IPufferProtocol.ValidatorExited( - _getPubKey(bytes32("dianna")), - 3, - PUFFER_MODULE_0, - pufferVault.convertToSharesUp(0.2 ether), - _getVTBurnAmount(100, _getEpochNumber(48 days, 100)) + 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.ValidatorExited( - _getPubKey(bytes32("eve")), - 4, - PUFFER_MODULE_0, - pufferProtocol.getValidatorInfo(PUFFER_MODULE_0, 4).bond, - 28 ether // minimum vt amount + emit IPufferProtocolEvents.ValidationTimeConsumed(eve, 0, 2 * EPOCHS_PER_DAY * BURN_RATE_PER_EPOCH); + vm.expectEmit(true, true, true, true); + 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)); - - assertApproxEqAbs( - _getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, 1, "protocol should have 0 eth bond" + pufferProtocol.batchHandleWithdrawals( + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline ); - // Alice got more because she earned the rewards from the others - assertGe(_getUnderlyingETHAmount(address(alice)), 1 ether, "alice got back the bond gt"); + assertEq(_getUnderlyingETHAmount(address(pufferProtocol)), 0 ether, "protocol should have 0 eth bond"); - // Bob got 0.9 ETH bond + some rewards from the others + // // Alice got more because she earned the rewards from the others + assertGe(_getUnderlyingETHAmount(address(alice)), 1.5 ether, "alice got back the bond gt"); + + // // Bob got 0.9 ETH bond + some rewards from the others assertGe(_getUnderlyingETHAmount(address(bob)), 0.9 ether, "bob got back the bond gt"); - // Charlie got 0 bond + // // Charlie got 0 bond assertEq(_getUnderlyingETHAmount(address(charlie)), 0, "charlie got 0 bond - slashed"); assertGe(_getUnderlyingETHAmount(address(dianna)), 0.8 ether, "dianna got back the bond gt"); @@ -1374,18 +1196,177 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(_getUnderlyingETHAmount(address(eve)), 0, "eve got 0 bond - slashed"); } + function test_oldVT_and_new_VT_model_only_vt_burned() public { + assertEq(pufferVault.convertToAssets(1 ether), 1 ether, "initial exchange rate is 1:1"); + + // Buy and approve VT, this changes the exchange rate + validatorTicket.purchaseValidatorTicket{ value: 1 ether }(address(this)); + validatorTicket.approve(address(pufferProtocol), 1000 ether); + + uint256 exchangeRateAfterVTPurchase = 1000945000000000000; + + // Exchange rate remained unchanged, 1 wei diff (rounding) + assertApproxEqAbs( + pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "initial exchange rate is ~1:1" + ); + + 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"); + + // Alice is provisioned with 30 'new VT' and has 100 validator tickets deposited + // Total 130 'days' of validation + _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); + + uint256 initialValidationTimeAfterProvisioning = pufferProtocol.getValidationTime(alice); + + // Alice exits a validator after 65 days of validation + StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](1); + stopInfos[0] = StoppedValidatorInfo({ + moduleName: PUFFER_MODULE_0, + module: NoRestakingModule, + pufferModuleIndex: 0, + withdrawalAmount: 32 ether, + totalEpochsValidated: 65 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 65 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false + }); + + // Exchange rate remained unchanged, 1 wei diff (rounding) + assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); + + pufferProtocol.batchHandleWithdrawals( + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline + ); + + // Validation time is unchanged + assertEq( + pufferProtocol.getValidationTime(alice), + initialValidationTimeAfterProvisioning, + "Alice has the same amount of validation time" + ); + + // Alice has 100-65 days of VT left + assertEq( + pufferProtocol.getValidatorTicketsBalance(alice), + 34.999999999999991875 ether, + "Alice has ~35 VT in the protocol" + ); + + // txs don't revert + vm.startPrank(alice); + pufferProtocol.withdrawValidationTime(uint96(initialValidationTimeAfterProvisioning), alice); + pufferProtocol.withdrawValidatorTickets(34.999999999999991875 ether, alice); + + // Alice has 0 VT in the protocol + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "Alice has 0 VT in the protocol"); + + // Alice has 0 validation time + assertEq(pufferProtocol.getValidationTime(alice), 0, "Alice has 0 validation time"); + } + + function test_oldVT_and_new_VT_model_only_both_burned() public { + assertEq(pufferVault.convertToAssets(1 ether), 1 ether, "initial exchange rate is 1:1"); + + // Buy and approve VT, this changes the exchange rate + validatorTicket.purchaseValidatorTicket{ value: 1 ether }(address(this)); + validatorTicket.approve(address(pufferProtocol), 1000 ether); + + uint256 exchangeRateAfterVTPurchase = 1000945000000000000; + + 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" + ); + + uint256 vtAmount = 100 ether; + pufferProtocol.depositValidatorTickets(alice, vtAmount); + + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 100 ether, "100 VT in the protocol"); + + // Alice is provisioned with 30 'new VT' and has 100 validator tickets deposited + // Total 130 'days' of validation + _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); + + // Alice exits a validator after 120 days of validation + StoppedValidatorInfo[] memory stopInfos = new StoppedValidatorInfo[](1); + stopInfos[0] = StoppedValidatorInfo({ + moduleName: PUFFER_MODULE_0, + module: NoRestakingModule, + pufferModuleIndex: 0, + withdrawalAmount: 32 ether, + totalEpochsValidated: 120 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 120 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false + }); + + // Exchange rate remained unchanged, 1 wei diff (rounding) + assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); + + pufferProtocol.batchHandleWithdrawals( + stopInfos, _getHandleBatchWithdrawalMessage(stopInfos, deadline), deadline + ); + + // Nothing is changed, we didn't deposit revenue + assertApproxEqAbs(pufferVault.convertToAssets(1 ether), exchangeRateAfterVTPurchase, 1, "exchange rate is ~1:1"); + + revenueDepositor.depositRevenue(); + + assertGt( + pufferVault.convertToAssets(1 ether), + exchangeRateAfterVTPurchase, + "exchange rate is now bigger because of revenue deposit" + ); + + // Alice has 0 VT in the protocol + assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 0, "Alice has 0 VT in the protocol"); + + // It is expected to have 10 days of validation time + uint256 expectedLeftOverValidationTime = 10 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(); + + // Alice has >0 validation time + assertEq( + pufferProtocol.getValidationTime(alice), expectedLeftOverValidationTime, "Alice has >0 validation time" + ); + + vm.startPrank(alice); + pufferProtocol.withdrawValidationTime(uint96(expectedLeftOverValidationTime), address(8888)); + + assertEq( + weth.balanceOf(address(8888)), + expectedLeftOverValidationTime, + "Recipient got WETH (validation time from Alice)" + ); + } + function test_single_withdrawal() public { _registerAndProvisionNode(bytes32("alice"), PUFFER_MODULE_0, alice); _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); + uint256 deadline = block.timestamp + 1 days; + StoppedValidatorInfo memory aliceInfo = StoppedValidatorInfo({ moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false }); StoppedValidatorInfo memory bobInfo = StoppedValidatorInfo({ @@ -1393,30 +1374,31 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 1, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: false + totalEpochsValidated: 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, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) - ); // 10 days of VT - _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, _getVTBurnAmount(100, _getEpochNumber(28 days, 100)) - ); // 10 days of VT - _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" ); // Alice got more because she earned the rewards from Bob's registration - assertGe(_getUnderlyingETHAmount(address(alice)), 1 ether, "alice got back the bond gt"); + assertGe(_getUnderlyingETHAmount(address(alice)), 1.5 ether, "alice got back the bond gt"); - assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 1 ether, 1, "bob got back the bond"); + assertApproxEqAbs(_getUnderlyingETHAmount(address(bob)), 1.5 ether, 1, "bob got back the bond"); } function test_batch_vs_multiple_single_withdrawals() public { @@ -1439,36 +1421,36 @@ 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) + guardianEOASignatures: _getHandleBatchWithdrawalMessage(stopInfos, deadline), + deadline: deadline }); } - // Register 2 validators and provision 1 validator and post full withdrawal proof for 29 eth (slash 3 ETH on one validator) + // Register 2 validators and provision 1 validator and post full withdrawal proof for 29 eth (slash 1.5 ETH from NoOp) // Case 1 function test_slashing_case_1() public { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + // This is because VT settlement now happens later, so the exchange rate is 1:1 + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning, 1:1"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + + uint256 deadline = block.timestamp + 1 days; // Give funds to modules vm.deal(NoRestakingModule, 200 ether); @@ -1481,48 +1463,50 @@ contract PufferProtocolTest is UnitTestHelper { module: NoRestakingModule, pufferModuleIndex: 0, withdrawalAmount: 29 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - wasSlashed: true + totalEpochsValidated: 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"); - // The other validator has less than 1 ETH in the bond - // Bad dept is shared between all pufETH holders assertApproxEqRel( pufferVault.balanceOf(address(pufferProtocol)), - 1 ether, + 1.5 ether, pointZeroOne, - "1 ETH worth of pufETH in the protocol" + "1.5 ETH worth of pufETH in the protocol" ); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); + + // Provision another Alice Validator works + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } - // Register 2 validators, provision 1, slash 1.5 whole validator bond owned by node operator + // Register 2 validators, provision 1, slash 2.5 whole validator bond owned by node operator // Case 2 function test_slashing_case_2() public { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning, 1:1"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + + uint256 deadline = block.timestamp + 1 days; vm.deal(NoRestakingModule, 200 ether); @@ -1533,48 +1517,58 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - withdrawalAmount: 30.5 ether, - wasSlashed: true + totalEpochsValidated: 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 worse than before + // 1 ETH gives you more pufETH after the `retrieveBond` call, meaning it is better for pufETH holders assertLt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); // The other validator has less than 1 ETH in the bond // Bad dept is shared between all pufETH holders assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, + 1.5 ether, pointZeroOne, - "1 ether ETH worth of pufETH in the protocol" + "1.5 ether ETH worth of pufETH in the protocol" ); assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); } - // Register 2 validators, provision 1, slash 1 whole validator bond (1 ETH) + // Register 2 validators, provision 1, slash 1 whole validator bond (2 ETH) // Case 3 function test_slashing_case_3() public { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators - uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + uint256 exchangeRateBefore = pufferVault.convertToAssets(1 ether); + // 2 bonds * 1.5 ETH + 1000 initial value in the vault + assertEq(address(pufferVault).balance, 1003 ether, "1003 ETH in the vault"); + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning, 1:1"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + + // We provision one validator + assertEq(address(pufferVault).balance, 971 ether, "971 ETH in the vault"); + + // Stays the same + assertEq(pufferVault.convertToAssets(1 ether), 1 ether, "shares after provisioning"); + assertEq(weth.balanceOf(address(pufferVault)), 0 ether, "0 WETH in the vault"); + + uint256 deadline = block.timestamp + 1 days; vm.deal(NoRestakingModule, 200 ether); @@ -1585,34 +1579,40 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), - withdrawalAmount: 31 ether, - wasSlashed: true + totalEpochsValidated: 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); - // Exchange rate remains the same, it is slightly better - assertApproxEqRel( - exchangeRateBefore, pufferVault.convertToShares(1 ether), pointZeroZeroOne, "shares after retrieve" + // 30 ETH was returned to the vault + assertEq(address(pufferVault).balance, 1001.9 ether, "1001.9 ETH in the vault"); + + revenueDepositor.depositRevenue(); + + assertGt(weth.balanceOf(address(pufferVault)), 0 ether, "WETH in the vault"); + + // Exchange rate changes in favour of remaining pufETH holders + assertLt( + exchangeRateBefore, + pufferVault.convertToAssets(1 ether), + "exchange rate after validator exits is better for pufETH holders" ); - // 1 ETH gives you less pufETH after the `retrieveBond` call, meaning it is better than before (slightly) - assertGt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); - // Alice has a little over 1 ETH because she earned something for paying the VT on the second validator registration + // Alice has a little over 1.5 ETH because she earned something from herself (her own exit + slashing) assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, - pointZeroZeroOne, - "1 ETH worth of pufETH in the protocol" - ); - assertGt( - pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, - "1 ETH worth of pufETH in the protocol gt" + 1.5 ether, + pointZeroOne, + "1.5 ETH worth of pufETH in the protocol" ); + // Alice didn't receive any bond for that one validator exit assertEq(pufferVault.balanceOf(alice), 0, "0 pufETH alice"); } @@ -1622,19 +1622,17 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); + + uint256 deadline = block.timestamp + 1 days; vm.deal(NoRestakingModule, 200 ether); @@ -1645,24 +1643,29 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, - startEpoch: 100, - endEpoch: _getEpochNumber(28 days, 100), + totalEpochsValidated: 28 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 28 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), withdrawalAmount: 31.9 ether, - wasSlashed: false + wasSlashed: false, + isDownsize: false }); // Burns one whole bond - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); - // Exchange rate stays the same - assertEq(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); + revenueDepositor.depositRevenue(); - // Alice has ~ 1 ETH locked in the protocol + // Exchange rate is better for pufETH holders + assertGt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); + + // Alice has ~ 1.5 ETH locked in the protocol assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, - pointZeroZeroOne, - "1 ETH worth of pufETH in the protocol" + 1.5 ether, + pointZeroOne, + "1.5 ETH worth of pufETH in the protocol" ); // Alice got a little over 0.9 ETH worth of pufETH because she earned something for paying the VT on the second validator registration assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 0.9 ether, ">0.9 ETH worth of pufETH alice"); @@ -1674,22 +1677,20 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); // Get the exchange rate before provisioning validators uint256 exchangeRateBefore = pufferVault.convertToShares(1 ether); - assertEq(exchangeRateBefore, 999433604122689216, "shares before provisioning"); + assertEq(exchangeRateBefore, 1 ether, "shares before provisioning"); - uint256 startTimestamp = 1707411226; - vm.warp(startTimestamp); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.deal(NoRestakingModule, 200 ether); + 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 @@ -1697,39 +1698,46 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, module: NoRestakingModule, pufferModuleIndex: 0, - startEpoch: 100, - endEpoch: _getEpochNumber(15 days, 100), + totalEpochsValidated: 15 * EPOCHS_PER_DAY, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + alice, 15 * EPOCHS_PER_DAY, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), withdrawalAmount: 32.1 ether, - wasSlashed: false + wasSlashed: false, + isDownsize: false }); // Burns one whole bond - _executeFullWithdrawal(validatorInfo); + _executeFullWithdrawal(validatorInfo, deadline); - // Exchange rate stays the same - assertEq(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); + revenueDepositor.depositRevenue(); + + // Exchange rate is better for pufETH holders + assertGt(exchangeRateBefore, pufferVault.convertToShares(1 ether), "shares after retrieve"); // Alice has ~ 1 ETH locked in the protocol assertApproxEqRel( pufferVault.convertToAssets(pufferVault.balanceOf(address(pufferProtocol))), - 1 ether, + 1.5 ether, pointZeroZeroOne, - "1 ETH worth of pufETH in the protocol" + "1.5 ETH worth of pufETH in the protocol" ); - // Alice got a little over 1 ETH worth of pufETH because she earned something for paying the VT on the second validator registration - assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1 ether, ">1 ETH worth of pufETH alice"); + // Alice got a little over 1.5 ETH worth of pufETH because she earned something for paying the VT on the second validator registration + assertGt(pufferVault.convertToAssets(pufferVault.balanceOf(alice)), 1.5 ether, ">1.5 ETH worth of pufETH alice"); } function test_validator_early_exit_dos() public { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + 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 _executeFullWithdrawal( @@ -1738,13 +1746,21 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(1 days, 100), - wasSlashed: false - }) + totalEpochsValidated: 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 ); - assertEq(pufferProtocol.getValidatorTicketsBalance(alice), 2 ether, "alice got 2 VT left in the protocol"); + uint256 leftOverValidationTime = 20 * EPOCHS_PER_DAY * pufferOracle.getValidatorTicketPrice(); + + assertEq( + pufferProtocol.getValidationTime(alice), leftOverValidationTime, "alice got 20 days left in the protocol" + ); } // Alice registers one 30 VT validator @@ -1754,17 +1770,17 @@ contract PufferProtocolTest is UnitTestHelper { vm.deal(alice, 10 ether); vm.startPrank(alice); - _registerValidatorKey(bytes32("alice"), PUFFER_MODULE_0); + _registerValidatorKey(alice, bytes32("alice"), PUFFER_MODULE_0, 0); vm.stopPrank(); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(bytes32("alice"))), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); vm.startPrank(DAO); pufferProtocol.changeMinimumVTAmount(35 ether); vm.stopPrank(); + uint256 deadline = block.timestamp + 1 days; + // Alice exited after 1 day _executeFullWithdrawal( StoppedValidatorInfo({ @@ -1772,66 +1788,38 @@ contract PufferProtocolTest is UnitTestHelper { moduleName: PUFFER_MODULE_0, pufferModuleIndex: 0, withdrawalAmount: 32 ether, - startEpoch: 100, - endEpoch: _getEpochNumber(3 days, 100), - wasSlashed: false - }) + totalEpochsValidated: 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"); } - // User purchases a lot of VT using ETH, but uses Permit to transfer pufETH - function test_purchase_big_amount_of_vt() public { + // User deposits a lot of ETH (validator tickets) + function test_big_eth_deposit() public { bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); - - // Alice mints 1 ETH of pufETH - vm.startPrank(alice); - pufferVault.depositETH{ value: 1 ether }(alice); assertEq(pufferVault.balanceOf(address(pufferProtocol)), 0, "zero pufETH before"); - assertEq(pufferVault.balanceOf(alice), 1 ether, "1 pufETH before for alice"); ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - // Generate Permit data for 1 pufETH to the protocol - Permit memory permit = _signPermit( - _testTemps("alice", address(pufferProtocol), 1 ether, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); + 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 ValidatorKeyRegistered(pubKey, 0, PUFFER_MODULE_0, true); - pufferProtocol.registerValidatorKey{ value: 9 ether }(data, PUFFER_MODULE_0, permit, emptyPermit); + 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); - // Because alice purchased VT in the registration TX, it modified the exchange rate and we take less pufETH from her. - assertEq(pufferVault.balanceOf(alice), 8424921124709635, "alice has 8424921124709635 pufETH after registering"); - } - - // Alice uses Permit for VT and pays for the bond with ETH, but sends more ETH than needed - function test_revert_for_excess_eth() public { - bytes memory pubKey = _getPubKey(bytes32("alice")); - vm.deal(alice, 10 ether); - - uint256 numberOfDays = 30; - uint256 amount = pufferOracle.getValidatorTicketPrice() * numberOfDays; - - // Alice mints 1 ETH of pufETH - vm.startPrank(alice); - // Alice purchases VT - validatorTicket.purchaseValidatorTicket{ value: amount }(alice); - - Permit memory vtPermit = _signPermit( - _testTemps("alice", address(pufferProtocol), _upscaleTo18Decimals(numberOfDays), block.timestamp), - validatorTicket.DOMAIN_SEPARATOR() - ); - - ValidatorKeyData memory data = _getMockValidatorKeyData(pubKey, PUFFER_MODULE_0); - - // User pays for the bond in pufETH, but decides to send more than the bond 1.1 eth - vm.expectRevert(IPufferProtocol.InvalidETHAmount.selector); - pufferProtocol.registerValidatorKey{ value: 1.1 ether }(data, PUFFER_MODULE_0, emptyPermit, vtPermit); + // Protocol holds 7.5 ETHER + assertEq(address(pufferProtocol).balance, 7.5 ether, "7.5 ETH in the protocol"); + assertEq(pufferVault.balanceOf(address(pufferProtocol)), 1.5 ether, "Bond in pufETH is held by the protocol"); } // Alice deposits VT to Bob and Bob has no validators in Puffer @@ -1840,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); @@ -1854,35 +1841,30 @@ contract PufferProtocolTest is UnitTestHelper { assertEq(validatorTicket.balanceOf(bob), 50 ether, "bob got the VT"); } - function _getGuardianSignatures(bytes memory pubKey) internal view returns (bytes[] memory) { + 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(); - Validator memory validator = pufferProtocol.getValidatorInfo(moduleName, pendingIdx); - // If there is no module return empty byte array - if (validator.module == address(0)) { - return new bytes[](0); - } - bytes memory withdrawalCredentials = pufferProtocol.getWithdrawalCredentials(validator.module); - - bytes32 digest = LibGuardianMessages._getBeaconDepositMessageToBeSigned( - pendingIdx, - pubKey, - _validatorSignature(), - withdrawalCredentials, - pufferProtocol.getDepositDataRoot({ - pubKey: pubKey, - signature: _validatorSignature(), - withdrawalCredentials: withdrawalCredentials - }) - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SKEnclave, digest); + bytes32 digest = LibGuardianMessages._getSkipProvisioningMessage(moduleName, pendingIdx); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(guardian1SK, digest); bytes memory signature1 = abi.encodePacked(r, s, v); // note the order here is different from line above. - (v, r, s) = vm.sign(guardian2SKEnclave, digest); - (v, r, s) = vm.sign(guardian3SKEnclave, digest); + (v, r, s) = vm.sign(guardian2SK, digest); bytes memory signature2 = abi.encodePacked(r, s, v); // note the order here is different from line above. - (v, r, s) = vm.sign(guardian3SKEnclave, digest); + (v, r, s) = vm.sign(guardian3SK, digest); bytes memory signature3 = abi.encodePacked(r, s, v); // note the order here is different from line above. bytes[] memory guardianSignatures = new bytes[](3); @@ -1893,10 +1875,22 @@ contract PufferProtocolTest is UnitTestHelper { return guardianSignatures; } - function _getGuardianSignaturesForSkipping() internal view returns (bytes[] memory) { - (bytes32 moduleName, uint256 pendingIdx) = pufferProtocol.getNextValidatorToProvision(); + /** + * @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 _getTotalEpochsValidatedSignatures( + address node, + uint256 validatedEpochsTotal, + uint256 deadline, + bytes32 funcSelector + ) internal view returns (bytes[] memory) { + uint256 nonce = pufferProtocol.nonces(funcSelector, node); - bytes32 digest = LibGuardianMessages._getSkipProvisioningMessage(moduleName, pendingIdx); + 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. @@ -1915,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); + 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. @@ -1947,7 +1941,7 @@ contract PufferProtocolTest is UnitTestHelper { hex"8aa088146c8c6ca6d8ad96648f20e791be7c449ce7035a6bd0a136b8c7b7867f730428af8d4a2b69658bfdade185d6110b938d7a59e98d905e922d53432e216dc88c3384157d74200d3f2de51d31737ce19098ff4d4f54f77f0175e23ac98da5"; } - // Generates a mock validator data for SGX 1 ETH case + // Generates a mock validator data for 1.5 ETH case function _getMockValidatorKeyData(bytes memory pubKey, bytes32 moduleName) internal view @@ -1974,10 +1968,8 @@ contract PufferProtocolTest is UnitTestHelper { signature: validatorSignature, withdrawalCredentials: withdrawalCredentials }), - blsEncryptedPrivKeyShares: new bytes[](3), - blsPubKeySet: new bytes(48), - raveEvidence: bytes("mock rave") // Guardians are checking it off chain - }); + numBatches: 1 + }); return validatorData; } @@ -2005,37 +1997,44 @@ contract PufferProtocolTest is UnitTestHelper { /** * @dev Registers validator key and pays for everything in ETH + * @dev epochValidated = sum for all all the validators and their consumption */ - function _registerValidatorKey(bytes32 pubKeyPart, bytes32 moduleName) internal { - uint256 numberOfDays = 30; - uint256 vtPrice = pufferOracle.getValidatorTicketPrice() * numberOfDays; + function _registerValidatorKey( + address nodeOperator, + bytes32 pubKeyPart, + bytes32 moduleName, + uint256 epochsValidated + ) internal { + uint256 amount = BOND + (pufferOracle.getValidatorTicketPrice() * MINIMUM_EPOCHS_VALIDATION); + bytes memory pubKey = _getPubKey(pubKeyPart); ValidatorKeyData memory validatorKeyData = _getMockValidatorKeyData(pubKey, moduleName); uint256 idx = pufferProtocol.getPendingValidatorIndex(moduleName); - uint256 bond = 1 ether; + 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 ValidatorKeyRegistered(pubKey, idx, moduleName, true); - pufferProtocol.registerValidatorKey{ value: (vtPrice + bond) }( - validatorKeyData, moduleName, emptyPermit, emptyPermit + emit IPufferProtocolEvents.ValidatorKeyRegistered(pubKey, idx, moduleName, 1); + pufferProtocol.registerValidatorKey{ value: amount }( + validatorKeyData, moduleName, epochsValidated, vtConsumptionSignatures, deadline ); } /** - * @dev Registers and provisions a new validator with 1 ETH bond (enclave) and 30 VTs (see _registerValidatorKey) + * @dev Registers and provisions a new validator with 2 ETH bond and 30 VTs (see _registerValidatorKey) */ function _registerAndProvisionNode(bytes32 pubKeyPart, bytes32 moduleName, address nodeOperator) internal { vm.deal(nodeOperator, 10 ether); vm.startPrank(nodeOperator); - _registerValidatorKey(pubKeyPart, moduleName); + _registerValidatorKey(nodeOperator, pubKeyPart, moduleName, 0); vm.stopPrank(); - pufferProtocol.provisionNode( - _getGuardianSignatures(_getPubKey(pubKeyPart)), _validatorSignature(), DEFAULT_DEPOSIT_ROOT - ); + pufferProtocol.provisionNode(_validatorSignature(), DEFAULT_DEPOSIT_ROOT); } /** @@ -2050,21 +2049,110 @@ contract PufferProtocolTest is UnitTestHelper { return amount * 1 ether; } - function _getEpochNumber(uint256 validationTimeInSeconds, uint256 startEpoch) - internal - pure - returns (uint256 endEpoch) - { - uint256 secondsInEpoch = 32 * 12; - uint256 numberOfEpochs = validationTimeInSeconds / secondsInEpoch; - return startEpoch + numberOfEpochs; + function test_getNodeInfo() public view { + // Test non-existent node + NodeInfo memory nodeInfo = pufferProtocol.getNodeInfo(address(0x123)); + assertEq(nodeInfo.activeValidatorCount, 0); + assertEq(nodeInfo.pendingValidatorCount, 0); + assertEq(nodeInfo.deprecated_vtBalance, 0); + assertEq(nodeInfo.validationTime, 0); + assertEq(nodeInfo.epochPrice, 0); + assertEq(nodeInfo.totalEpochsValidated, 0); + + // Test registered node (alice) + nodeInfo = pufferProtocol.getNodeInfo(alice); + assertEq(nodeInfo.activeValidatorCount, 0); + assertEq(nodeInfo.pendingValidatorCount, 0); + assertEq(nodeInfo.deprecated_vtBalance, 0); + assertEq(nodeInfo.validationTime, 0); + assertEq(nodeInfo.epochPrice, 0); + assertEq(nodeInfo.totalEpochsValidated, 0); + } + + function test_setVTPenalty_invalid_amount() public { + vm.startPrank(DAO); + vm.expectRevert(PufferProtocolBase.InvalidVTAmount.selector); + pufferProtocol.setVTPenalty(type(uint256).max); + } + + function test_checkValidatorRegistrationInputs_invalid_pubkey() public { + bytes memory invalidPubKey = new bytes(47); // Invalid length + ValidatorKeyData memory data = _getMockValidatorKeyData(invalidPubKey, PUFFER_MODULE_0); + + vm.expectRevert(PufferProtocolBase.InvalidBLSPubKey.selector); + pufferProtocol.registerValidatorKey{ value: 3 ether }( + data, PUFFER_MODULE_0, 0, new bytes[](0), block.timestamp + 1 days + ); + } + + function test_changeMinimumVTAmount_invalid_amount() public { + vm.startPrank(DAO); + 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, + moduleName: PUFFER_MODULE_0, + pufferModuleIndex: 0, + withdrawalAmount: 32 ether, + totalEpochsValidated: type(uint256).max, + vtConsumptionSignature: _getTotalEpochsValidatedSignatures( + bob, type(uint256).max, deadline, IPufferProtocolLogic.batchHandleWithdrawals.selector + ), + wasSlashed: false, + isDownsize: false + }); + + StoppedValidatorInfo[] memory validatorInfos = new StoppedValidatorInfo[](1); + validatorInfos[0] = info; + + _registerAndProvisionNode(bytes32("bob"), PUFFER_MODULE_0, bob); + + // Panic Error is expected panic: arithmetic underflow or overflow (0x11) + vm.expectRevert(bytes("panic: arithmetic underflow or overflow (0x11)")); + pufferProtocol.batchHandleWithdrawals( + validatorInfos, _getHandleBatchWithdrawalMessage(validatorInfos, deadline), deadline + ); + } + + function test_useVTOrValidationTime_edge_cases() public { + // Test with zero VT and validation time + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + + // Test with maximum VT and validation time + vm.deal(alice, 1000 ether); + vm.startPrank(alice); + validatorTicket.purchaseValidatorTicket{ value: 1000 ether }(alice); + validatorTicket.approve(address(pufferProtocol), type(uint256).max); + pufferProtocol.depositValidatorTickets(alice, 0); + vm.stopPrank(); + } + + function test_settleVTAccounting_edge_cases() public { + // Test with zero VT balance + _registerValidatorKey(address(this), bytes32("alice"), PUFFER_MODULE_0, 0); + + // Test with maximum VT balance + vm.deal(alice, 1000 ether); + vm.startPrank(alice); + validatorTicket.purchaseValidatorTicket{ value: 1000 ether }(alice); + validatorTicket.approve(address(pufferProtocol), type(uint256).max); + pufferProtocol.depositValidatorTickets(alice, 0); + vm.stopPrank(); } - function _getVTBurnAmount(uint256 startEpoch, uint256 endEpoch) internal pure returns (uint256) { - uint256 validatedEpochs = endEpoch - startEpoch; - // Epoch has 32 blocks, each block is 12 seconds, we upscale to 18 decimals to get the VT amount and divide by 1 day - // The formula is validatedEpochs * 32 * 12 * 1 ether / 1 days (4444444444444444.44444444...) we round it up - return validatedEpochs * 4444444444444445; + function _getTotalEpochsValidatedMessage( + address node, + uint256 totalEpochsValidated, + uint256 nonce, + uint256 deadline + ) internal pure returns (bytes32) { + return keccak256(abi.encode(node, totalEpochsValidated, nonce, deadline)).toEthSignedMessageHash(); } } diff --git a/mainnet-contracts/test/unit/Timelock.t.sol b/mainnet-contracts/test/unit/Timelock.t.sol index ce6fc788..4df51456 100644 --- a/mainnet-contracts/test/unit/Timelock.t.sol +++ b/mainnet-contracts/test/unit/Timelock.t.sol @@ -237,7 +237,8 @@ contract TimelockTest is Test { } function test_pause_depositor_slectors(address caller) public { - vm.startPrank(timelock.pauserMultisig()); + vm.assume(caller != timelock.pauserMultisig()); + vm.assume(caller != 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); // foundry default caller vm.assume(caller != address(timelock)); vm.assume(caller != address(accessManager)); @@ -249,6 +250,7 @@ contract TimelockTest is Test { selectors[0][0] = PufferDepositor.swapAndDeposit.selector; + vm.startPrank(timelock.pauserMultisig()); timelock.pauseSelectors(targets, selectors); (bool canCall, uint32 delay) = diff --git a/mainnet-contracts/test/unit/ValidatorTicket.t.sol b/mainnet-contracts/test/unit/ValidatorTicket.t.sol index 2432aa4e..6de16dac 100644 --- a/mainnet-contracts/test/unit/ValidatorTicket.t.sol +++ b/mainnet-contracts/test/unit/ValidatorTicket.t.sol @@ -80,39 +80,6 @@ contract ValidatorTicketTest is UnitTestHelper { assertEq(validatorTicket.getGuardiansFeeRate(), 1000, "new guardians fee rate"); } - function test_funds_splitting() public { - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - - uint256 amount = vtPrice * 2000; // 20000 VTs is 20 ETH - vm.deal(address(this), amount); - - address treasury = validatorTicket.TREASURY(); - - assertEq(validatorTicket.balanceOf(address(this)), 0, "should start with 0"); - assertEq(treasury.balance, 0, "treasury balance should start with 0"); - assertEq(address(guardianModule).balance, 0, "guardian balance should start with 0"); - - validatorTicket.purchaseValidatorTicket{ value: amount }(address(this)); - - // 0.5% from 20 ETH is 0.1 ETH - assertEq(address(guardianModule).balance, 0.1 ether, "guardians balance"); - // 5% from 20 ETH is 1 ETH - assertEq(treasury.balance, 1 ether, "treasury should get 1 ETH for 100 VTs"); - } - - function test_non_whole_number_purchase() public { - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - - uint256 amount = 5.123 ether; - uint256 expectedTotal = (amount * 1 ether / vtPrice); - - vm.deal(address(this), amount); - uint256 mintedAmount = validatorTicket.purchaseValidatorTicket{ value: amount }(address(this)); - - assertEq(validatorTicket.balanceOf(address(this)), expectedTotal, "VT balance"); - assertEq(mintedAmount, expectedTotal, "minted amount"); - } - function test_zero_protocol_fee_rate() public { vm.startPrank(DAO); vm.expectEmit(true, true, true, true); @@ -121,29 +88,6 @@ contract ValidatorTicketTest is UnitTestHelper { vm.stopPrank(); // because this test is reused in other test } - function test_split_funds_no_protocol_fee_rate() public { - test_zero_protocol_fee_rate(); - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 amount = vtPrice * 2000; // 20000 VTs is 20 ETH - vm.deal(address(this), amount); - - vm.expectEmit(true, true, true, true); - emit IValidatorTicket.DispersedETH(0, 0.1 ether, 19.9 ether); - validatorTicket.purchaseValidatorTicket{ value: amount }(address(this)); - - // 0.5% from 20 ETH is 0.1 ETH - assertEq(address(guardianModule).balance, 0.1 ether, "guardians balance"); - assertEq(address(validatorTicket).balance, 0, "treasury should get 0 ETH"); - } - - function test_zero_vt_purchase() public { - // No operation tx, nothing happens but doesn't revert - vm.expectEmit(true, true, true, true); - emit IValidatorTicket.DispersedETH(0, 0, 0); - validatorTicket.purchaseValidatorTicket{ value: 0 }(address(this)); - } - /// forge-config: default.allow_internal_expect_revert = true function test_overflow_protocol_fee_rate() public { vm.startPrank(DAO); @@ -163,139 +107,7 @@ contract ValidatorTicketTest is UnitTestHelper { assertEq(validatorTicket.getProtocolFeeRate(), newFeeRate, "updated"); } - function test_purchaseValidatorTicketWithPufETH() public { - uint256 vtAmount = 10 ether; - address recipient = actors[0]; - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 requiredETH = vtAmount.mulDiv(vtPrice, 1 ether, Math.Rounding.Ceil); - - uint256 expectedPufEthUsed = pufferVault.convertToSharesUp(requiredETH); - - _givePufETH(expectedPufEthUsed, recipient); - - vm.startPrank(recipient); - pufferVault.approve(address(validatorTicket), expectedPufEthUsed); - - uint256 pufEthUsed = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, vtAmount); - vm.stopPrank(); - - assertEq(pufEthUsed, expectedPufEthUsed, "PufETH used should match expected"); - assertEq(validatorTicket.balanceOf(recipient), vtAmount, "VT balance should match requested amount"); - } - - function test_purchaseValidatorTicketWithPufETH_exchangeRateChange() public { - uint256 vtAmount = 10 ether; - address recipient = actors[2]; - - uint256 exchangeRate = pufferVault.convertToAssets(1 ether); - assertEq(exchangeRate, 1 ether, "1:1 exchange rate"); - - // Simulate + 10% increase in ETH - deal(address(pufferVault), 1110 ether); - exchangeRate = pufferVault.convertToAssets(1 ether); - assertGt(exchangeRate, 1 ether, "Now exchange rate should be greater than 1"); - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 requiredETH = vtAmount.mulDiv(vtPrice, 1 ether, Math.Rounding.Ceil); - - uint256 pufEthAmount = pufferVault.convertToSharesUp(requiredETH); - - _givePufETH(pufEthAmount, recipient); - - vm.startPrank(recipient); - pufferVault.approve(address(validatorTicket), pufEthAmount); - uint256 pufEthUsed = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, vtAmount); - vm.stopPrank(); - - assertEq(pufEthUsed, pufEthAmount, "PufETH used should match expected"); - assertEq(validatorTicket.balanceOf(recipient), vtAmount, "VT balance should match requested amount"); - } - - function test_purchaseValidatorTicketWithPufETHAndPermit() public { - uint256 vtAmount = 10 ether; - address recipient = actors[2]; - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 requiredETH = vtAmount * vtPrice / 1 ether; - - uint256 pufETHToETHExchangeRate = pufferVault.convertToAssets(1 ether); - uint256 expectedPufEthUsed = (requiredETH * 1 ether) / pufETHToETHExchangeRate; - - _givePufETH(expectedPufEthUsed, recipient); - - // Create a permit - Permit memory permit = _signPermit( - _testTemps("charlie", address(validatorTicket), expectedPufEthUsed, block.timestamp), - pufferVault.DOMAIN_SEPARATOR() - ); - - vm.prank(recipient); - uint256 pufEthUsed = validatorTicket.purchaseValidatorTicketWithPufETHAndPermit(recipient, vtAmount, permit); - - assertEq(pufEthUsed, expectedPufEthUsed, "PufETH used should match expected"); - assertEq(validatorTicket.balanceOf(recipient), vtAmount, "VT balance should match requested amount"); - } - function _givePufETH(uint256 pufEthAmount, address recipient) internal { deal(address(pufferVault), recipient, pufEthAmount); } - - function test_funds_splitting_with_pufETH() public { - uint256 vtAmount = 2000 ether; // Want to mint 2000 VTs - address recipient = actors[0]; - address treasury = validatorTicket.TREASURY(); - address operationsMultisig = validatorTicket.OPERATIONS_MULTISIG(); - - uint256 vtPrice = pufferOracle.getValidatorTicketPrice(); - uint256 requiredETH = vtAmount.mulDiv(vtPrice, 1 ether, Math.Rounding.Ceil); - - uint256 pufEthAmount = pufferVault.convertToSharesUp(requiredETH); - - _givePufETH(pufEthAmount, recipient); - - uint256 initialTreasuryBalance = pufferVault.balanceOf(treasury); - uint256 initialOpsMultisigBalance = pufferVault.balanceOf(operationsMultisig); - uint256 initialBurnedAmount = pufferVault.totalSupply(); - - vm.startPrank(recipient); - pufferVault.approve(address(validatorTicket), pufEthAmount); - uint256 pufEthUsed = validatorTicket.purchaseValidatorTicketWithPufETH(recipient, vtAmount); - vm.stopPrank(); - - assertEq(pufEthUsed, pufEthAmount, "PufETH used should match expected"); - assertEq(validatorTicket.balanceOf(recipient), vtAmount, "Should mint requested VTs"); - - uint256 expectedTreasuryAmount = pufEthAmount.mulDiv(500, 10000, Math.Rounding.Ceil); // 5% to treasury - uint256 expectedGuardianAmount = pufEthAmount.mulDiv(50, 10000, Math.Rounding.Ceil); // 0.5% to guardians - uint256 expectedBurnAmount = pufEthAmount - expectedTreasuryAmount - expectedGuardianAmount; - - assertEq( - pufferVault.balanceOf(treasury) - initialTreasuryBalance, - expectedTreasuryAmount, - "Treasury should receive 5% of pufETH" - ); - assertEq( - pufferVault.balanceOf(operationsMultisig) - initialOpsMultisigBalance, - expectedGuardianAmount, - "Operations Multisig should receive 0.5% of pufETH" - ); - assertEq( - initialBurnedAmount - pufferVault.totalSupply(), expectedBurnAmount, "Remaining pufETH should be burned" - ); - } - - function test_revert_zero_recipient() public { - uint256 vtAmount = 10 ether; - - vm.expectRevert(IValidatorTicket.RecipientIsZeroAddress.selector); - validatorTicket.purchaseValidatorTicketWithPufETH(address(0), vtAmount); - - Permit memory permit = _signPermit( - _testTemps("charlie", address(validatorTicket), vtAmount, block.timestamp), pufferVault.DOMAIN_SEPARATOR() - ); - - vm.expectRevert(IValidatorTicket.RecipientIsZeroAddress.selector); - validatorTicket.purchaseValidatorTicketWithPufETHAndPermit(address(0), vtAmount, permit); - } }