diff --git a/foundry.toml b/foundry.toml index f92eb61..4a7a30b 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,5 +1,6 @@ [profile.default] src = 'src' +test = 'src/test' out = 'out' libs = ['lib'] remappings = [ diff --git a/src/eigenlayer/ObolEigenLayerPodController.sol b/src/eigenlayer/ObolEigenLayerPodController.sol index e221dbb..f47dd57 100644 --- a/src/eigenlayer/ObolEigenLayerPodController.sol +++ b/src/eigenlayer/ObolEigenLayerPodController.sol @@ -4,173 +4,155 @@ pragma solidity 0.8.19; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {Ownable} from "solady/auth/Ownable.sol"; -import { - IEigenLayerUtils, - IEigenPodManager, - IDelayedWithdrawalRouter -} from "../interfaces/IEigenLayer.sol"; - +import {IEigenLayerUtils, IEigenPodManager, IDelayedWithdrawalRouter} from "../interfaces/IEigenLayer.sol"; /// @title ObolEigenLayerPodController /// @author Obol Labs /// @notice A contract for controlling an Eigenpod and withdrawing the balance into an Obol Split /// @dev The address returned should be used as the EigenPodController address contract ObolEigenLayerPodController { - - /// @dev returned on failed call - error CallFailed(bytes data); - /// @dev If Invalid fee setup - error Invalid_FeeSetup(); - /// @dev user unauthorized - error Unauthorized(); - /// @dev contract already initialized - error AlreadyInitialized(); - - /// @dev Emiited on intialize - event Initialized(address eigenPod, address owner); - - /// ----------------------------------------------------------------------- - /// libraries - /// ----------------------------------------------------------------------- - using SafeTransferLib for address; - using SafeTransferLib for ERC20; - - uint256 internal constant PERCENTAGE_SCALE = 1e5; - - /// ----------------------------------------------------------------------- - /// storage - immutables - /// ----------------------------------------------------------------------- - - /// @notice address of Eigenlayer delegation manager - /// @dev This is the address of the delegation manager transparent proxy - address public immutable eigenLayerDelegationManager; - - /// @notice address of EigenLayerPod Manager - /// @dev this is the pod manager transparent proxy - IEigenPodManager public immutable eigenLayerPodManager; - - /// @notice address of delay withdrawal router - IDelayedWithdrawalRouter public immutable delayedWithdrawalRouter; - - /// @notice fee address - address public immutable feeRecipient; - - /// @notice fee share. Represented as an integer from 1->10000 (100%) - uint256 public immutable feeShare; - - /// ----------------------------------------------------------------------- - /// storage - /// ----------------------------------------------------------------------- - - /// @notice address of deployed Eigen pod - address public eigenPod; - - /// @notice address of a splitter - address public split; - - /// @notice address of owner - address public owner; - - - modifier onlyOwner() { - if (msg.sender != owner) { - revert Unauthorized(); - } - _; - } - - constructor( - address recipient, - uint256 share, - address delegationManager, - address eigenPodManager, - address withdrawalRouter - ) { - if (recipient != address(0) && share == 0) revert Invalid_FeeSetup(); - - feeRecipient = recipient; - feeShare = share; - eigenLayerDelegationManager = delegationManager; - eigenLayerPodManager = IEigenPodManager(eigenPodManager); - delayedWithdrawalRouter = IDelayedWithdrawalRouter(withdrawalRouter); + /// @dev returned on failed call + error CallFailed(bytes data); + /// @dev If Invalid fee setup + error Invalid_FeeSetup(); + /// @dev Invalid fee share + error Invalid_FeeShare(); + /// @dev user unauthorized + error Unauthorized(); + /// @dev contract already initialized + error AlreadyInitialized(); + + /// @dev Emiited on intialize + event Initialized(address eigenPod, address owner); + + /// ----------------------------------------------------------------------- + /// libraries + /// ----------------------------------------------------------------------- + using SafeTransferLib for address; + using SafeTransferLib for ERC20; + + uint256 internal constant PERCENTAGE_SCALE = 1e5; + + /// ----------------------------------------------------------------------- + /// storage - immutables + /// ----------------------------------------------------------------------- + + /// @notice address of Eigenlayer delegation manager + /// @dev This is the address of the delegation manager transparent proxy + address public immutable eigenLayerDelegationManager; + + /// @notice address of EigenLayerPod Manager + /// @dev this is the pod manager transparent proxy + IEigenPodManager public immutable eigenLayerPodManager; + + /// @notice address of delay withdrawal router + IDelayedWithdrawalRouter public immutable delayedWithdrawalRouter; + + /// @notice fee address + address public immutable feeRecipient; + + /// @notice fee share. Represented as an integer from 1->10000 (100%) + uint256 public immutable feeShare; + + /// ----------------------------------------------------------------------- + /// storage + /// ----------------------------------------------------------------------- + + /// @notice address of deployed Eigen pod + address public eigenPod; + + /// @notice address of a withdrawalAddress + address public withdrawalAddress; + + /// @notice address of owner + address public owner; + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + constructor( + address recipient, + uint256 share, + address delegationManager, + address eigenPodManager, + address withdrawalRouter + ) { + if (recipient != address(0) && share == 0) revert Invalid_FeeSetup(); + if (share > PERCENTAGE_SCALE) revert Invalid_FeeShare(); + + feeRecipient = recipient; + feeShare = share; + eigenLayerDelegationManager = delegationManager; + eigenLayerPodManager = IEigenPodManager(eigenPodManager); + delayedWithdrawalRouter = IDelayedWithdrawalRouter(withdrawalRouter); + } + + /// @dev Enables contract to receive ETH + // defined on the clone implementation + // receive() external payable {} + + /// @notice initializes the controller + /// @param _owner address of the controller owner + /// @param _withdrawalAddress address to receive funds + function initialize(address _owner, address _withdrawalAddress) external { + if (owner != address(0)) revert AlreadyInitialized(); + + eigenPod = eigenLayerPodManager.createPod(); + owner = _owner; + withdrawalAddress = _withdrawalAddress; + + emit Initialized(eigenPod, _owner); + } + + /// @notice Call the eigenPod contract + /// @param data to call eigenPod contract + function callEigenPod(bytes calldata data) external payable onlyOwner { + _executeCall(address(eigenPod), msg.value, data); + } + + /// @notice Call the Eigenlayer delegation Manager contract + /// @param data to call eigenPod contract + function callDelegationManager(bytes calldata data) external payable onlyOwner { + _executeCall(address(eigenLayerDelegationManager), msg.value, data); + } + + /// @notice Call the Eigenlayer Manager contract + /// @param data to call contract + function callEigenPodManager(bytes calldata data) external payable onlyOwner { + _executeCall(address(eigenLayerPodManager), msg.value, data); + } + + /// @notice Withdraw funds from the delayed withdrawal router + /// @param numberOfDelayedWithdrawalsToClaim number of claims + function claimDelayedWithdrawals(uint256 numberOfDelayedWithdrawalsToClaim) external { + delayedWithdrawalRouter.claimDelayedWithdrawals(address(this), numberOfDelayedWithdrawalsToClaim); + + // transfer eth to withdrawalAddress + uint256 balance = address(this).balance; + if (feeShare > 0) { + uint256 fee = (balance * feeShare) / PERCENTAGE_SCALE; + feeRecipient.safeTransferETH(fee); + withdrawalAddress.safeTransferETH(balance -= fee); + } else { + withdrawalAddress.safeTransferETH(balance); } - - /// @dev Enables contract to receive ETH - // defined on the clone implementation - // receive() external payable {} - - /// @notice initializes the controller - /// @param _owner address of the controller owner - /// @param splitter address of splitter - function initialize(address _owner, address splitter) external { - if (owner != address(0)) revert AlreadyInitialized(); - - eigenPod = eigenLayerPodManager.createPod(); - owner = _owner; - split = splitter; - - emit Initialized(eigenPod, _owner); - } - - /// @notice Call the eigenPod contract - /// @param data to call eigenPod contract - function callEigenPod( - bytes calldata data - ) external payable onlyOwner { - _executeCall(address(eigenPod), msg.value, data); - } - - /// @notice Call the Eigenlayer delegation Manager contract - /// @param data to call eigenPod contract - function callDelegationManager( - bytes calldata data - ) external payable onlyOwner { - _executeCall(address(eigenLayerDelegationManager), msg.value, data); - } - - /// @notice Call the Eigenlayer Manager contract - /// @param data to call contract - function callEigenPodManager( - bytes calldata data - ) external payable onlyOwner { - _executeCall(address(eigenLayerPodManager), msg.value, data); - } - - /// @notice Withdraw funds from the delayed withdrawal router - /// @param numberOfDelayedWithdrawalsToClaim number of claims - function claimDelayedWithdrawals(uint256 numberOfDelayedWithdrawalsToClaim) - external - { - delayedWithdrawalRouter.claimDelayedWithdrawals( - address(this), - numberOfDelayedWithdrawalsToClaim - ); - - // transfer eth to split - uint256 balance = address(this).balance; - if (feeShare > 0) { - uint256 fee = (balance * feeShare) / PERCENTAGE_SCALE; - feeRecipient.safeTransferETH(fee); - split.safeTransferETH(balance -= fee); - } else { - split.safeTransferETH(address(this).balance); - } - } - - /// @notice Rescue stuck tokens by sending them to the split contract. - /// @param token address of token - /// @param amount amount of token to rescue - function rescueFunds(address token, uint256 amount) external { - if (amount > 0) ERC20(token).safeTransfer(split, amount); - } - - /// @notice Execute a low level call - /// @param to address to execute call - /// @param value amount of ETH to send with call - /// @param data bytes array to execute - function _executeCall(address to, uint256 value, bytes memory data) internal { - (bool success,) = address(to).call{value: value}(data); - if (!success) revert CallFailed(data); - } - -} \ No newline at end of file + } + + /// @notice Rescue stuck tokens by sending them to the split contract. + /// @param token address of token + /// @param amount amount of token to rescue + function rescueFunds(address token, uint256 amount) external { + if (amount > 0) ERC20(token).safeTransfer(withdrawalAddress, amount); + } + + /// @notice Execute a low level call + /// @param to address to execute call + /// @param value amount of ETH to send with call + /// @param data bytes array to execute + function _executeCall(address to, uint256 value, bytes memory data) internal { + (bool success,) = address(to).call{value: value}(data); + if (!success) revert CallFailed(data); + } +} diff --git a/src/eigenlayer/ObolEigenLayerPodControllerFactory.sol b/src/eigenlayer/ObolEigenLayerPodControllerFactory.sol index bec4826..2716e01 100644 --- a/src/eigenlayer/ObolEigenLayerPodControllerFactory.sol +++ b/src/eigenlayer/ObolEigenLayerPodControllerFactory.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.19; + import {ObolEigenLayerPodController} from "./ObolEigenLayerPodController.sol"; import {LibClone} from "solady/utils/LibClone.sol"; @@ -8,71 +9,65 @@ import {LibClone} from "solady/utils/LibClone.sol"; /// @notice A factory contract for cheaply deploying ObolLidoEigenLayer. /// @dev The address returned should be used to as the EigenPod address contract ObolEigenLayerPodControllerFactory { + error Invalid_Owner(); + error Invalid_WithdrawalAddress(); + error Invalid_DelegationManager(); + error Invalid_EigenPodManaager(); + error Invalid_WithdrawalRouter(); + + using LibClone for address; - error Invalid_Owner(); - error Invalid_Split(); - error Invalid_DelegationManager(); - error Invalid_EigenPodManaager(); - error Invalid_WithdrawalRouter(); + event CreatePodController(address indexed controller, address indexed withdrawalAddress, address owner); - using LibClone for address; + ObolEigenLayerPodController public immutable controllerImplementation; - event CreatePodController( - address indexed controller, - address indexed split, - address owner - ); - - ObolEigenLayerPodController public immutable controllerImplementation; + constructor( + address feeRecipient, + uint256 feeShare, + address delegationManager, + address eigenPodManager, + address withdrawalRouter + ) { + if (delegationManager == address(0)) revert Invalid_DelegationManager(); + if (eigenPodManager == address(0)) revert Invalid_EigenPodManaager(); + if (withdrawalRouter == address(0)) revert Invalid_WithdrawalRouter(); - constructor( - address feeRecipient, - uint256 feeShare, - address delegationManager, - address eigenPodManager, - address withdrawalRouter - ) { - if (delegationManager == address(0)) revert Invalid_DelegationManager(); - if (eigenPodManager == address(0)) revert Invalid_EigenPodManaager(); - if (withdrawalRouter == address(0)) revert Invalid_WithdrawalRouter(); + controllerImplementation = + new ObolEigenLayerPodController(feeRecipient, feeShare, delegationManager, eigenPodManager, withdrawalRouter); + // initialize implementation + controllerImplementation.initialize(feeRecipient, feeRecipient); + } - controllerImplementation = new ObolEigenLayerPodController( - feeRecipient, - feeShare, - delegationManager, - eigenPodManager, - withdrawalRouter - ); + /// Creates a minimal proxy clone of implementation + /// @param owner address of owner + /// @param withdrawalAddress address of withdrawalAddress + /// @return controller Deployed obol eigen layer controller + function createPodController(address owner, address withdrawalAddress) external returns (address controller) { + if (owner == address(0)) revert Invalid_Owner(); + if (withdrawalAddress == address(0)) revert Invalid_WithdrawalAddress(); - controllerImplementation.initialize( - feeRecipient, - feeRecipient - ); - } + bytes32 salt = _createSalt(owner, withdrawalAddress); - /// Creates a minimal proxy clone of implementation - /// @param owner address of owner - /// @param split address of split - /// @return controller Deployed obol eigen layer controller - function createPodController(address owner, address split) - external - returns (address controller) - { - if (owner == address(0)) revert Invalid_Owner(); - if (split == address(0)) revert Invalid_Split(); + controller = address(controllerImplementation).cloneDeterministic("", salt); - controller = address(controllerImplementation).clone(""); + ObolEigenLayerPodController(controller).initialize(owner, withdrawalAddress); - ObolEigenLayerPodController(controller).initialize( - owner, - split - ); + emit CreatePodController(controller, withdrawalAddress, owner); + } - emit CreatePodController( - controller, - split, - owner - ); - } + /// Predict the controller address + /// @param owner address of owner + /// @param withdrawalAddress address to withdraw funds to + function predictControllerAddress(address owner, address withdrawalAddress) + external + view + returns (address controller) + { + bytes32 salt = _createSalt(owner, withdrawalAddress); + controller = address(controllerImplementation).predictDeterministicAddress("", salt, address(this)); + } -} \ No newline at end of file + function _createSalt(address owner, address withdrawalAddress) internal pure returns (bytes32 salt) { + return keccak256(abi.encode(owner, withdrawalAddress)); + } +} diff --git a/src/test/eigenlayer/EigenLayerTestBase.sol b/src/test/eigenlayer/EigenLayerTestBase.sol new file mode 100644 index 0000000..f0aa775 --- /dev/null +++ b/src/test/eigenlayer/EigenLayerTestBase.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import { + IEigenPod, + IDelegationManager, + IEigenPodManager, + IEigenLayerUtils, + IDelayedWithdrawalRouter +} from "src/interfaces/IEigenLayer.sol"; + +abstract contract EigenLayerTestBase is Test { + uint256 public constant PERCENTAGE_SCALE = 1e5; + + address public constant SPLIT_MAIN_GOERLI = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + address public constant ENS_REVERSE_REGISTRAR_GOERLI = 0x084b1c3C81545d370f3634392De611CaaBFf8148; + + address public constant DEPOSIT_CONTRACT_GOERLI = 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b; + address public constant DELEGATION_MANAGER_GOERLI = 0x1b7b8F6b258f95Cf9596EabB9aa18B62940Eb0a8; + address public constant POD_MANAGER_GOERLI = 0xa286b84C96aF280a49Fe1F40B9627C2A2827df41; + address public constant DELAY_ROUTER_GOERLI = 0x89581561f1F98584F88b0d57c2180fb89225388f; + // eigenlayer admin + address public constant DELAY_ROUTER_OWNER_GOERLI = 0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF; + address public constant EIGEN_LAYER_OPERATOR_GOERLI = 0x3DeD1CB5E25FE3eC9811B918A809A371A4965A5D; + + uint256 internal constant BALANCE_CLASSIFICATION_THRESHOLD = 16 ether; + + function encodeEigenPodCall(address recipient, uint256 amount) internal pure returns (bytes memory callData) { + callData = abi.encodeCall(IEigenPod.withdrawNonBeaconChainETHBalanceWei, (recipient, amount)); + } + + function encodeDelegationManagerCall(address operator) internal pure returns (bytes memory callData) { + IEigenLayerUtils.SignatureWithExpiry memory signature = IEigenLayerUtils.SignatureWithExpiry(bytes(""), 0); + callData = abi.encodeCall(IDelegationManager.delegateTo, (operator, signature, bytes32(0))); + } + + function encodeEigenPodManagerCall(uint256) internal pure returns (bytes memory callData) { + bytes memory pubkey = bytes(""); + bytes memory signature = bytes(""); + bytes32 dataRoot = bytes32(0); + + callData = abi.encodeCall(IEigenPodManager.stake, (pubkey, signature, dataRoot)); + } + + function _min(uint256 a, uint256 b) internal pure returns (uint256 min) { + min = a > b ? b : a; + } + + function _max(uint256 a, uint256 b) internal pure returns (uint256 max) { + max = a > b ? a : b; + } +} diff --git a/src/test/eigenlayer/OELPCFactory.t.sol b/src/test/eigenlayer/OELPCFactory.t.sol index dd7a05b..e46514f 100644 --- a/src/test/eigenlayer/OELPCFactory.t.sol +++ b/src/test/eigenlayer/OELPCFactory.t.sol @@ -1,119 +1,88 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.19; + import "forge-std/Test.sol"; -import { ObolEigenLayerPodControllerFactory } from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; - -contract ObolEigenLayerPodControllerFactoryTest is Test { - error Invalid_Owner(); - error Invalid_Split(); - error Invalid_DelegationManager(); - error Invalid_EigenPodManaager(); - error Invalid_WithdrawalRouter(); - - event CreatePodController( - address indexed controller, - address indexed split, - address owner +import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; +import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; + +contract ObolEigenLayerPodControllerFactoryTest is EigenLayerTestBase { + error Invalid_Owner(); + error Invalid_WithdrawalAddress(); + error Invalid_DelegationManager(); + error Invalid_EigenPodManaager(); + error Invalid_WithdrawalRouter(); + + event CreatePodController(address indexed controller, address indexed split, address owner); + + ObolEigenLayerPodControllerFactory factory; + + address owner; + address user1; + address withdrawalAddress; + address feeRecipient; + + uint256 feeShare; + + function setUp() public { + uint256 goerliBlock = 10_205_449; + vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + withdrawalAddress = makeAddr("withdrawalAddress"); + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e3; + + factory = new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + } + + function test_RevertIfInvalidDelegationManger() external { + vm.expectRevert(Invalid_DelegationManager.selector); + new ObolEigenLayerPodControllerFactory(feeRecipient, feeShare, address(0), POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI); + } + + function test_RevertIfInvalidPodManger() external { + vm.expectRevert(Invalid_EigenPodManaager.selector); + new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, address(0), DELAY_ROUTER_GOERLI + ); + } + + function test_RevertIfInvalidWithdrawalRouter() external { + vm.expectRevert(Invalid_WithdrawalRouter.selector); + new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, address(0) ); + } + + function test_RevertIfOwnerIsZero() external { + vm.expectRevert(Invalid_Owner.selector); + factory.createPodController(address(0), withdrawalAddress); + } + + function test_RevertIfOWRIsZero() external { + vm.expectRevert(Invalid_WithdrawalAddress.selector); + factory.createPodController(user1, address(0)); + } + + function test_CreatePodController() external { + vm.expectEmit(false, false, false, true); - address DELEGATION_MANAGER_GOERLI = 0x1b7b8F6b258f95Cf9596EabB9aa18B62940Eb0a8; - address POD_MANAGER_GOERLI = 0xa286b84C96aF280a49Fe1F40B9627C2A2827df41; - address DELAY_ROUTER_GOERLI = 0x89581561f1F98584F88b0d57c2180fb89225388f; - - ObolEigenLayerPodControllerFactory factory; - - address owner; - address user1; - address splitter; - address feeRecipient; - - uint256 feeShare; - - function setUp() public { - uint256 goerliBlock = 10_205_449; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); - - owner = makeAddr("owner"); - user1 = makeAddr("user1"); - splitter = makeAddr("splitter"); - feeRecipient = makeAddr("feeRecipient"); - feeShare = 1e3; - - factory = new ObolEigenLayerPodControllerFactory( - feeRecipient, - feeShare, - DELEGATION_MANAGER_GOERLI, - POD_MANAGER_GOERLI, - DELAY_ROUTER_GOERLI - ); - } - - function test_RevertIfInvalidDelegationManger() external { - vm.expectRevert(Invalid_DelegationManager.selector); - new ObolEigenLayerPodControllerFactory( - feeRecipient, - feeShare, - address(0), - POD_MANAGER_GOERLI, - DELAY_ROUTER_GOERLI - ); - } - - function test_RevertIfInvalidPodManger() external { - vm.expectRevert(Invalid_EigenPodManaager.selector); - new ObolEigenLayerPodControllerFactory( - feeRecipient, - feeShare, - DELEGATION_MANAGER_GOERLI, - address(0), - DELAY_ROUTER_GOERLI - ); - } - - function test_RevertIfInvalidWithdrawalRouter() external { - vm.expectRevert(Invalid_WithdrawalRouter.selector); - new ObolEigenLayerPodControllerFactory( - feeRecipient, - feeShare, - DELEGATION_MANAGER_GOERLI, - POD_MANAGER_GOERLI, - address(0) - ); - } - - function test_RevertIfOwnerIsZero() external { - vm.expectRevert(Invalid_Owner.selector); - factory.createPodController( - address(0), - splitter - ); - } - - function test_RevertIfSplitIsZero() external { - vm.expectRevert(Invalid_Split.selector); - factory.createPodController( - user1, - address(0) - ); - } - - function test_CreatePodController() external { - vm.expectEmit( - false, - false, - false, - true - ); - - emit CreatePodController( - address(0), - splitter, - user1 - ); - - factory.createPodController( - user1, - splitter - ); - } -} \ No newline at end of file + emit CreatePodController(address(0), withdrawalAddress, user1); + + address predictedAddress = factory.predictControllerAddress( + user1, + withdrawalAddress + ); + + address createdAddress = factory.createPodController(user1, withdrawalAddress); + + assertEq( + predictedAddress, + createdAddress, + "predicted address is equivalent" + ); + } +} diff --git a/src/test/eigenlayer/ObolEigenLayerPodController.t.sol b/src/test/eigenlayer/ObolEigenLayerPodController.t.sol index 63f6774..7372d47 100644 --- a/src/test/eigenlayer/ObolEigenLayerPodController.t.sol +++ b/src/test/eigenlayer/ObolEigenLayerPodController.t.sol @@ -1,254 +1,227 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; + import "forge-std/Test.sol"; -import { ObolEigenLayerPodController } from "src/eigenlayer/ObolEigenLayerPodController.sol"; -import { ObolEigenLayerPodControllerFactory } from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; -import { IEigenPod, IDelegationManager, IEigenPodManager, IEigenLayerUtils, IDelayedWithdrawalRouter } from "src/interfaces/IEigenLayer.sol"; +import {ObolEigenLayerPodController} from "src/eigenlayer/ObolEigenLayerPodController.sol"; +import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; +import { + IEigenPod, + IDelegationManager, + IEigenPodManager, + IEigenLayerUtils, + IDelayedWithdrawalRouter +} from "src/interfaces/IEigenLayer.sol"; import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; +import { + OptimisticWithdrawalRecipientFactory, + OptimisticWithdrawalRecipient +} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; +import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; interface IDepositContract { - - function deposit( - bytes calldata pubkey, - bytes calldata withdrawal_credentials, - bytes calldata signature, - bytes32 deposit_data_root - ) external payable; - + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable; } -contract ObolEigenLayerPodControllerTest is Test { - error Unauthorized(); - error AlreadyInitialized(); - - uint256 internal constant PERCENTAGE_SCALE = 1e5; - - - address constant DEPOSIT_CONTRACT_GOERLI = 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b; - address constant DELEGATION_MANAGER_GOERLI = 0x1b7b8F6b258f95Cf9596EabB9aa18B62940Eb0a8; - address constant POD_MANAGER_GOERLI = 0xa286b84C96aF280a49Fe1F40B9627C2A2827df41; - address constant DELAY_ROUTER_GOERLI = 0x89581561f1F98584F88b0d57c2180fb89225388f; - - address constant EIGEN_LAYER_OPERATOR_GOERLI = 0x3DeD1CB5E25FE3eC9811B918A809A371A4965A5D; - - ObolEigenLayerPodControllerFactory factory; - ObolEigenLayerPodController controller; - address owner; - address user1; - address user2; - address splitter; - address feeRecipient; - - uint256 feeShare; - - MockERC20 mERC20; - - function setUp() public { - uint256 goerliBlock = 10_205_449; - vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); +contract ObolEigenLayerPodControllerTest is EigenLayerTestBase { + error Unauthorized(); + error AlreadyInitialized(); + error Invalid_FeeShare(); + error CallFailed(bytes); + + ObolEigenLayerPodControllerFactory factory; + ObolEigenLayerPodControllerFactory zeroFeeFactory; + + ObolEigenLayerPodController controller; + ObolEigenLayerPodController zeroFeeController; + + address owner; + address user1; + address user2; + address withdrawalAddress; + address principalRecipient; + address feeRecipient; + + uint256 feeShare; + + MockERC20 mERC20; + + function setUp() public { + uint256 goerliBlock = 10_205_449; + vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + + vm.mockCall( + DEPOSIT_CONTRACT_GOERLI, abi.encodeWithSelector(IDepositContract.deposit.selector), bytes.concat(bytes32(0)) + ); + + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + user1 = makeAddr("user2"); + principalRecipient = makeAddr("principalRecipient"); + withdrawalAddress = makeAddr("withdrawalAddress"); + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e3; + + factory = new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + + zeroFeeFactory = new ObolEigenLayerPodControllerFactory( + address(0), 0, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + + controller = ObolEigenLayerPodController(factory.createPodController(owner, withdrawalAddress)); + zeroFeeController = ObolEigenLayerPodController(zeroFeeFactory.createPodController(owner, withdrawalAddress)); + + mERC20 = new MockERC20("Test Token", "TOK", 18); + mERC20.mint(type(uint256).max); + + vm.prank(DELAY_ROUTER_OWNER_GOERLI); + // set the delay withdrawal duration to zero + IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); + } + + function test_RevertIfInvalidFeeShare() external { + vm.expectRevert(Invalid_FeeShare.selector); + new ObolEigenLayerPodControllerFactory( + feeRecipient, 1e7, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + } + + function test_RevertIfNotOwnerCallEigenPod() external { + vm.prank(user1); + vm.expectRevert(Unauthorized.selector); + controller.callEigenPod(encodeEigenPodCall(user1, 1 ether)); + } + + function test_RevertIfDoubleInitialize() external { + vm.prank(user1); + vm.expectRevert(AlreadyInitialized.selector); + controller.initialize(owner, withdrawalAddress); + } + + function test_CallEigenPod() external { + address pod = controller.eigenPod(); + uint256 amount = 1 ether; + + // airdrop ether to pod + (bool success,) = pod.call{value: amount}(""); + require(success, "call failed"); + + vm.prank(owner); + controller.callEigenPod(encodeEigenPodCall(user1, amount)); + } + + function test_CallDelegationManager() external { + vm.prank(owner); + controller.callDelegationManager(encodeDelegationManagerCall(EIGEN_LAYER_OPERATOR_GOERLI)); + } + + function test_OnlyOwnerCallDelegationManager() external { + vm.prank(user1); + vm.expectRevert(Unauthorized.selector); + controller.callDelegationManager(encodeDelegationManagerCall(EIGEN_LAYER_OPERATOR_GOERLI)); + } + + function test_CallEigenPodManager() external { + uint256 etherStake = 32 ether; + vm.deal(owner, etherStake + 1 ether); + vm.prank(owner); + controller.callEigenPodManager{value: etherStake}(encodeEigenPodManagerCall(0)); + } + + function test_OnlyOwnerEigenPodManager() external { + vm.expectRevert(Unauthorized.selector); + controller.callEigenPodManager(encodeEigenPodManagerCall(0)); + } + + function test_ClaimDelayedWithdrawals() external { + uint256 amountToDeposit = 2 ether; + + // transfer unstake beacon eth to eigenPod + (bool success,) = address(controller.eigenPod()).call{value: amountToDeposit}(""); + require(success, "call failed"); - vm.mockCall( - DEPOSIT_CONTRACT_GOERLI, abi.encodeWithSelector(IDepositContract.deposit.selector), bytes.concat(bytes32(0)) - ); + vm.startPrank(owner); + { + controller.callEigenPod(encodeEigenPodCall(address(controller), amountToDeposit)); + controller.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + assertEq(address(feeRecipient).balance, 20_000_000_000_000_000, "fee recipient balance increased"); + assertEq(address(withdrawalAddress).balance, 1_980_000_000_000_000_000, "withdrawal balance increased"); + } + + function test_ClaimDelayedWithdrawalsZeroFee() external { + uint256 amountToDeposit = 20 ether; + + // transfer unstake beacon eth to eigenPod + (bool success,) = address(zeroFeeController.eigenPod()).call{value: amountToDeposit}(""); + require(success, "call failed"); + + vm.startPrank(owner); + { + zeroFeeController.callEigenPod(encodeEigenPodCall(address(zeroFeeController), amountToDeposit)); + zeroFeeController.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + assertEq(address(withdrawalAddress).balance, amountToDeposit, "withdrawal balance increased"); + } - owner = makeAddr("owner"); - user1 = makeAddr("user1"); - user1 = makeAddr("user2"); - splitter = makeAddr("splitter"); - feeRecipient = makeAddr("feeRecipient"); - feeShare = 1e3; + function test_InvalidCallReverts() external { + uint256 amountToDeposit = 20 ether; + bytes memory data = encodeEigenPodCall(address(0x2), amountToDeposit); + vm.expectRevert(abi.encodeWithSelector(CallFailed.selector, data)); + vm.prank(owner); + zeroFeeController.callEigenPod(data); + vm.stopPrank(); + } + + function testFuzz_ClaimDelayedWithdrawals(uint256 amount) external { + amount = bound(amount, _min(amount, address(this).balance), type(uint96).max); + + address DELAY_ROUTER_OWNER = 0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF; + vm.prank(DELAY_ROUTER_OWNER); + // set the delay withdrawal duration to zero + IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); - factory = new ObolEigenLayerPodControllerFactory( - feeRecipient, - feeShare, - DELEGATION_MANAGER_GOERLI, - POD_MANAGER_GOERLI, - DELAY_ROUTER_GOERLI - ); + // transfer unstake beacon eth to eigenPod + (bool success,) = address(controller.eigenPod()).call{value: amount}(""); + require(success, "call failed"); - controller = ObolEigenLayerPodController(factory.createPodController( - owner, - splitter - )); - - mERC20 = new MockERC20("Test Token", "TOK", 18); - mERC20.mint(type(uint256).max); - - } - - function test_RevertIfNotOwnerCallEigenPod() external { - vm.prank(user1); - vm.expectRevert(Unauthorized.selector); - controller.callEigenPod( - encodeEigenPodCall(user1, 1 ether) - ); - } - - function test_RevertIfDoubleInitialize() external { - vm.prank(user1); - vm.expectRevert(AlreadyInitialized.selector); - controller.initialize( - owner, - splitter - ); + vm.startPrank(owner); + { + controller.callEigenPod(encodeEigenPodCall(address(controller), amount)); + controller.claimDelayedWithdrawals(1); } + vm.stopPrank(); - function test_CallEigenPod() external { - address pod = controller.eigenPod(); - uint256 amount = 1 ether; + uint256 fee = amount * feeShare / PERCENTAGE_SCALE; - // airdrop ether to pod - (bool success,) = pod.call{value: amount}(""); - require(success, "call failed"); + assertEq(address(feeRecipient).balance, fee, "invalid fee"); - vm.prank(owner); - controller.callEigenPod( - encodeEigenPodCall(user1, amount) - ); - } + assertEq(address(withdrawalAddress).balance, amount -= fee, "invalid withdrawalAddress balance"); + } - function test_CallDelegationManager() external { - vm.prank(owner); - controller.callDelegationManager( - encodeDelegationManagerCall( - EIGEN_LAYER_OPERATOR_GOERLI - ) - ); - } + function test_RescueFunds() external { + uint256 amount = 1e18; + mERC20.transfer(address(controller), amount); - function test_OnlyOwnerCallDelegationManager() external { - vm.prank(user1); - vm.expectRevert(Unauthorized.selector); - controller.callDelegationManager( - encodeDelegationManagerCall(EIGEN_LAYER_OPERATOR_GOERLI) - ); - } + controller.rescueFunds(address(mERC20), amount); - function test_CallEigenPodManager() external { - uint256 etherStake = 32 ether; - vm.deal(owner, etherStake + 1 ether); - vm.prank(owner); - controller.callEigenPodManager{value: etherStake}( - encodeEigenPodManagerCall(0) - ); - } + assertEq(mERC20.balanceOf(withdrawalAddress), amount, "could not rescue funds"); + } - function test_OnlyOwnerEigenPodManager() external { - vm.expectRevert(Unauthorized.selector); - controller.callEigenPodManager( - encodeEigenPodManagerCall(0) - ); - } + function test_RescueFundsZero() external { + uint256 amount = 0; + controller.rescueFunds(address(mERC20), amount); - - function test_ClaimDelayedWithdrawals() external { - address DELAY_ROUTER_OWNER = 0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF ; - vm.prank(DELAY_ROUTER_OWNER); - // set the delay withdrawal duration to zero - IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); - - uint256 amountToDeposit = 2 ether; - - // transfer unstake beacon eth to eigenPod - (bool success, ) = address(controller.eigenPod()).call{value: amountToDeposit}(""); - require(success, "call failed"); - - vm.startPrank(owner); - { - controller.callEigenPod( - encodeEigenPodCall(address(controller), amountToDeposit) - ); - controller.claimDelayedWithdrawals(1); - } - vm.stopPrank(); - - assertEq( - address(feeRecipient).balance, - 20000000000000000, - "user balance increased" - ); - assertEq( - address(splitter).balance, - 1980000000000000000, - "user balance increased" - ); - } - - function testFuzz_ClaimDelayedWithdrawals(uint256 amount) external { - amount = bound(amount, _min(amount, address(this).balance), type(uint96).max); - - address DELAY_ROUTER_OWNER = 0x37bAFb55BC02056c5fD891DFa503ee84a97d89bF ; - vm.prank(DELAY_ROUTER_OWNER); - // set the delay withdrawal duration to zero - IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); - - // transfer unstake beacon eth to eigenPod - (bool success, ) = address(controller.eigenPod()).call{value: amount}(""); - require(success, "call failed"); - - vm.startPrank(owner); - { - controller.callEigenPod( - encodeEigenPodCall(address(controller), amount) - ); - controller.claimDelayedWithdrawals(1); - } - vm.stopPrank(); - - uint256 fee = amount * feeShare / PERCENTAGE_SCALE; - - assertEq( - address(feeRecipient).balance, - fee, - "invalid fee" - ); - - assertEq( - address(splitter).balance, - amount -= fee, - "invalid splitter balance" - ); - - } - - function test_RescueFunds() external { - uint256 amount = 1e18; - mERC20.transfer(address(controller), amount); - - controller.rescueFunds(address(mERC20), amount); - - assertEq( - mERC20.balanceOf(splitter), - amount, - "could not rescue funds" - ); - } - - function encodeEigenPodCall(address recipient, uint256 amount) internal pure returns (bytes memory callData) { - callData = abi.encodeCall(IEigenPod.withdrawNonBeaconChainETHBalanceWei, (recipient, amount)); - } - - function encodeDelegationManagerCall(address operator) internal pure returns (bytes memory callData) { - IEigenLayerUtils.SignatureWithExpiry memory signature = IEigenLayerUtils.SignatureWithExpiry( - bytes(""), 0 - ); - callData = abi.encodeCall(IDelegationManager.delegateTo, (operator, signature, bytes32(0))); - } - - function encodeEigenPodManagerCall(uint256) internal pure returns (bytes memory callData) { - bytes memory pubkey = bytes(""); - bytes memory signature = bytes(""); - bytes32 dataRoot = bytes32(0); - - callData = abi.encodeCall(IEigenPodManager.stake, (pubkey, signature, dataRoot)); - } - - function _min(uint256 a, uint256 b) internal pure returns (uint256 min) { - min = a > b ? b : a; - } - - function _max(uint256 a, uint256 b) internal pure returns (uint256 max) { - max = a > b ? a : b; - } - -} \ No newline at end of file + assertEq(mERC20.balanceOf(withdrawalAddress), amount, "balance should be zero"); + } +} diff --git a/src/test/eigenlayer/integration/OELPCIntegration.t.sol b/src/test/eigenlayer/integration/OELPCIntegration.t.sol new file mode 100644 index 0000000..2e87f4a --- /dev/null +++ b/src/test/eigenlayer/integration/OELPCIntegration.t.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {ObolEigenLayerPodController} from "src/eigenlayer/ObolEigenLayerPodController.sol"; +import {ObolEigenLayerPodControllerFactory} from "src/eigenlayer/ObolEigenLayerPodControllerFactory.sol"; +import { + IEigenPod, + IDelegationManager, + IEigenPodManager, + IEigenLayerUtils, + IDelayedWithdrawalRouter +} from "src/interfaces/IEigenLayer.sol"; +import {MockERC20} from "src/test/utils/mocks/MockERC20.sol"; +import {ISplitMain} from "src/interfaces/ISplitMain.sol"; +import { + OptimisticWithdrawalRecipientFactory, + OptimisticWithdrawalRecipient +} from "src/owr/OptimisticWithdrawalRecipientFactory.sol"; +import {IENSReverseRegistrar} from "../../../interfaces/IENSReverseRegistrar.sol"; +import {EigenLayerTestBase} from "src/test/eigenlayer/EigenLayerTestBase.sol"; + +contract OELPCIntegration is EigenLayerTestBase { + ObolEigenLayerPodControllerFactory factory; + ObolEigenLayerPodController owrController; + ObolEigenLayerPodController splitController; + + address[] accounts; + uint32[] percentAllocations; + + address owner; + address user1; + address user2; + + address owrWithdrawalAddress; + address splitWithdrawalAddress; + + address principalRecipient; + address rewardRecipient; + address feeRecipient; + + uint256 feeShare; + + function setUp() public { + uint256 goerliBlock = 10_205_449; + vm.createSelectFork(getChain("goerli").rpcUrl, goerliBlock); + + vm.mockCall( + ENS_REVERSE_REGISTRAR_GOERLI, + abi.encodeWithSelector(IENSReverseRegistrar.setName.selector), + bytes.concat(bytes32(0)) + ); + vm.mockCall( + ENS_REVERSE_REGISTRAR_GOERLI, + abi.encodeWithSelector(IENSReverseRegistrar.claim.selector), + bytes.concat(bytes32(0)) + ); + + owner = makeAddr("owner"); + user1 = makeAddr("user1"); + user1 = makeAddr("user2"); + principalRecipient = makeAddr("principalRecipient"); + rewardRecipient = makeAddr("rewardRecipient"); + feeRecipient = makeAddr("feeRecipient"); + feeShare = 1e3; + + OptimisticWithdrawalRecipientFactory owrFactory = + new OptimisticWithdrawalRecipientFactory("demo.obol.eth", ENS_REVERSE_REGISTRAR_GOERLI, address(this)); + + owrWithdrawalAddress = + address(owrFactory.createOWRecipient(address(0), principalRecipient, rewardRecipient, 32 ether)); + + factory = new ObolEigenLayerPodControllerFactory( + feeRecipient, feeShare, DELEGATION_MANAGER_GOERLI, POD_MANAGER_GOERLI, DELAY_ROUTER_GOERLI + ); + + owrController = ObolEigenLayerPodController(factory.createPodController(owner, owrWithdrawalAddress)); + + accounts = new address[](2); + accounts[0] = makeAddr("accounts0"); + accounts[1] = makeAddr("accounts1"); + + percentAllocations = new uint32[](2); + percentAllocations[0] = 300_000; + percentAllocations[1] = 700_000; + + splitWithdrawalAddress = ISplitMain(SPLIT_MAIN_GOERLI).createSplit(accounts, percentAllocations, 0, address(0)); + + splitController = ObolEigenLayerPodController(factory.createPodController(owner, splitWithdrawalAddress)); + + vm.prank(DELAY_ROUTER_OWNER_GOERLI); + // set the delay withdrawal duration to zero + IDelayedWithdrawalRouter(DELAY_ROUTER_GOERLI).setWithdrawalDelayBlocks(0); + } + + function testFuzz_WithdrawOWR(uint256 amountToDeposit) external { + vm.assume(amountToDeposit > 0); + + uint256 stakeSize = 32 ether; + + amountToDeposit = boundETH(amountToDeposit); + // transfer unstake beacon eth to eigenPod + (bool success,) = address(owrController.eigenPod()).call{value: amountToDeposit}(""); + require(success, "call failed"); + + vm.startPrank(owner); + { + owrController.callEigenPod(encodeEigenPodCall(address(owrController), amountToDeposit)); + owrController.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + uint256 fee = amountToDeposit * feeShare / PERCENTAGE_SCALE; + + assertEq(address(feeRecipient).balance, fee, "fee recipient balance increased"); + + uint256 owrBalance = amountToDeposit - fee; + assertEq(address(owrWithdrawalAddress).balance, owrBalance, "owr balance increased"); + + // call distribute on owrWithdrawal address + OptimisticWithdrawalRecipient(owrWithdrawalAddress).distributeFunds(); + + // check the princiapl recipient + if (owrBalance >= BALANCE_CLASSIFICATION_THRESHOLD) { + if (owrBalance > stakeSize) { + // prinicipal rexeives 32 eth and reward recieves remainder + assertEq(address(principalRecipient).balance, stakeSize, "invalid principal balance"); + assertEq(address(rewardRecipient).balance, owrBalance - stakeSize, "invalid reward balance"); + } else { + // principal receives everything + assertEq(address(principalRecipient).balance, owrBalance, "invalid principal balance"); + } + } else { + // reward recipient receives everything + assertEq(address(rewardRecipient).balance, owrBalance, "invalid reward balance"); + } + } + + function testFuzz_WithdrawSplit(uint256 amountToDeposit) external { + vm.assume(amountToDeposit > 0); + + amountToDeposit = boundETH(amountToDeposit); + // transfer unstake beacon eth to eigenPod + (bool success,) = address(splitController.eigenPod()).call{value: amountToDeposit}(""); + require(success, "call failed"); + + vm.startPrank(owner); + { + splitController.callEigenPod(encodeEigenPodCall(address(splitController), amountToDeposit)); + splitController.claimDelayedWithdrawals(1); + } + vm.stopPrank(); + + uint256 fee = amountToDeposit * feeShare / PERCENTAGE_SCALE; + assertEq(address(feeRecipient).balance, fee, "fee recipient balance increased"); + + uint256 splitBalance = amountToDeposit - fee; + + assertEq(address(splitWithdrawalAddress).balance, splitBalance, "invalid balance"); + } + + function boundETH(uint256 amount) internal view returns (uint256 result) { + result = bound(amount, 1, type(uint96).max); + } +}