Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
72f8a26
Implemented triggerValidatorExit in PM and PMM
eladiosch Oct 22, 2025
34a1764
Added function to exit validator from PufferProtocol, and more checks
eladiosch Oct 22, 2025
9a6652d
Reverted old fmt, improved natspec and removed role
eladiosch Dec 1, 2025
737d390
Fixed codespell in dependecy
eladiosch Dec 1, 2025
5ac7b07
Merge commit '4eeb307f174bd83d131e5b1a49ba6c021145ae42' into feat/eip…
eladiosch Dec 1, 2025
57c4cdc
Fixed edge case in testing where caller is broadcaster
eladiosch Dec 1, 2025
2d9089b
Implemented PMM tests
eladiosch Dec 2, 2025
957b7d1
Added access control to the new flows
eladiosch Dec 2, 2025
9f70b7e
Fixes access control, added tests for PufferProtocol
eladiosch Dec 2, 2025
efe3eb2
Implemented extra tests
eladiosch Dec 3, 2025
6aa3c9a
Added new role for validator ejection
eladiosch Dec 9, 2025
6fed931
feat: init designing of permissioned validator
ksatyarth2 Jan 20, 2026
e99bdce
feat: add NonRestakingWithdrawalCredentials contract
ksatyarth2 Jan 20, 2026
9fd53a5
feat: add IPermissionedModule interface
ksatyarth2 Jan 20, 2026
051c2ff
feat: add PermissionedModule contract
ksatyarth2 Jan 20, 2026
3ee87ed
feat: add permissioned module and oracle
ksatyarth2 Jan 21, 2026
9d15c5d
feat: remove consolidation
ksatyarth2 Jan 26, 2026
42ac37d
feat: partial withdrawals, skip provisioning and permissionedModule s…
ksatyarth2 Jan 27, 2026
2f76042
fix: storage keccak
ksatyarth2 Jan 28, 2026
7267bf6
feat: add interfaces
ksatyarth2 Jan 28, 2026
6e7ce63
fix: compile issue due to constructor arg
ksatyarth2 Jan 28, 2026
a09ca85
feat: add permissionedModule test
ksatyarth2 Jan 28, 2026
8f65740
fix: oracle handling and index issue
ksatyarth2 Feb 3, 2026
4a2c8ab
feat: add fork test for permissioned validator flow
ksatyarth2 Feb 3, 2026
bf4e747
forge fmt
ksatyarth2 Feb 4, 2026
93466dd
feat: edge case fuzz test
ksatyarth2 Feb 9, 2026
e93a79c
feat: make NRWC upgradable
ksatyarth2 Feb 18, 2026
a941fbc
feat: PermissionedModule storage and beacon deploy
ksatyarth2 Feb 18, 2026
d25ddbe
fix: improve natspec
ksatyarth2 Feb 18, 2026
14323ed
forge fmt
ksatyarth2 Feb 18, 2026
68e66c2
fix: poc to have beacon
ksatyarth2 Feb 18, 2026
fe949d3
Merge branch 'feat/no-vt-bond' of https://github.com/PufferFinance/pu…
ksatyarth2 Feb 18, 2026
6ff2b08
fix: pufferVault natspec
ksatyarth2 Feb 18, 2026
f708ad8
fix: NRWC beacon deploy
ksatyarth2 Feb 18, 2026
b223400
forge fmt
ksatyarth2 Feb 18, 2026
7b7ad6e
feat: transfer rewards to the vault or recipient
ksatyarth2 Feb 18, 2026
aa2e98c
Merge branch 'feat/no-vt-bond' of https://github.com/PufferFinance/pu…
ksatyarth2 Feb 18, 2026
d51cf6d
forge fmt
ksatyarth2 Feb 18, 2026
55a8439
fix: error names in PMM
ksatyarth2 Feb 18, 2026
a65f7a8
feat: move beacons to constructor
ksatyarth2 Feb 23, 2026
03cc09f
feat: deployment upgrade scripts
ksatyarth2 Feb 24, 2026
0d6103f
forge fmt
ksatyarth2 Feb 24, 2026
4f85c67
fix: provisioning index error
ksatyarth2 Feb 24, 2026
4eef193
chore: remove unused imports
ksatyarth2 Feb 25, 2026
f5f2561
fix: event
ksatyarth2 Feb 25, 2026
c380837
fix: remove index as args in skip and provision
ksatyarth2 Feb 25, 2026
806bc80
Fixed compiling issues
eladiosch Mar 16, 2026
a15367c
forge fmt
eladiosch Mar 16, 2026
2b499d0
Fixed scripts and tests
eladiosch Mar 17, 2026
78a1e5e
forge fmt
eladiosch Mar 17, 2026
520a1f0
Adapted scripts and tests to hoodi
eladiosch Mar 17, 2026
20d47bc
forge fmt
eladiosch Mar 17, 2026
ad9485a
Added broadcast to _deployRevenueDepositor
eladiosch Mar 17, 2026
c0a2c5f
Adapted natspec of DeployEverything script
eladiosch Mar 17, 2026
35a337c
Adapted tests and scripts
eladiosch Mar 19, 2026
585fe5e
forge fmt
eladiosch Mar 19, 2026
81b4325
Added permissioned validators calldata to SetupAccess
eladiosch Mar 20, 2026
b3d5323
forge fmt
eladiosch Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions .github/workflows/mainnet-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,33 @@ on:

jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Codespell
uses: codespell-project/actions-codespell@v2.0
with:
path: mainnet-contracts
check_hidden: true
check_filenames: true
skip: "pnpm-lock.yaml"
name: Check for spelling errors
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Codespell
uses: codespell-project/actions-codespell@v2.0
with:
path: mainnet-contracts
check_hidden: true
check_filenames: true
skip: "pnpm-lock.yaml"

tests:
runs-on: ubuntu-latest
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.12.1

- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: 'yarn'
cache: "yarn"
cache-dependency-path: yarn.lock
node-version: 20

Expand Down Expand Up @@ -69,7 +69,7 @@ jobs:
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "forge fmt"
file_pattern: '*.sol'
file_pattern: "*.sol"

- name: List selectors
working-directory: mainnet-contracts
Expand All @@ -81,11 +81,11 @@ jobs:
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.12.1

- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: 'yarn'
cache: "yarn"
cache-dependency-path: yarn.lock
node-version: 20

Expand All @@ -106,6 +106,7 @@ jobs:
env:
ETH_RPC_URL: ${{ secrets.ETH_RPC_URL }}
HOLESKY_RPC_URL: ${{ secrets.HOLESKY_RPC_URL }}
HOODI_RPC_URL: ${{ secrets.HOODI_RPC_URL }}

- name: "Upload coverage report to Codecov"
uses: "codecov/codecov-action@v4"
Expand Down
1 change: 1 addition & 0 deletions mainnet-contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ bracket_spacing = true
[rpc_endpoints]
mainnet="${ETH_RPC_URL}"
holesky="${HOLESKY_RPC_URL}"
hoodi="${HOODI_RPC_URL}"
sepolia="${SEPOLIA_RPC_URL}"
opsepolia ="${OP_SEPOLIA_RPC_URL}"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.0 <0.9.0;

import { Script } from "forge-std/Script.sol";
import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol";
import { Multicall } from "@openzeppelin/contracts/utils/Multicall.sol";
import { PufferProtocol } from "../../src/PufferProtocol.sol";
import { PufferModuleManager } from "../../src/PufferModuleManager.sol";
import { PermissionedOracle } from "../../src/PermissionedOracle.sol";
import {
ROLE_ID_DAO,
ROLE_ID_OPERATIONS_PAYMASTER,
ROLE_ID_PUFFER_PROTOCOL,
ROLE_ID_VALIDATOR_EJECTOR,
ROLE_ID_PERMISSIONED_OPERATOR,
ROLE_ID_PERMISSIONED_ETH_MANAGER
} from "../../script/Roles.sol";

