Skip to content

Comments

Permissioned Validators#141

Draft
ksatyarth2 wants to merge 39 commits intomasterfrom
feat/no-vt-bond
Draft

Permissioned Validators#141
ksatyarth2 wants to merge 39 commits intomasterfrom
feat/no-vt-bond

Conversation

@ksatyarth2
Copy link
Contributor

What this PR does / why we need it:

Which issue(s) does this PR fixes:

Additional comments:

@ksatyarth2 ksatyarth2 requested a review from eladiosch February 4, 2026 10:56
Copy link
Contributor

@eladiosch eladiosch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few comments. I haven't dived deeply into tests or scripts. I want to validate the approach and flows first

* @dev Permissioned module beacon address (stored in contract storage for upgradeability)
* keccak256(abi.encode(uint256(keccak256("PufferModuleManager.permissionedModuleBeacon")) - 1)) & ~bytes32(uint256(0xff))
*/
bytes32 private constant _PERMISSIONED_MODULE_BEACON_SLOT =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to follow this approach to store the beacon? Why not follow the same approach with the regular puffer module beacon? It's a bit strage to have different approaches for pretty much the same thing in the same contract

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PUFFER_MODULE_BEACON is immutable and fixed at deployment of this contract, but _PERMISSIONED_MODULE_BEACON_SLOT uses storage for post deployment upgradability

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I see that. However, since both are really similar (and now also the beacon for NRWC). Using the current approach, when deploying we need to also send 2 transactions to set these beacons (right now those are not part of the deployment flow). I think it might be easy to miss and complicated the deployment flow.

If we went with the constructor arg approach it will be simpler and if we need to upgrade any of the beacons we can upgrade the PMM just deploying a new implementation instead of sending the transactions to set the beacons. It would also mean a simpler code since we remove the setter functions and the storage slots, etc

// Create EigenPod for restaked validators
$.eigenPod = IEigenPod(address(EIGEN_POD_MANAGER.createPod()));
// Deploy NonRestakingWithdrawalCredentials for non-restaked validators
$.nonRestakingWithdrawalCredentials = new NonRestakingWithdrawalCredentials(address(this), initialAuthority);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this way of deploying NonRestakingWithdralCredentials. If we need to change the withdrawal credentials contract, we have no way of upgrading the it, maybe it should be upgradeable. Imagine we found a bug in there, or a new EIP comes with new functionality to interact with the beacon deposit contract, we don't even have custom external call (EDIT: I see we do have a call function). I think it should be upgradeable

And if we have a lot of permissioned modules in the future, it would be very tedious to upgrade the contracts one by one. I think we should use a beacon proxy, just like with the modules

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense. made NRWC as beacon upgradable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. See my other comment related to how to manage the beacons

