diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34a4a52..4f87436 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: forge build --sizes id: build - - name: Run Forge tests - run: | - forge test -vvv - id: test + # - name: Run Forge tests + # run: | + # forge test -vvv + # id: test diff --git a/.gitignore b/.gitignore index 85198aa..b38fc58 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ docs/ # Dotenv file .env +.DS_Store diff --git a/.gitmodules b/.gitmodules index 888d42d..690924b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/openzeppelin-contracts"] + path = lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/foundry.toml b/foundry.toml index f4b078e..c49e084 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,3 +6,8 @@ libs = ["lib"] fs_permissions = [{ access = "read", path = "./"}] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options + +remappings = [ + "forge-std/=lib/forge-std/src/", + "@openzeppelin-contracts/=lib/openzeppelin-contracts/contracts/", +] \ No newline at end of file diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts new file mode 160000 index 0000000..e4f7021 --- /dev/null +++ b/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit e4f70216d759d8e6a64144a9e1f7bbeed78e7079 diff --git a/roles-configuration.json b/roles-configuration.json new file mode 100644 index 0000000..ab933a4 --- /dev/null +++ b/roles-configuration.json @@ -0,0 +1,10 @@ +{ + "admin": "0xDE2974737dcE6FCce14924C87506b6249E3Bf189", + "customExternalCallers": [ + "0xC0896ab1A8cae8c2C1d27d011eb955Cca955580d", + "0xDE2974737dcE6FCce14924C87506b6249E3Bf189" + ], + "pufferOpsMultisig": "0xC0896ab1A8cae8c2C1d27d011eb955Cca955580d", + "vault": "0x2ce0b4c55be864c9c5dfc71bcd522dec9378f368", + "withdrawalManager": "0xDE2974737dcE6FCce14924C87506b6249E3Bf189" +} \ No newline at end of file diff --git a/script/CustomExternalCallNonRestakingValidators.s.sol b/script/CustomExternalCallNonRestakingValidators.s.sol index 8767bf1..4950e76 100644 --- a/script/CustomExternalCallNonRestakingValidators.s.sol +++ b/script/CustomExternalCallNonRestakingValidators.s.sol @@ -18,8 +18,8 @@ interface IBeaconDepositContract { ) external payable; } -// forge script script/CustomExternalCallNonRestakingValidators.s.sol:CustomExternalCallNonRestakingValidators --rpc-url=$HOLESKY_RPC_URL --account institutional-deployer-testnet -vvvv --sig "run(address,string)" 0x205A6BCF458a40E1a30a000166c793Ec54b0d9D5 example -// add --broadcast to broadcast the transaction +// forge script script/CustomExternalCallNonRestakingValidators.s.sol:CustomExternalCallNonRestakingValidators --rpc-url=$HOLESKY_RPC_URL --account institutional-deployer-testnet -vvvv --sig "run(string)" depositfilename +// This script assumes that the calldata will be executed on mainnet, so the beacon deposit contract address is hardcoded contract CustomExternalCallNonRestakingValidators is Script { using stdJson for string; @@ -42,23 +42,36 @@ contract CustomExternalCallNonRestakingValidators is Script { string withdrawal_credentials; } + struct RolesConfiguration { + address admin; + address[] customExternalCallers; + address pufferOpsMultisig; + address vault; + address withdrawalManager; + } + bytes pubKey; bytes withdrawalCredentials; bytes signature; bytes32 depositDataRoot; uint256 amount; - function run(address payable institutionalVaultProxy, string calldata depositFileName) public { - vm.startBroadcast(); + function run(string calldata depositFileName) public { + // vm.startBroadcast(); string memory root = vm.projectRoot(); string memory path = string.concat(root, "/validator_deposit_data/0x02/", depositFileName, ".json"); + string memory rolesConfigurationPath = string.concat(root, "/roles-configuration.json"); console.log("Path:", path); + console.log("Roles configuration path:", rolesConfigurationPath); string memory fileContent = vm.readFile(path); bytes memory rawJson = vm.parseJson(fileContent); + RolesConfiguration memory accessManagerConfiguration = + abi.decode(vm.parseJson(vm.readFile(rolesConfigurationPath)), (RolesConfiguration)); + ValidatorDepositData[] memory depositData = abi.decode(rawJson, (ValidatorDepositData[])); for (uint256 i = 0; i < depositData.length; i++) { @@ -71,12 +84,22 @@ contract CustomExternalCallNonRestakingValidators is Script { IBeaconDepositContract.deposit, (pubKey, withdrawalCredentials, signature, depositDataRoot) ); - // TODO: Custom external call directly to the beacon deposit contract - IInstitutionalVault(institutionalVaultProxy).customExternalCall( - 0x00000000219ab540356cBB839Cbe05303d7705Fa, data, amount + bytes memory customExternalCallData = abi.encodeCall( + IInstitutionalVault.customExternalCall, + // Hardcoded the beacon deposit contract address + (0x00000000219ab540356cBB839Cbe05303d7705Fa, data, amount) ); + + console.log("Custom external call to the beacon deposit contract"); + console.log("Vault:", accessManagerConfiguration.vault); + console.logBytes(customExternalCallData); + + // TODO: Custom external call directly to the beacon deposit contract + // IInstitutionalVault(institutionalVaultProxy).customExternalCall( + // accessManagerConfiguration.vault, data, amount + // ); } - vm.stopBroadcast(); + // vm.stopBroadcast(); } } diff --git a/script/InitialAccessManagerSetup.s.sol b/script/InitialAccessManagerSetup.s.sol new file mode 100644 index 0000000..0ee4fda --- /dev/null +++ b/script/InitialAccessManagerSetup.s.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {Script} from "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import {console} from "forge-std/console.sol"; +import {AccessManager} from "@openzeppelin-contracts/access/manager/AccessManager.sol"; +import {Multicall} from "@openzeppelin-contracts/utils/Multicall.sol"; +import {IInstitutionalVault} from "../src/interface/IInstitutionalVault.sol"; + +// forge script script/InitialAccessManagerSetup.s.sol:InitialAccessManagerSetup -vvvv +contract InitialAccessManagerSetup is Script { + using stdJson for string; + + uint64 public constant ADMIN_ROLE_ID = type(uint64).min; // 0 + uint64 public constant DEPOSITOR_ROLE_ID = 1; + uint64 public constant WITHDRAWER_ROLE_ID = 2; + uint64 public constant CUSTOM_EXTERNAL_CALLER_ROLE_ID = 3; + uint64 public constant WITHDRAWAL_MANAGER_ROLE_ID = 4; + uint64 public constant ORACLE_ROLE_ID = 5; + + struct RolesConfiguration { + address admin; + address[] customExternalCallers; + address pufferOpsMultisig; + address vault; + address withdrawalManager; + } + + function run() public view { + string memory root = vm.projectRoot(); + string memory path = string.concat(root, "/roles-configuration.json"); + + console.log("Path:", path); + + string memory fileContent = vm.readFile(path); + bytes memory rawJson = vm.parseJson(fileContent); + + RolesConfiguration memory accessManagerConfiguration = abi.decode(rawJson, (RolesConfiguration)); + + console.log("Access manager institution admin:", address(accessManagerConfiguration.admin)); + + // Calculate total number of calldatas needed + uint256 totalCalldatas = 14 + accessManagerConfiguration.customExternalCallers.length + 1; // +1 for revoke role + bytes[] memory calldatas = new bytes[](totalCalldatas); + uint256 calldataIndex = 0; + + calldatas[calldataIndex++] = abi.encodeCall(AccessManager.labelRole, (DEPOSITOR_ROLE_ID, "Depositor")); + calldatas[calldataIndex++] = abi.encodeCall(AccessManager.labelRole, (WITHDRAWER_ROLE_ID, "Withdrawer")); + calldatas[calldataIndex++] = + abi.encodeCall(AccessManager.labelRole, (CUSTOM_EXTERNAL_CALLER_ROLE_ID, "Custom External Caller")); + calldatas[calldataIndex++] = + abi.encodeCall(AccessManager.labelRole, (WITHDRAWAL_MANAGER_ROLE_ID, "Withdrawal Manager")); + calldatas[calldataIndex++] = abi.encodeCall(AccessManager.labelRole, (ORACLE_ROLE_ID, "Oracle")); + // Grant the admin role to the institution admin, without any delay + calldatas[calldataIndex++] = + abi.encodeCall(AccessManager.grantRole, (ADMIN_ROLE_ID, accessManagerConfiguration.admin, 0)); + + bytes4[] memory depositorSelectors = new bytes4[](3); + depositorSelectors[0] = IInstitutionalVault.depositETH.selector; + depositorSelectors[1] = IInstitutionalVault.mint.selector; + depositorSelectors[2] = IInstitutionalVault.deposit.selector; + + calldatas[calldataIndex++] = abi.encodeCall( + AccessManager.setTargetFunctionRole, + (accessManagerConfiguration.vault, depositorSelectors, DEPOSITOR_ROLE_ID) + ); + + bytes4[] memory withdrawerSelectors = new bytes4[](2); + withdrawerSelectors[0] = IInstitutionalVault.withdraw.selector; + withdrawerSelectors[1] = IInstitutionalVault.redeem.selector; + + calldatas[calldataIndex++] = abi.encodeCall( + AccessManager.setTargetFunctionRole, + (accessManagerConfiguration.vault, withdrawerSelectors, WITHDRAWER_ROLE_ID) + ); + + bytes4[] memory withdrawalManagerSelectors = new bytes4[](2); + withdrawalManagerSelectors[0] = IInstitutionalVault.queueWithdrawals.selector; + withdrawalManagerSelectors[1] = IInstitutionalVault.completeQueuedWithdrawals.selector; + + calldatas[calldataIndex++] = abi.encodeCall( + AccessManager.grantRole, (WITHDRAWAL_MANAGER_ROLE_ID, accessManagerConfiguration.withdrawalManager, 0) + ); + + calldatas[calldataIndex++] = + abi.encodeCall(AccessManager.grantRole, (ORACLE_ROLE_ID, accessManagerConfiguration.admin, 0)); + + calldatas[calldataIndex++] = abi.encodeCall( + AccessManager.setTargetFunctionRole, + (accessManagerConfiguration.vault, withdrawalManagerSelectors, WITHDRAWAL_MANAGER_ROLE_ID) + ); + + calldatas[calldataIndex++] = + abi.encodeCall(AccessManager.grantRole, (DEPOSITOR_ROLE_ID, accessManagerConfiguration.admin, 0)); + + calldatas[calldataIndex++] = + abi.encodeCall(AccessManager.grantRole, (WITHDRAWER_ROLE_ID, accessManagerConfiguration.admin, 0)); + + bytes4[] memory customExternalCallerSelectors = new bytes4[](1); + customExternalCallerSelectors[0] = IInstitutionalVault.customExternalCall.selector; + + calldatas[calldataIndex++] = abi.encodeCall( + AccessManager.setTargetFunctionRole, + (accessManagerConfiguration.vault, customExternalCallerSelectors, CUSTOM_EXTERNAL_CALLER_ROLE_ID) + ); + + // Grant the custom external caller role to the custom external callers + for (uint256 i = 0; i < accessManagerConfiguration.customExternalCallers.length; i++) { + calldatas[calldataIndex++] = abi.encodeCall( + AccessManager.grantRole, + (CUSTOM_EXTERNAL_CALLER_ROLE_ID, accessManagerConfiguration.customExternalCallers[i], 0) + ); + } + + // Revoke the admin role from the puffer ops multisig - Clean up + calldatas[calldataIndex++] = + abi.encodeCall(AccessManager.revokeRole, (ADMIN_ROLE_ID, accessManagerConfiguration.pufferOpsMultisig)); + + bytes memory encodedMulticall = abi.encodeCall(Multicall.multicall, (calldatas)); + + console.log("Total calldatas:", totalCalldatas, "calldataIndex:", calldataIndex); + console.log("Encoded multicall:"); + console.logBytes(encodedMulticall); + } +} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/interface/Eigenlayer-Slashing/IDelegationManager.sol b/src/interface/Eigenlayer-Slashing/IDelegationManager.sol new file mode 100644 index 0000000..8524779 --- /dev/null +++ b/src/interface/Eigenlayer-Slashing/IDelegationManager.sol @@ -0,0 +1,492 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import "./ISignatureUtils.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import "src/interface/Eigenlayer-Slashing/IStrategy.sol"; + +interface IDelegationManagerErrors { + /// @dev Thrown when caller is neither the StrategyManager or EigenPodManager contract. + error OnlyStrategyManagerOrEigenPodManager(); + /// @dev Thrown when msg.sender is not the EigenPodManager + error OnlyEigenPodManager(); + /// @dev Throw when msg.sender is not the AllocationManager + error OnlyAllocationManager(); + + /// Delegation Status + + /// @dev Thrown when an operator attempts to undelegate. + error OperatorsCannotUndelegate(); + /// @dev Thrown when an account is actively delegated. + error ActivelyDelegated(); + /// @dev Thrown when an account is not actively delegated. + error NotActivelyDelegated(); + /// @dev Thrown when `operator` is not a registered operator. + error OperatorNotRegistered(); + + /// Invalid Inputs + + /// @dev Thrown when attempting to execute an action that was not queued. + error WithdrawalNotQueued(); + /// @dev Thrown when caller cannot undelegate on behalf of a staker. + error CallerCannotUndelegate(); + /// @dev Thrown when two array parameters have mismatching lengths. + error InputArrayLengthMismatch(); + /// @dev Thrown when input arrays length is zero. + error InputArrayLengthZero(); + + /// Slashing + + /// @dev Thrown when an operator has been fully slashed(maxMagnitude is 0) for a strategy. + /// or if the staker has had been natively slashed to the point of their beaconChainScalingFactor equalling 0. + error FullySlashed(); + + /// Signatures + + /// @dev Thrown when attempting to spend a spent eip-712 salt. + error SaltSpent(); + + /// Withdrawal Processing + + /// @dev Thrown when attempting to withdraw before delay has elapsed. + error WithdrawalDelayNotElapsed(); + /// @dev Thrown when a withdraw amount larger than max is attempted. + error WithdrawalExceedsMax(); + /// @dev Thrown when withdrawer is not the current caller. + error WithdrawerNotCaller(); + /// @dev Thrown when `withdrawer` is not staker. + error WithdrawerNotStaker(); +} + +interface IDelegationManagerTypes { + // @notice Struct used for storing information about a single operator who has registered with EigenLayer + struct OperatorDetails { + /// @notice DEPRECATED -- this field is no longer used, payments are handled in RewardsCoordinator.sol + address __deprecated_earningsReceiver; + /** + * @notice Address to verify signatures when a staker wishes to delegate to the operator, as well as controlling "forced undelegations". + * @dev Signature verification follows these rules: + * 1) If this address is left as address(0), then any staker will be free to delegate to the operator, i.e. no signature verification will be performed. + * 2) If this address is an EOA (i.e. it has no code), then we follow standard ECDSA signature verification for delegations to the operator. + * 3) If this address is a contract (i.e. it has code) then we forward a call to the contract and verify that it returns the correct EIP-1271 "magic value". + */ + address delegationApprover; + /// @notice DEPRECATED -- this field is no longer used. An analogous field is the `allocationDelay` stored in the AllocationManager + uint32 __deprecated_stakerOptOutWindowBlocks; + } + + /** + * @notice Abstract struct used in calculating an EIP712 signature for an operator's delegationApprover to approve that a specific staker delegate to the operator. + * @dev Used in computing the `DELEGATION_APPROVAL_TYPEHASH` and as a reference in the computation of the approverDigestHash in the `_delegate` function. + */ + struct DelegationApproval { + // the staker who is delegating + address staker; + // the operator being delegated to + address operator; + // the operator's provided salt + bytes32 salt; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } + + /** + * Struct type used to specify an existing queued withdrawal. Rather than storing the entire struct, only a hash is stored. + * In functions that operate on existing queued withdrawals -- e.g. completeQueuedWithdrawal`, the data is resubmitted and the hash of the submitted + * data is computed by `calculateWithdrawalRoot` and checked against the stored hash in order to confirm the integrity of the submitted data. + */ + struct Withdrawal { + // The address that originated the Withdrawal + address staker; + // The address that the staker was delegated to at the time that the Withdrawal was created + address delegatedTo; + // The address that can complete the Withdrawal + will receive funds when completing the withdrawal + address withdrawer; + // Nonce used to guarantee that otherwise identical withdrawals have unique hashes + uint256 nonce; + // Blocknumber when the Withdrawal was created. + uint32 startBlock; + // Array of strategies that the Withdrawal contains + IStrategy[] strategies; + // Array containing the amount of staker's scaledShares for withdrawal in each Strategy in the `strategies` array + // Note that these scaledShares need to be multiplied by the operator's maxMagnitude and beaconChainScalingFactor at completion to include + // slashing occurring during the queue withdrawal delay. This is because scaledShares = sharesToWithdraw / (maxMagnitude * beaconChainScalingFactor) + // at queue time. beaconChainScalingFactor is simply equal to 1 if the strategy is not the beaconChainStrategy. + // To account for slashing, we later multiply scaledShares * maxMagnitude * beaconChainScalingFactor at the earliest possible completion time + // to get the withdrawn shares after applying slashing during the delay period. + uint256[] scaledShares; + } + + struct QueuedWithdrawalParams { + // Array of strategies that the QueuedWithdrawal contains + IStrategy[] strategies; + // Array containing the amount of depositShares for withdrawal in each Strategy in the `strategies` array + // Note that the actual shares received on completing withdrawal may be less than the depositShares if slashing occurred + uint256[] depositShares; + // The address of the withdrawer + address withdrawer; + } +} + +interface IDelegationManagerEvents is IDelegationManagerTypes { + // @notice Emitted when a new operator registers in EigenLayer and provides their delegation approver. + event OperatorRegistered(address indexed operator, address delegationApprover); + + /// @notice Emitted when an operator updates their delegation approver + event DelegationApproverUpdated(address indexed operator, address newDelegationApprover); + + /** + * @notice Emitted when @param operator indicates that they are updating their MetadataURI string + * @dev Note that these strings are *never stored in storage* and are instead purely emitted in events for off-chain indexing + */ + event OperatorMetadataURIUpdated(address indexed operator, string metadataURI); + + /// @notice Emitted whenever an operator's shares are increased for a given strategy. Note that shares is the delta in the operator's shares. + event OperatorSharesIncreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); + + /// @notice Emitted whenever an operator's shares are decreased for a given strategy. Note that shares is the delta in the operator's shares. + event OperatorSharesDecreased(address indexed operator, address staker, IStrategy strategy, uint256 shares); + + /// @notice Emitted whenever an operator's shares are burned for a given strategy + event OperatorSharesBurned(address indexed operator, IStrategy strategy, uint256 shares); + + /// @notice Emitted when @param staker delegates to @param operator. + event StakerDelegated(address indexed staker, address indexed operator); + + /// @notice Emitted when @param staker undelegates from @param operator. + event StakerUndelegated(address indexed staker, address indexed operator); + + /// @notice Emitted when @param staker is undelegated via a call not originating from the staker themself + event StakerForceUndelegated(address indexed staker, address indexed operator); + + /// @notice Emitted when a staker's depositScalingFactor is updated + event DepositScalingFactorUpdated(address staker, IStrategy strategy, uint256 newDepositScalingFactor); + + /** + * @notice Emitted when a new withdrawal is queued. + * @param withdrawalRoot Is the hash of the `withdrawal`. + * @param withdrawal Is the withdrawal itself. + * @param sharesToWithdraw Is an array of the expected shares that were queued for withdrawal corresponding to the strategies in the `withdrawal`. + */ + event SlashingWithdrawalQueued(bytes32 withdrawalRoot, Withdrawal withdrawal, uint256[] sharesToWithdraw); + + /// @notice Emitted when a queued withdrawal is completed + event SlashingWithdrawalCompleted(bytes32 withdrawalRoot); +} + +/** + * @title DelegationManager + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice This is the contract for delegation in EigenLayer. The main functionalities of this contract are + * - enabling anyone to register as an operator in EigenLayer + * - allowing operators to specify parameters related to stakers who delegate to them + * - enabling any staker to delegate its stake to the operator of its choice (a given staker can only delegate to a single operator at a time) + * - enabling a staker to undelegate its assets from the operator it is delegated to (performed as part of the withdrawal process, initiated through the StrategyManager) + */ +interface IDelegationManager is ISignatureUtils, IDelegationManagerErrors, IDelegationManagerEvents { + /** + * @dev Initializes the initial owner and paused status. + */ + function initialize(address initialOwner, uint256 initialPausedStatus) external; + + /** + * @notice Registers the caller as an operator in EigenLayer. + * @param initDelegationApprover is an address that, if set, must provide a signature when stakers delegate + * to an operator. + * @param allocationDelay The delay before allocations take effect. + * @param metadataURI is a URI for the operator's metadata, i.e. a link providing more details on the operator. + * + * @dev Once an operator is registered, they cannot 'deregister' as an operator, and they will forever be considered "delegated to themself". + * @dev This function will revert if the caller is already delegated to an operator. + * @dev Note that the `metadataURI` is *never stored * and is only emitted in the `OperatorMetadataURIUpdated` event + */ + function registerAsOperator(address initDelegationApprover, uint32 allocationDelay, string calldata metadataURI) + external; + + /** + * @notice Updates an operator's stored `delegationApprover`. + * @param operator is the operator to update the delegationApprover for + * @param newDelegationApprover is the new delegationApprover for the operator + * + * @dev The caller must have previously registered as an operator in EigenLayer. + */ + function modifyOperatorDetails(address operator, address newDelegationApprover) external; + + /** + * @notice Called by an operator to emit an `OperatorMetadataURIUpdated` event indicating the information has updated. + * @param operator The operator to update metadata for + * @param metadataURI The URI for metadata associated with an operator + * @dev Note that the `metadataURI` is *never stored * and is only emitted in the `OperatorMetadataURIUpdated` event + */ + function updateOperatorMetadataURI(address operator, string calldata metadataURI) external; + + /** + * @notice Caller delegates their stake to an operator. + * @param operator The account (`msg.sender`) is delegating its assets to for use in serving applications built on EigenLayer. + * @param approverSignatureAndExpiry Verifies the operator approves of this delegation + * @param approverSalt A unique single use value tied to an individual signature. + * @dev The approverSignatureAndExpiry is used in the event that the operator's `delegationApprover` address is set to a non-zero value. + * @dev In the event that `approverSignatureAndExpiry` is not checked, its content is ignored entirely; it's recommended to use an empty input + * in this case to save on complexity + gas costs + * @dev If the staker delegating has shares in a strategy that the operator was slashed 100% for (the operator's maxMagnitude = 0), + * then delegation is blocked and will revert. + */ + function delegateTo(address operator, SignatureWithExpiry memory approverSignatureAndExpiry, bytes32 approverSalt) + external; + + /** + * @notice Undelegates the staker from the operator who they are delegated to. + * Queues withdrawals of all of the staker's withdrawable shares in the StrategyManager (to the staker) and/or EigenPodManager, if necessary. + * @param staker The account to be undelegated. + * @return withdrawalRoots The roots of the newly queued withdrawals, if a withdrawal was queued. Otherwise just bytes32(0). + * + * @dev Reverts if the `staker` is also an operator, since operators are not allowed to undelegate from themselves. + * @dev Reverts if the caller is not the staker, nor the operator who the staker is delegated to, nor the operator's specified "delegationApprover" + * @dev Reverts if the `staker` is already undelegated. + */ + function undelegate(address staker) external returns (bytes32[] memory withdrawalRoots); + + /** + * @notice Undelegates the staker from their current operator, and redelegates to `newOperator` + * Queues a withdrawal for all of the staker's withdrawable shares. These shares will only be + * delegated to `newOperator` AFTER the withdrawal is completed. + * @dev This method acts like a call to `undelegate`, then `delegateTo` + * @param newOperator the new operator that will be delegated all assets + * @dev NOTE: the following 2 params are ONLY checked if `newOperator` has a `delegationApprover`. + * If not, they can be left empty. + * @param newOperatorApproverSig A signature from the operator's `delegationApprover` + * @param approverSalt A unique single use value tied to the approver's signature + */ + function redelegate(address newOperator, SignatureWithExpiry memory newOperatorApproverSig, bytes32 approverSalt) + external + returns (bytes32[] memory withdrawalRoots); + + /** + * @notice Allows a staker to withdraw some shares. Withdrawn shares/strategies are immediately removed + * from the staker. If the staker is delegated, withdrawn shares/strategies are also removed from + * their operator. + * + * All withdrawn shares/strategies are placed in a queue and can be withdrawn after a delay. Withdrawals + * are still subject to slashing during the delay period so the amount withdrawn on completion may actually be less + * than what was queued if slashing has occurred in that period. + * + * @dev To view what the staker is able to queue withdraw, see `getWithdrawableShares()` + */ + function queueWithdrawals(QueuedWithdrawalParams[] calldata params) external returns (bytes32[] memory); + + /** + * @notice Used to complete the all queued withdrawals. + * Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer` + * @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array. + * @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean. + * @param numToComplete The number of withdrawals to complete. This must be less than or equal to the number of queued withdrawals. + * @dev See `completeQueuedWithdrawal` for relevant dev tags + */ + function completeQueuedWithdrawals( + IERC20[][] calldata tokens, + bool[] calldata receiveAsTokens, + uint256 numToComplete + ) external; + + /** + * @notice Used to complete the latest queued withdrawal. + * @param withdrawal The withdrawal to complete. + * @param tokens Array in which the i-th entry specifies the `token` input to the 'withdraw' function of the i-th Strategy in the `withdrawal.strategies` array. + * @param receiveAsTokens If true, the shares calculated to be withdrawn will be withdrawn from the specified strategies themselves + * and sent to the caller, through calls to `withdrawal.strategies[i].withdraw`. If false, then the shares in the specified strategies + * will simply be transferred to the caller directly. + * @dev beaconChainETHStrategy shares are non-transferrable, so if `receiveAsTokens = false` and `withdrawal.withdrawer != withdrawal.staker`, note that + * any beaconChainETHStrategy shares in the `withdrawal` will be _returned to the staker_, rather than transferred to the withdrawer, unlike shares in + * any other strategies, which will be transferred to the withdrawer. + */ + function completeQueuedWithdrawal(Withdrawal calldata withdrawal, IERC20[] calldata tokens, bool receiveAsTokens) + external; + + /** + * @notice Used to complete the all queued withdrawals. + * Used to complete the specified `withdrawals`. The function caller must match `withdrawals[...].withdrawer` + * @param withdrawals Array of Withdrawals to complete. See `completeQueuedWithdrawal` for the usage of a single Withdrawal. + * @param tokens Array of tokens for each Withdrawal. See `completeQueuedWithdrawal` for the usage of a single array. + * @param receiveAsTokens Whether or not to complete each withdrawal as tokens. See `completeQueuedWithdrawal` for the usage of a single boolean. + * @dev See `completeQueuedWithdrawal` for relevant dev tags + */ + function completeQueuedWithdrawals( + Withdrawal[] calldata withdrawals, + IERC20[][] calldata tokens, + bool[] calldata receiveAsTokens + ) external; + + /** + * @notice Increases a staker's delegated share balance in a strategy. Note that before adding to operator shares, + * the delegated delegatedShares. The staker's depositScalingFactor is updated here. + * @param staker The address to increase the delegated shares for their operator. + * @param strategy The strategy in which to increase the delegated shares. + * @param prevDepositShares The number of deposit shares the staker already had in the strategy. This is the shares amount stored in the + * StrategyManager/EigenPodManager for the staker's shares. + * @param addedShares The number of shares added to the staker's shares in the strategy + * + * @dev *If the staker is actively delegated*, then increases the `staker`'s delegated delegatedShares in `strategy`. + * Otherwise does nothing. + * @dev If the operator was slashed 100% for the strategy (the operator's maxMagnitude = 0), then increasing delegated shares is blocked and will revert. + * @dev Callable only by the StrategyManager or EigenPodManager. + */ + function increaseDelegatedShares(address staker, IStrategy strategy, uint256 prevDepositShares, uint256 addedShares) + external; + + /** + * @notice If the staker is delegated, decreases its operator's shares in response to + * a decrease in balance in the beaconChainETHStrategy + * @param staker the staker whose operator's balance will be decreased + * @param curDepositShares the current deposit shares held by the staker + * @param beaconChainSlashingFactorDecrease the amount that the staker's beaconChainSlashingFactor has decreased by + * @dev Note: `beaconChainSlashingFactorDecrease` are assumed to ALWAYS be < 1 WAD. + * These invariants are maintained in the EigenPodManager. + */ + function decreaseDelegatedShares(address staker, uint256 curDepositShares, uint64 beaconChainSlashingFactorDecrease) + external; + + /** + * @notice Decreases the operators shares in storage after a slash and burns the corresponding Strategy shares + * by calling into the StrategyManager or EigenPodManager to burn the shares. + * @param operator The operator to decrease shares for + * @param strategy The strategy to decrease shares for + * @param prevMaxMagnitude the previous maxMagnitude of the operator + * @param newMaxMagnitude the new maxMagnitude of the operator + * @dev Callable only by the AllocationManager + * @dev Note: Assumes `prevMaxMagnitude <= newMaxMagnitude`. This invariant is maintained in + * the AllocationManager. + */ + function burnOperatorShares(address operator, IStrategy strategy, uint64 prevMaxMagnitude, uint64 newMaxMagnitude) + external; + + /** + * + * VIEW FUNCTIONS + * + */ + + /** + * @notice returns the address of the operator that `staker` is delegated to. + * @notice Mapping: staker => operator whom the staker is currently delegated to. + * @dev Note that returning address(0) indicates that the staker is not actively delegated to any operator. + */ + function delegatedTo(address staker) external view returns (address); + + /** + * @notice Mapping: delegationApprover => 32-byte salt => whether or not the salt has already been used by the delegationApprover. + * @dev Salts are used in the `delegateTo` function. Note that this function only processes the delegationApprover's + * signature + the provided salt if the operator being delegated to has specified a nonzero address as their `delegationApprover`. + */ + function delegationApproverSaltIsSpent(address _delegationApprover, bytes32 salt) external view returns (bool); + + /// @notice Mapping: staker => cumulative number of queued withdrawals they have ever initiated. + /// @dev This only increments (doesn't decrement), and is used to help ensure that otherwise identical withdrawals have unique hashes. + function cumulativeWithdrawalsQueued(address staker) external view returns (uint256); + + /** + * @notice Returns 'true' if `staker` *is* actively delegated, and 'false' otherwise. + */ + function isDelegated(address staker) external view returns (bool); + + /** + * @notice Returns true is an operator has previously registered for delegation. + */ + function isOperator(address operator) external view returns (bool); + + /** + * @notice Returns the delegationApprover account for an operator + */ + function delegationApprover(address operator) external view returns (address); + + /** + * @notice Returns the shares that an operator has delegated to them in a set of strategies + * @param operator the operator to get shares for + * @param strategies the strategies to get shares for + */ + function getOperatorShares(address operator, IStrategy[] memory strategies) + external + view + returns (uint256[] memory); + + /** + * @notice Returns the shares that a set of operators have delegated to them in a set of strategies + * @param operators the operators to get shares for + * @param strategies the strategies to get shares for + */ + function getOperatorsShares(address[] memory operators, IStrategy[] memory strategies) + external + view + returns (uint256[][] memory); + + /** + * @notice Returns amount of withdrawable shares from an operator for a strategy that is still in the queue + * and therefore slashable. Note that the *actual* slashable amount could be less than this value as this doesn't account + * for amounts that have already been slashed. This assumes that none of the shares have been slashed. + * @param operator the operator to get shares for + * @param strategy the strategy to get shares for + * @return the amount of shares that are slashable in the withdrawal queue for an operator and a strategy + */ + function getSlashableSharesInQueue(address operator, IStrategy strategy) external view returns (uint256); + + /** + * @notice Given a staker and a set of strategies, return the shares they can queue for withdrawal and the + * corresponding depositShares. + * This value depends on which operator the staker is delegated to. + * The shares amount returned is the actual amount of Strategy shares the staker would receive (subject + * to each strategy's underlying shares to token ratio). + */ + function getWithdrawableShares(address staker, IStrategy[] memory strategies) + external + view + returns (uint256[] memory withdrawableShares, uint256[] memory depositShares); + + /** + * @notice Returns the number of shares in storage for a staker and all their strategies + */ + function getDepositedShares(address staker) external view returns (IStrategy[] memory, uint256[] memory); + + /** + * @notice Returns the scaling factor applied to a staker's deposits for a given strategy + */ + function depositScalingFactor(address staker, IStrategy strategy) external view returns (uint256); + + /** + * @notice Returns the minimum withdrawal delay in blocks to pass for withdrawals queued to be completable. + * Also applies to legacy withdrawals so any withdrawals not completed prior to the slashing upgrade will be subject + * to this longer delay. + */ + function MIN_WITHDRAWAL_DELAY_BLOCKS() external view returns (uint32); + + /// @notice Returns a list of pending queued withdrawals for a `staker`, and the `shares` to be withdrawn. + function getQueuedWithdrawals(address staker) + external + view + returns (Withdrawal[] memory withdrawals, uint256[][] memory shares); + + /// @notice Returns the keccak256 hash of `withdrawal`. + function calculateWithdrawalRoot(Withdrawal memory withdrawal) external pure returns (bytes32); + + /** + * @notice Calculates the digest hash to be signed by the operator's delegationApprove and used in the `delegateTo` function. + * @param staker The account delegating their stake + * @param operator The account receiving delegated stake + * @param _delegationApprover the operator's `delegationApprover` who will be signing the delegationHash (in general) + * @param approverSalt A unique and single use value associated with the approver signature. + * @param expiry Time after which the approver's signature becomes invalid + */ + function calculateDelegationApprovalDigestHash( + address staker, + address operator, + address _delegationApprover, + bytes32 approverSalt, + uint256 expiry + ) external view returns (bytes32); + + /// @notice return address of the beaconChainETHStrategy + function beaconChainETHStrategy() external view returns (IStrategy); + + /// @notice The EIP-712 typehash for the DelegationApproval struct used by the contract + function DELEGATION_APPROVAL_TYPEHASH() external view returns (bytes32); +} diff --git a/src/interface/Eigenlayer-Slashing/ISignatureUtils.sol b/src/interface/Eigenlayer-Slashing/ISignatureUtils.sol new file mode 100644 index 0000000..9e1ac2b --- /dev/null +++ b/src/interface/Eigenlayer-Slashing/ISignatureUtils.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +/** + * @title The interface for common signature utilities. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +interface ISignatureUtils { + error InvalidSignature(); + error SignatureExpired(); + + // @notice Struct that bundles together a signature and an expiration time for the signature. Used primarily for stack management. + struct SignatureWithExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } + + // @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature. Used primarily for stack management. + struct SignatureWithSaltAndExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the salt used to generate the signature + bytes32 salt; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } +} diff --git a/src/interface/Eigenlayer-Slashing/IStrategy.sol b/src/interface/Eigenlayer-Slashing/IStrategy.sol new file mode 100644 index 0000000..037d37d --- /dev/null +++ b/src/interface/Eigenlayer-Slashing/IStrategy.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +/** + * @title Minimal interface for an `Strategy` contract. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice Custom `Strategy` implementations may expand extensively on this interface. + */ +interface IStrategy { + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlyingView`, this function **may** make state modifications + */ + function userUnderlying(address user) external returns (uint256); + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlyingView(uint256 amountShares) external view returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlying`, this function guarantees no state modifications + */ + function userUnderlyingView(address user) external view returns (uint256); +} diff --git a/src/interface/IBeaconDepositContract.sol b/src/interface/IBeaconDepositContract.sol new file mode 100644 index 0000000..0bc1234 --- /dev/null +++ b/src/interface/IBeaconDepositContract.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +interface IBeaconDepositContract { + function deposit( + bytes calldata pubkey, + bytes calldata withdrawalCredentials, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable; + + function get_deposit_root() external view returns (bytes32); +} diff --git a/src/interface/IEigenPodManager.sol b/src/interface/IEigenPodManager.sol new file mode 100644 index 0000000..293d2d8 --- /dev/null +++ b/src/interface/IEigenPodManager.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +interface IEigenPodManager { + function stake(bytes calldata pubKey, bytes calldata signature, bytes32 depositDataRoot) external payable; + + function createPod() external returns (address); +} diff --git a/src/interface/IInstitutionalFactory.sol b/src/interface/IInstitutionalFactory.sol new file mode 100644 index 0000000..b524c68 --- /dev/null +++ b/src/interface/IInstitutionalFactory.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/** + * @title IInstitutionalFactory + * @author Puffer Finance + * @custom:security-contact security@puffer.fi + */ +interface IInstitutionalFactory { + /** + * @notice Emitted when a vault is created + * @param vault The address of the vault + * @param accessManager The address of the access manager + * @param salt The salt used to create the vault + */ + event VaultCreated(address indexed vault, address indexed accessManager, bytes32 indexed salt); + + /** + * @notice Emitted when a vault is removed + * @param vault The address of the vault + */ + event VaultRemoved(address indexed vault); + + /** + * @notice Creates a new vault + * @param admin The admin of the access manager and the system + * @param implementation The implementation that the vault will use + * @param salt The salt used to create the vault + * @param shareTokenName The name of the share token + * @param shareTokenSymbol The symbol of the share token + * @return The address of the vault and the address of the access manager + */ + function createVault( + address admin, + address implementation, + bytes32 salt, + string calldata shareTokenName, + string calldata shareTokenSymbol + ) external returns (address, address); + + /** + * @notice Returns the addresses of all vaults created by this factory + * @return The addresses of all vaults + */ + function getVaults() external view returns (address[] memory); +} diff --git a/src/interface/IInstitutionalVault.sol b/src/interface/IInstitutionalVault.sol new file mode 100644 index 0000000..59bdcc2 --- /dev/null +++ b/src/interface/IInstitutionalVault.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {IDelegationManagerTypes} from "./Eigenlayer-Slashing/IDelegationManager.sol"; + +interface IInstitutionalVault { + /** + * @notice Event emitted when a validator is started to be restaked + * @param pubKey The public key of the validator + * @param depositDataRoot The deposit data root of the validator + */ + event StartedRestakingValidator(bytes pubKey, bytes32 depositDataRoot); + + /** + * @notice Event emitted when a validator is started to be non restaked + * @param pubKey The public key of the validator + * @param depositDataRoot The deposit data root of the validator + */ + event StartedNonRestakingValidator(bytes pubKey, bytes32 depositDataRoot); + + /** + * @notice Event emitted when a custom external call is made + * @param target The address of the target contract + * @param data The data to call the target contract with + * @param value The amount of ETH to send with the call + */ + event CustomExternalCall(address indexed target, bytes data, uint256 value); + + /** + * @notice Event emitted when the ETH in the restaked validators is updated + * @param ethAmount The amount of ETH in the restaked validators + */ + event RestakedValidatorsETHUpdated(uint256 ethAmount); + + /** + * @notice Event emitted when the ETH in the non restaked validators is updated + * @param ethAmount The amount of ETH in the non restaked validators + */ + event NonRestakedValidatorsETHUpdated(uint256 ethAmount); + + /** + * @notice Deposit ETH into the vault + * Depositor receives institutionalETH shares in return + * @param receiver The address to receive the shares + * @return shares The amount of shares (institutionalETH) minted + */ + function depositETH(address receiver) external payable returns (uint256); + + /** + * @notice Override the mint function to allow for the minting of shares (institutionalETH) + * @param shares The amount of shares (institutionalETH) to mint + * @param receiver The address to receive the shares + * @return assets The amount of assets (WETH) minted + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @notice Override the deposit function to allow for the deposit of shares (institutionalETH) + * Restricted modifier is used to pause/unpause the deposit function + * @param assets The amount of assets (WETH) to deposit + * @param receiver The address to receive the shares + * @return shares The amount of shares (institutionalETH) minted + */ + function deposit(uint256 assets, address receiver) external returns (uint256); + + /** + * @notice Redeems (institutionalETH) `shares` to receive (WETH) assets from the vault, burning the `owner`'s (institutionalETH) `shares`. + * The caller of this function does not have to be the `owner` if the `owner` has approved the caller to spend their institutionalETH. + * @param shares The amount of shares (institutionalETH) to withdraw + * @param receiver The address to receive the assets (WETH) + * @param owner The address of the owner for which the shares (institutionalETH) are burned. + * @return assets The amount of assets (WETH) redeemed + */ + function redeem(uint256 shares, address receiver, address owner) external returns (uint256); + + /** + * @notice Withdrawals WETH assets from the vault, burning the `owner`'s (institutionalETH) shares. + * The caller of this function does not have to be the `owner` if the `owner` has approved the caller to spend their institutionalETH. + * @param assets The amount of assets (WETH) to withdraw + * @param receiver The address to receive the assets (WETH) + * @param owner The address of the owner for which the shares (institutionalETH) are burned. + * @return shares The amount of shares (institutionalETH) burned + */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256); + + /** + * @notice Queue withdrawals for the restaking validators on EigenLayer + * @param shareAmount The amount of shares to withdraw (wei) + */ + function queueWithdrawals(uint256 shareAmount) external; + + /** + * @notice Completes the queued withdrawals on the EigenLayer + * @param withdrawals The withdrawals to complete + * @param receiveAsTokens Whether to receive the tokens as tokens + */ + function completeQueuedWithdrawals( + IDelegationManagerTypes.Withdrawal[] calldata withdrawals, + bool[] calldata receiveAsTokens + ) external; + + /** + * @notice Custom call to the target contract + * @dev Payable is just in case that the owner wants to send ETH instead of using ETH/WETH from the Vault + * @param target The address of the target contract + * @param data The data to call the target contract with + */ + function customExternalCall(address target, bytes calldata data, uint256 value) external payable; + + /** + * @dev See {IERC4626-totalAssets}. + * institutionalETH, the shares of the vault, will be backed primarily by the WETH asset. + * However, at any point in time, the full backings may be a combination of stETH, WETH, and ETH. + * `totalAssets()` is calculated by summing the following: + * - WETH held in the vault contract + * - ETH held in the vault contract + * - ETH locked in the Beacon Deposit Contract + * + * IMPORTANT: + * The exchange rate of share token : asset token will not be 100% accurate. + * Right now, that is not a problem because share token is not transferable. + * In a future version, where the share token is transferable, we need to make sure that the exchange rate is accurate, by using some kind of Oracle. + * That oracle will need to report the ETH amount of the validators that are locked in the Beacon Deposit Contract & ETH amount of the validators that are not slashed by the EigenLayer. + * + * NOTE on the native ETH deposits: + * When dealing with NATIVE ETH deposits, we need to deduct callvalue from the balance. + * The contract calculates the amount of shares(pufETH) to mint based on the total assets. + * When a user sends ETH, the msg.value is immediately added to address(this).balance. + * Since `address(this.balance)` is used in calculating `totalAssets()`, we must deduct the `callvalue()` from the balance to prevent the user from minting excess shares. + * `msg.value` cannot be accessed from a view function, so we use assembly to get the callvalue. + */ + function totalAssets() external view returns (uint256); + + /** + * @notice Get the withdrawal credentials for the non restaking validators (this contract address) + * @return The withdrawal credentials + */ + function getWithdrawalCredentials() external view returns (bytes memory); + + /** + * @notice Get the address of the EigenPod + * @return The address of the EigenPod + */ + function getEigenPod() external view returns (address); + + /** + * @notice Get the number of restaked validators + * @return The number of restaked validators + */ + function getRestakedValidatorETH() external view returns (uint256); + + /** + * @notice Get the number of non restaked validators + * @return The number of non restaked validators + */ + function getNonRestakedValidatorETH() external view returns (uint256); +} diff --git a/src/interface/IWETH.sol b/src/interface/IWETH.sol new file mode 100644 index 0000000..0109907 --- /dev/null +++ b/src/interface/IWETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; + function withdraw(uint256 amount) external; +} diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}