/**
* @title GeneratePermissionedModuleCalldata
* @author Puffer Finance
* @notice Generates the AccessManager calldata to set up access control for the permissioned
* validator feature: PermissionedOracle, new PufferProtocol functions, and new
* PufferModuleManager functions.
*
* The returned calldata is queued and executed through the Timelock:
* 1. timelock.queueTransaction(address(accessManager), encodedMulticall, 1)
* 2. ... 7 days later ...
* 3. timelock.executeTransaction(address(accessManager), encodedMulticall, 1)
*
* forge script script/AccessManagerMigrations/09_GeneratePermissionedModuleCalldata.s.sol \
* --sig 'run(address,address,address)' \
* <PUFFER_PROTOCOL_PROXY> <MODULE_MANAGER_PROXY> <PERMISSIONED_ORACLE> \
* -vvvv
*/
contract GeneratePermissionedModuleCalldata is Script {
function run(address pufferProtocol, address moduleManager, address permissionedOracle)
public
pure
returns (bytes memory)
{
bytes[] memory calldatas = new bytes[](9);

// 1. PermissionedOracle: restrict to PUFFER_PROTOCOL role
calldatas[0] = _setupPermissionedOracleAccess(permissionedOracle);

// 2. PufferProtocol: DAO-restricted permissioned functions
calldatas[1] = _setupProtocolDaoAccess(pufferProtocol);

// 3. PufferProtocol: paymaster-restricted permissioned functions
calldatas[2] = _setupProtocolPaymasterAccess(pufferProtocol);

// 4. PufferProtocol: permissioned-operator-restricted functions
calldatas[3] = _setupProtocolPermissionedOperatorAccess(pufferProtocol);

// 5. PufferModuleManager: DAO permissioned functions
calldatas[4] = _setupModuleManagerDaoAccess(moduleManager);

// 6. PufferModuleManager: paymaster permissioned functions
calldatas[5] = _setupModuleManagerPaymasterAccess(moduleManager);

// 7. PufferModuleManager: validator ejector permissioned functions
calldatas[6] = _setupModuleManagerEjectorAccess(moduleManager);

// 8. PufferModuleManager: dedicated role for ETH transfers out of permissioned modules
// Overrides the prior DAO assignment from SetupAccess — grant ROLE_ID_PERMISSIONED_ETH_MANAGER
// to the appropriate multisig/address via a separate DAO tx after this migration.
calldatas[7] = _setupModuleManagerEthManagerAccess(moduleManager);

// 9. Label the new role
calldatas[8] = abi.encodeWithSelector(
AccessManager.labelRole.selector, ROLE_ID_PERMISSIONED_ETH_MANAGER, "Permissioned ETH Manager"
);

bytes memory encodedMulticall = abi.encodeCall(Multicall.multicall, (calldatas));
return encodedMulticall;
}

/**
* @dev PermissionedOracle functions are restricted to PUFFER_PROTOCOL (called by PufferProtocol).
*/
function _setupPermissionedOracleAccess(address permissionedOracle) internal pure returns (bytes memory) {
bytes4[] memory selectors = new bytes4[](3);
selectors[0] = PermissionedOracle.provisionValidator.selector;
selectors[1] = PermissionedOracle.exitValidator.selector;
selectors[2] = PermissionedOracle.adjustLockedEth.selector;

return abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, permissionedOracle, selectors, ROLE_ID_PUFFER_PROTOCOL
);
}

/**
* @dev PufferProtocol DAO functions: module creation (matches createPufferModule pattern).
*/
function _setupProtocolDaoAccess(address pufferProtocol) internal pure returns (bytes memory) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = PufferProtocol.createPermissionedModule.selector;

return
abi.encodeWithSelector(AccessManager.setTargetFunctionRole.selector, pufferProtocol, selectors, ROLE_ID_DAO);
}

/**
* @dev PufferProtocol paymaster functions: provisioning and exit handling
* (matches provisionNode / batchHandleWithdrawals / skipProvisioning pattern).
*/
function _setupProtocolPaymasterAccess(address pufferProtocol) internal pure returns (bytes memory) {
bytes4[] memory selectors = new bytes4[](3);
selectors[0] = PufferProtocol.provisionPermissionedValidator.selector;
selectors[1] = PufferProtocol.handlePermissionedValidatorExit.selector;
selectors[2] = PufferProtocol.skipPermissionedProvisioning.selector;

return abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, pufferProtocol, selectors, ROLE_ID_OPERATIONS_PAYMASTER
);
}

/**
* @dev PufferProtocol permissioned operator functions: validator key registration.
* ROLE_ID_PERMISSIONED_OPERATOR (29) must be granted to operator addresses separately.
*/
function _setupProtocolPermissionedOperatorAccess(address pufferProtocol) internal pure returns (bytes memory) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = PufferProtocol.registerPermissionedValidatorKey.selector;

return abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, pufferProtocol, selectors, ROLE_ID_PERMISSIONED_OPERATOR
);
}

/**
* @dev PufferModuleManager DAO functions for permissioned modules
* (matches callDelegateTo / callUndelegate / callSetProofSubmitter / callSetClaimerFor pattern).
*/
function _setupModuleManagerDaoAccess(address moduleManager) internal pure returns (bytes memory) {
bytes4[] memory selectors = new bytes4[](4);
selectors[0] = PufferModuleManager.callDelegateToPermissioned.selector;
selectors[1] = PufferModuleManager.callUndelegatePermissioned.selector;
selectors[2] = PufferModuleManager.callSetProofSubmitterPermissioned.selector;
selectors[3] = PufferModuleManager.callSetClaimerForPermissioned.selector;

return
abi.encodeWithSelector(AccessManager.setTargetFunctionRole.selector, moduleManager, selectors, ROLE_ID_DAO);
}

/**
* @dev PufferModuleManager paymaster functions for permissioned modules:
* queue/complete withdrawals, withdraw non-restaked ETH, trigger non-restaked withdrawals.
*/
function _setupModuleManagerPaymasterAccess(address moduleManager) internal pure returns (bytes memory) {
bytes4[] memory selectors = new bytes4[](4);
selectors[0] = PufferModuleManager.callQueueWithdrawalsPermissioned.selector;
selectors[1] = PufferModuleManager.callCompleteQueuedWithdrawalsPermissioned.selector;
selectors[2] = PufferModuleManager.withdrawNonRestakedETH.selector;
selectors[3] = PufferModuleManager.triggerNonRestakedValidatorWithdrawals.selector;

return abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, moduleManager, selectors, ROLE_ID_OPERATIONS_PAYMASTER
);
}

/**
* @dev PufferModuleManager validator ejector functions for permissioned modules.
*/
function _setupModuleManagerEjectorAccess(address moduleManager) internal pure returns (bytes memory) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = PufferModuleManager.triggerRestakedValidatorsExit.selector;

return abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, moduleManager, selectors, ROLE_ID_VALIDATOR_EJECTOR
);
}

/**
* @dev transferPermissionedModuleETH gets its own dedicated role because it directly controls
* outbound ETH flow from permissioned modules and deserves independent access governance.
*/
function _setupModuleManagerEthManagerAccess(address moduleManager) internal pure returns (bytes memory) {
bytes4[] memory selectors = new bytes4[](1);
selectors[0] = PufferModuleManager.transferPermissionedModuleETH.selector;

return abi.encodeWithSelector(
AccessManager.setTargetFunctionRole.selector, moduleManager, selectors, ROLE_ID_PERMISSIONED_ETH_MANAGER
);
}
}
4 changes: 4 additions & 0 deletions mainnet-contracts/script/BaseScript.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ abstract contract BaseScript is Script {
return (block.chainid == 17000);
}

function isHoodi() internal view returns (bool) {
return (block.chainid == 560048);
}

function isAnvil() internal view returns (bool) {
return (block.chainid == 31337);
}
Expand Down
11 changes: 8 additions & 3 deletions mainnet-contracts/script/DeployEverything.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DeployPufETH, PufferDeployment } from "../script/DeployPufETH.s.sol";
import { UpgradePufETH } from "../script/UpgradePufETH.s.sol";
import { DeployPufETHBridging } from "../script/DeployPufETHBridging.s.sol";
import { DeployPufferOracle } from "script/DeployPufferOracle.s.sol";
import { DeployPermissionedOracle } from "script/DeployPermissionedOracle.s.sol";
import { GuardiansDeployment, PufferProtocolDeployment, BridgingDeployment } from "./DeploymentStructs.sol";
import { PufferRevenueDepositor } from "src/PufferRevenueDepositor.sol";
import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
Expand All @@ -22,7 +23,7 @@ import { MockAeraVault } from "test/mocks/MockAeraVault.sol";
* @author Puffer Finance
* @notice Deploys pufETH (upgrade it in test environment), Guardians, Oracle, Puffer, and sets up the access control
* @dev Example on how to run the script
* forge script script/DeployEverything.s.sol:DeployEverything --rpc-url=$RPC_URL --sig 'run(address[] calldata, uint256)' "[$DEV_WALLET]" 1 --broadcast
* forge script script/DeployEverything.s.sol:DeployEverything --rpc-url=$RPC_URL --sig 'run(address[] calldata, uint256, address)' "[$DEV_WALLET]" 1 $DEV_WALLET --broadcast
*/
contract DeployEverything is BaseScript {
address DAO;
Expand Down Expand Up @@ -51,8 +52,10 @@ contract DeployEverything is BaseScript {
puffETHDeployment.accessManager, guardiansDeployment.guardianModule, puffETHDeployment.pufferVault
);

address permissionedOracle = address(new DeployPermissionedOracle().run(puffETHDeployment.accessManager));

PufferProtocolDeployment memory pufferDeployment =
new DeployPuffer().run(guardiansDeployment, puffETHDeployment.pufferVault, pufferOracle);
new DeployPuffer().run(guardiansDeployment, puffETHDeployment.pufferVault, pufferOracle, permissionedOracle);

pufferDeployment.pufferDepositor = puffETHDeployment.pufferDepositor;
pufferDeployment.pufferVault = puffETHDeployment.pufferVault;
Expand All @@ -64,7 +67,7 @@ contract DeployEverything is BaseScript {
address revenueDepositor = _deployRevenueDepositor(puffETHDeployment);
pufferDeployment.revenueDepositor = revenueDepositor;

new UpgradePufETH().run(puffETHDeployment, pufferOracle, revenueDepositor);
new UpgradePufETH().run(puffETHDeployment, pufferOracle, revenueDepositor, permissionedOracle);

// `anvil` in the terminal
if (_localAnvil) {
Expand Down Expand Up @@ -105,6 +108,7 @@ contract DeployEverything is BaseScript {

// script/DeployRevenueDepositor.s.sol It should match the one in the script
function _deployRevenueDepositor(PufferDeployment memory puffETHDeployment) internal returns (address) {
vm.startBroadcast();
MockAeraVault mockAeraVault = new MockAeraVault();

PufferRevenueDepositor revenueDepositorImpl = new PufferRevenueDepositor({
Expand All @@ -123,6 +127,7 @@ contract DeployEverything is BaseScript {
)
)
);
vm.stopBroadcast();

bytes memory accessManagerCd =
new GenerateRevenueDepositorCalldata().run(address(revenueDepositor), makeAddr("operationsMultisig"));
Expand Down
Loading