/**
* @inheritdoc IPermissionedOracle
*/
function adjustLockedEth(bytes32 moduleName, uint256 reductionAmount) external restricted {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure about this oracle approach.Since non-restaked validators don't get their rewards in the withdrawal credentials automatically (unless they get to the max 2048 ETH), the rewards will be compounding in the validator in the beaconchain. That extra ETH is not represented in our protocol in any way at the moment, so I think we should add another function here that increases the locked ETH amount that would be the auto-compounding of the rewards in the beaconchain. Otherwise, the accounting is not being totally accurate. A backend service would need to check beaconchain periodically and call this function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mentioned in natspec now, In oracle, adjustLockedEth is only for slashing/inactivity penalties, not rewards
Extra ETH (rewards) flows to module automatically, or keep compounding. They are not the part of the exchange rate ever - unless we plan to transfer the ETH to the vault. And for that we already have withdrawNonRestakedETH in PufferModuleManager that moves ETH from NRWC -> PermissionedModule.
And then we can call transferPermissionedModuleETHToVault , that moves ETH to the vault - increasing the exchange rate - which is intentional and if we plan to profit pufETH holders.
Otherwise we can add another function in PMM to utilize call() of PermissionedModule to transfer ETH to another recipient like external EOA/Multisig/Partner

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll add another function to transfer rewards to arbitrary EOA/multisig

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I get it. Since the auto compounding rewards might or might not go into the vault, it shouldn't be reflected in the exchange rate, even if 32 ETH where used for a validator that currently it's at say 1k ETH in the beacon chain

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's the whole point of having permissioned validator - the rewards are revenue to the protocol or partner who's provided the ETH/ running the validator. And it's their decision whether they want to provide benefit to pufETH holders or keep the revenue to themselves.

* If withdrawalAmount < stakeAmount, a slashing event is emitted for transparency.
* If withdrawalAmount > stakeAmount, extra is considered rewards (oracle only deducts stake).
*/
function handlePermissionedValidatorExit(bytes32 moduleName, uint256 validatorIndex, uint256 withdrawalAmount)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I understand this correctly, this function is only to adjust the eth locked in the new oracle and remove them from the storage, right?

However this only allows to adjust for the initial stake amount (or minus penalties). However, how are compounding rewards managed in non-restaking validators?

(This is related to my comment in the oracle)

Are we going to withdraw the compounding rewards from 0x02 validators periodically, as if they were 0x01? I think the auto compounding is a very interesting part of pectra validators and maybe we should use it properly, allowing for rewards to auto compound, updating the locked eth amount in the oracle, and allowing to reduce the eth of a 0x02 validator up to 32 ETH (we would need to be careful dividing the amount to the noop and the vault).

Maybe we need to give it some thought... what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

improved the natspec to describe the flow of rewards better, let me know if this is more clearer

@ksatyarth2 ksatyarth2 requested a review from eladiosch February 18, 2026 10:56
beaconDepositContract: _getBeaconDepositContract()
})
beaconDepositContract: _getBeaconDepositContract(),
permissionedOracle: IPermissionedOracle(address(0)) // TODO: set actual address
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add _getPermissionedOracle() function, like we do with puffer oracle

revenueDepositor: IPufferRevenueDepositor(_getRevenueDepositor())
});
revenueDepositor: IPufferRevenueDepositor(_getRevenueDepositor()),
permissionedOracle: IPermissionedOracle(address(0)) // TODO: set actual address
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like mentioned in other comment, should call a _getPermissionedOracle() function

beaconDepositContract: getStakingContract()
});
beaconDepositContract: getStakingContract(),
permissionedOracle: IPermissionedOracle(address(0)) // Will be set in upgrade
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think permissionedOracle should be passed as a param, just like with basic oracle

enclaveVerifier: guardiansDeployment.enclaveVerifier,
beacon: address(pufferModuleBeacon),
restakingOperatorBeacon: address(restakingOperatorBeacon),
permissionedModuleBeacon: address(0), // Set during permissioned module deployment
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is the right approach. These beacons (and the permissioned oracle) should be deployed here and assigned to the PufferProtocolDeployment. And the beacons should be set to the PMM, calling the setPermissionedModuleBeacon and setNRWCBeacon. Actually, I still think they should be set in the constructor, just like the PM beacon. Having to send different transactions for that seems inefficient and it can be easy to miss. I'll leave a comment in the PR about this subject

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, i see your point now. i'll move it to the constructor. my bad

IPufferOracleV2(pufferOracle),
IPufferRevenueDepositor(revenueDepositor)
IPufferRevenueDepositor(revenueDepositor),
IPermissionedOracle(address(0))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be passed as a param

selectors[4] = PufferModuleManager.callRegisterOperatorToAVS.selector;
selectors[5] = PufferModuleManager.callDeregisterOperatorFromAVS.selector;
selectors[6] = PufferModuleManager.customExternalCall.selector;
selectors[7] = PufferModuleManager.transferPermissionedModuleETH.selector;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some restricted functions in PMM not reflected here

* @param withdrawalCredentials The NRWC contract address
*/
event NonRestakingWithdrawalCredentialsSet(
address indexed permissionedModule, address indexed withdrawalCredentials
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the permissionedModule param here the address of the contract that emits the event? We could remove it in that case since it would be redundant

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants