diff --git a/README.md b/README.md index 0a4f1364..3ca4ae00 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,12 @@ Developers can build new Caveat Enforcers for their own use cases, and the possi [Read more on "Caveats" ->](/documents/DelegationManager.md#Caveats) +### Delegation Adapters + +Delegation Adapters are specialized contracts that bridge the gap between the delegation framework and external protocols that don't natively support delegations. + +[Read more on "Delegation Adapters" ->](/documents/Adapters.md) + ## Development ### Third Party Developers diff --git a/documents/Adapters.md b/documents/Adapters.md new file mode 100644 index 00000000..107c6168 --- /dev/null +++ b/documents/Adapters.md @@ -0,0 +1,17 @@ +# Delegation Adapters + +Delegation adapters are specialized contracts that simplify integration between the delegation framework and external protocols that don't natively support delegations. Many DeFi protocols require users to perform multi-step operations—typically providing ERC-20 approvals followed by specific function calls—which adapters combine into single, atomic, delegatable executions. This enables users to delegate complex protocol interactions while ensuring outputs are delivered to the root delegator or their specified recipients. Adapters are particularly valuable for enabling non-DeleGator accounts to redeem delegations and meet delegator-specified requirements. Since most existing protocols haven't yet implemented native delegation support, adapters serve as an essential bridge layer that converts delegation-based permissions into protocol-compatible interactions while enforcing proper restrictions and safeguards during redemption. + +## Current Adapters + +### DelegationMetaSwapAdapter + +Facilitates token swaps through DEX aggregators by leveraging ERC-20 delegations with enforced outcome validation. This adapter integrates with the MetaSwap aggregator to execute optimal token swaps while maintaining delegation-based access control. It enables users to delegate ERC-20 token permissions and add conditions for enforced outcomes by the end of redemption. The adapter creates a self-redemption mechanism that atomically executes both the ERC-20 transfer and swap during the execution phase, followed by outcome validation in the afterAllHooks phase. It supports configurable token whitelisting through caveat enforcers and includes signature validation with expiration timestamps for secure API integration. + +### LiquidStakingAdapter + +Manages stETH withdrawal operations through Lido's withdrawal queue using delegation-based permissions. This adapter specifically focuses on withdrawal functions that require ERC-20 approvals, while deposit operations are excluded since they only require ETH and do not need ERC-20 approval mechanisms. The adapter facilitates stETH withdrawal requests by using delegations to transfer stETH tokens and supports both delegation-based transfers and ERC-20 permit signatures for gasless approvals. It has been designed to enhance the delegation experience by allowing users to enforce stricter delegation restrictions related to the Lido protocol, ensuring that redeemers must send withdrawal request ownership and any resulting tokens directly to the root delegator. + +### AaveAdapter + +_(Coming soon)_ Lending and borrowing operations on Aave protocol diff --git a/script/coverage.sh b/script/coverage.sh index f9bed604..56f5397b 100755 --- a/script/coverage.sh +++ b/script/coverage.sh @@ -7,17 +7,105 @@ cd .. folder_path="coverage" if [ ! -d "$folder_path" ]; then - # If not, create the folder mkdir -p "$folder_path" echo "Folder created at: $folder_path" else echo "Folder already exists at: $folder_path" fi +# Configuration: Define test files for different EVM versions +declare -a SHANGHAI_TESTS=( + "test/helpers/LiquidStaking.t.sol" + # Add more shanghai tests here in the future + # "test/helpers/AnotherShanghaiTest.t.sol" +) +declare -a CANCUN_TESTS=( + # Add cancun tests here when needed + # "test/helpers/CancunTest.t.sol" +) -# Generates lcov.info -forge coverage --report lcov --skip scripts --report-file "$folder_path/lcov.info" +# Function to build match patterns for forge coverage +build_match_patterns() { + local tests=("$@") + local patterns="" + + for test in "${tests[@]}"; do + if [[ -n "$patterns" ]]; then + patterns="$patterns --match-path *$(basename "$test")" + else + patterns="--match-path *$(basename "$test")" + fi + done + + echo "$patterns" +} + +# Function to build no-match patterns for forge coverage +build_no_match_patterns() { + local tests=("$@") + local patterns="" + + for test in "${tests[@]}"; do + if [[ -n "$patterns" ]]; then + patterns="$patterns --no-match-path *$(basename "$test")" + else + patterns="--no-match-path *$(basename "$test")" + fi + done + + echo "$patterns" +} + +echo "Running coverage with inline EVM version flags..." +echo "-----------------------------------------------" + +# Build list of all special EVM tests to exclude from default London run +ALL_SPECIAL_EVM_TESTS=("${SHANGHAI_TESTS[@]}" "${CANCUN_TESTS[@]}") +LONDON_NO_MATCH_PATTERNS=$(build_no_match_patterns "${ALL_SPECIAL_EVM_TESTS[@]}") + +# Generate coverage for London EVM (default) - exclude special EVM tests +if [[ -n "$LONDON_NO_MATCH_PATTERNS" ]]; then + echo "Running coverage for London EVM..." + echo "Excluding: ${ALL_SPECIAL_EVM_TESTS[*]}" + forge coverage --evm-version london --report lcov --skip scripts $LONDON_NO_MATCH_PATTERNS --report-file "$folder_path/lcov-london.info" +else + echo "Running coverage for London EVM - no exclusions..." + forge coverage --evm-version london --report lcov --skip scripts --report-file "$folder_path/lcov-london.info" +fi + +# Generate coverage for Shanghai EVM tests if any exist +if [ ${#SHANGHAI_TESTS[@]} -gt 0 ]; then + echo "Running coverage for Shanghai EVM..." + echo "Including: ${SHANGHAI_TESTS[*]}" + SHANGHAI_MATCH_PATTERNS=$(build_match_patterns "${SHANGHAI_TESTS[@]}") + forge coverage --evm-version shanghai --report lcov --skip scripts $SHANGHAI_MATCH_PATTERNS --report-file "$folder_path/lcov-shanghai.info" +fi + +# Generate coverage for Cancun EVM tests if any exist +if [ ${#CANCUN_TESTS[@]} -gt 0 ]; then + echo "Running coverage for Cancun EVM..." + echo "Including: ${CANCUN_TESTS[*]}" + CANCUN_MATCH_PATTERNS=$(build_match_patterns "${CANCUN_TESTS[@]}") + forge coverage --evm-version cancun --report lcov --skip scripts $CANCUN_MATCH_PATTERNS --report-file "$folder_path/lcov-cancun.info" +fi + +# Build the list of coverage files to merge +COVERAGE_FILES=("$folder_path/lcov-london.info") +if [ ${#SHANGHAI_TESTS[@]} -gt 0 ]; then + COVERAGE_FILES+=("$folder_path/lcov-shanghai.info") +fi +if [ ${#CANCUN_TESTS[@]} -gt 0 ]; then + COVERAGE_FILES+=("$folder_path/lcov-cancun.info") +fi + +# Merge the lcov files +echo "Merging coverage reports..." +echo "Files to merge: ${COVERAGE_FILES[*]}" +lcov \ + --rc branch_coverage=1 \ + $(printf -- "--add-tracefile %s " "${COVERAGE_FILES[@]}") \ + --output-file "$folder_path/lcov.info" # Filter out test, mock, and script files lcov \ @@ -39,4 +127,4 @@ then --output-directory "$folder_path" \ "$folder_path/filtered-lcov.info" open "$folder_path/index.html" -fi \ No newline at end of file +fi \ No newline at end of file diff --git a/src/helpers/LiquidStakingAdapter.sol b/src/helpers/LiquidStakingAdapter.sol new file mode 100644 index 00000000..102e8059 --- /dev/null +++ b/src/helpers/LiquidStakingAdapter.sol @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { IWithdrawalQueue } from "./interfaces/IWithdrawalQueue.sol"; +import { Delegation, ModeCode } from "../utils/Types.sol"; + +/// @title LiquidStakingAdapter +/// @notice Adapter contract for liquid staking withdrawal operations using delegations or permits +/// @dev This contract facilitates stETH withdrawals through Lido's withdrawal queue using two approaches: +/// 1. Delegation-based: Users create delegations allowing this contract to transfer their stETH, +/// then the contract requests withdrawals on their behalf. The user retains ownership of withdrawal requests. +/// 2. Permit-based: Users sign permits allowing gasless approvals, then the contract transfers stETH +/// and requests withdrawals. +/// +/// The contract acts as an intermediary that: +/// - Receives stETH through delegation redemption or direct transfer with permit +/// - Approves the withdrawal queue to spend stETH +/// - Requests withdrawals from Lido's queue, with the original token owner maintaining request ownership +/// - Never permanently holds user funds (all operations are atomic) +/// +/// Ownable functionality is implemented for emergency token recovery only. The owner can withdraw +/// tokens that users may have accidentally sent directly to this contract (bypassing the intended +/// delegation/permit flows). Under normal operation, this contract should never hold tokens as all +/// operations transfer tokens directly between users and Lido's contracts. +contract LiquidStakingAdapter is Ownable2Step { + using SafeERC20 for IERC20; + + /// @notice Thrown when a zero address is provided for required parameters + error InvalidZeroAddress(); + + /// @notice Thrown when the number of delegations provided is not exactly one + error InvalidDelegationsLength(); + + /// @notice Thrown when no amounts are specified for withdrawal + error NoAmountsSpecified(); + + /// @notice Delegation manager for handling delegated operations + IDelegationManager public immutable delegationManager; + /// @notice Lido withdrawal queue contract + IWithdrawalQueue public immutable withdrawalQueue; + /// @notice stETH token contract + IERC20 public immutable stETH; + + /// @notice Event emitted when withdrawal requests are created + /// @param delegator Address of the delegator (stETH owner) + /// @param amounts Array of withdrawal amounts + /// @param requestIds Array of withdrawal request IDs created + event WithdrawalRequestsCreated(address indexed delegator, uint256[] amounts, uint256[] requestIds); + + /// @notice Event emitted when tokens are withdrawn + /// @param token Address of the token withdrawn + /// @param recipient Address of the recipient + /// @param amount Amount of tokens withdrawn + event StuckTokensWithdrawn(IERC20 indexed token, address indexed recipient, uint256 amount); + + /// @notice Initializes the adapter with required contract addresses + /// @param _owner Address of the owner of the contract + /// @param _delegationManager Address of the delegation manager contract + /// @param _withdrawalQueue Address of the Lido withdrawal queue contract + /// @param _stETH Address of the stETH token contract + constructor(address _owner, address _delegationManager, address _withdrawalQueue, address _stETH) Ownable(_owner) { + if (_delegationManager == address(0) || _withdrawalQueue == address(0) || _stETH == address(0)) revert InvalidZeroAddress(); + + delegationManager = IDelegationManager(_delegationManager); + withdrawalQueue = IWithdrawalQueue(_withdrawalQueue); + stETH = IERC20(_stETH); + } + + /// @notice Request withdrawals using delegation-based stETH transfer + /// @dev Uses a delegation to transfer stETH, then requests withdrawals. The delegator owns the withdrawal requests. + /// @param _delegations Array containing a single delegation for stETH transfer + /// @param _amounts Array of stETH amounts to withdraw + /// @return requestIds_ Array of withdrawal request IDs + function requestWithdrawalsByDelegation( + Delegation[] memory _delegations, + uint256[] memory _amounts + ) + external + returns (uint256[] memory requestIds_) + { + if (_delegations.length != 1) revert InvalidDelegationsLength(); + + address delegator_ = _delegations[0].delegator; + uint256 totalAmount_ = _calculateTotalAmount(_amounts); + + // Redeem delegation to transfer stETH to this contract + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(_delegations); + + ModeCode[] memory encodedModes_ = new ModeCode[](1); + encodedModes_[0] = ModeLib.encodeSimpleSingle(); + + bytes[] memory executionCallDatas_ = new bytes[](1); + bytes memory encodedTransfer_ = abi.encodeCall(IERC20.transfer, (address(this), totalAmount_)); + executionCallDatas_[0] = ExecutionLib.encodeSingle(address(stETH), 0, encodedTransfer_); + + delegationManager.redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + + // Execute common withdrawal logic + requestIds_ = _requestWithdrawals(_amounts, totalAmount_, delegator_); + } + + /// @notice Request withdrawals with permit + /// @dev Delegates can execute this function to request withdrawals using permit signatures + /// @param _amounts Array of stETH amounts to withdraw + /// @param _permit Permit signature data for gasless approval + /// @return requestIds_ Array of withdrawal request IDs + function requestWithdrawalsWithPermit( + uint256[] memory _amounts, + IWithdrawalQueue.PermitInput memory _permit + ) + external + returns (uint256[] memory requestIds_) + { + uint256 totalAmount_ = _calculateTotalAmount(_amounts); + + // Use permit to approve stETH transfer + IERC20Permit(address(stETH)).permit( + msg.sender, address(this), _permit.value, _permit.deadline, _permit.v, _permit.r, _permit.s + ); + + // Transfer stETH from sender to this contract + stETH.safeTransferFrom(msg.sender, address(this), totalAmount_); + + // Execute common withdrawal logic + requestIds_ = _requestWithdrawals(_amounts, totalAmount_, msg.sender); + } + + /** + * @notice Emergency function to recover tokens accidentally sent to this contract. + * @dev This contract should never hold ERC20 tokens as all token operations are handled + * through delegation-based transfers that move tokens directly between users and Lido. + * This function is only for recovering tokens that users may have sent to this contract + * by mistake (e.g., direct transfers instead of using delegation functions). + * @param _token The token to be recovered. + * @param _amount The amount of tokens to recover. + * @param _recipient The address to receive the recovered tokens. + */ + function withdraw(IERC20 _token, uint256 _amount, address _recipient) external onlyOwner { + IERC20(_token).safeTransfer(_recipient, _amount); + + emit StuckTokensWithdrawn(_token, _recipient, _amount); + } + + /// @notice Internal function to handle common withdrawal request logic + /// @param _amounts Array of stETH amounts to withdraw + /// @param _totalAmount Total amount of stETH to withdraw + /// @param _delegator Address of the delegator who will own the withdrawal requests + /// @return requestIds_ Array of withdrawal request IDs + function _requestWithdrawals( + uint256[] memory _amounts, + uint256 _totalAmount, + address _delegator + ) + internal + returns (uint256[] memory requestIds_) + { + _ensureAllowance(_totalAmount); + + requestIds_ = withdrawalQueue.requestWithdrawals(_amounts, _delegator); + + emit WithdrawalRequestsCreated(_delegator, _amounts, requestIds_); + } + + /// @notice Ensures sufficient token allowance for withdrawal queue operations + /// @dev Checks current allowance and increases to max if needed + /// @param _amount Amount needed for the operation + function _ensureAllowance(uint256 _amount) private { + uint256 allowance_ = stETH.allowance(address(this), address(withdrawalQueue)); + if (allowance_ < _amount) { + stETH.safeIncreaseAllowance(address(withdrawalQueue), type(uint256).max); + } + } + + /// @notice Calculates total amount from amounts array + /// @param _amounts Array of amounts to sum + /// @return total_ Total amount + function _calculateTotalAmount(uint256[] memory _amounts) private pure returns (uint256 total_) { + if (_amounts.length == 0) revert NoAmountsSpecified(); + + uint256 length_ = _amounts.length; + for (uint256 i = 0; i < length_; i++) { + total_ += _amounts[i]; + } + return total_; + } +} diff --git a/src/helpers/interfaces/IWithdrawalQueue.sol b/src/helpers/interfaces/IWithdrawalQueue.sol new file mode 100644 index 00000000..639d52a1 --- /dev/null +++ b/src/helpers/interfaces/IWithdrawalQueue.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +/// @title Interface for Lido's withdrawal queue contract +/// @notice Interface for requesting and claiming stETH withdrawals +interface IWithdrawalQueue { + /// @notice Output format struct for withdrawal status + struct WithdrawalRequestStatus { + /// @notice stETH token amount that was locked on withdrawal queue for this request + uint256 amountOfStETH; + /// @notice amount of stETH shares locked on withdrawal queue for this request + uint256 amountOfShares; + /// @notice address that can claim or transfer this request + address owner; + /// @notice timestamp of when the request was created, in seconds + uint256 timestamp; + /// @notice true, if request is finalized + bool isFinalized; + /// @notice true, if request is claimed. Request is claimable if (isFinalized && !isClaimed) + bool isClaimed; + } + + /// @notice Permit input structure for gasless approvals + struct PermitInput { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + /// @notice Request withdrawals with permit signature + /// @param _amounts Array of stETH amounts to withdraw + /// @param _owner Address that will own the withdrawal requests + /// @param _permit Permit signature data + /// @return requestIds Array of withdrawal request IDs + function requestWithdrawalsWithPermit( + uint256[] calldata _amounts, + address _owner, + PermitInput calldata _permit + ) + external + returns (uint256[] memory requestIds); + + /// @notice Request withdrawals (requires prior approval) + /// @param _amounts Array of stETH amounts to withdraw + /// @param _owner Address that will own the withdrawal requests + /// @return requestIds Array of withdrawal request IDs + function requestWithdrawals(uint256[] calldata _amounts, address _owner) external returns (uint256[] memory requestIds); + + /// @notice Get withdrawal requests for an owner + /// @param _owner Address to get requests for + /// @return requestsIds Array of request IDs + function getWithdrawalRequests(address _owner) external view returns (uint256[] memory requestsIds); + + /// @notice Returns status for requests with provided ids + /// @param _requestIds Array of withdrawal request ids + /// @return statuses Array of withdrawal request statuses + function getWithdrawalStatus(uint256[] calldata _requestIds) + external + view + returns (WithdrawalRequestStatus[] memory statuses); + + /// @notice Claim withdrawal requests + /// @param _requestIds Array of request IDs to claim + /// @param _hints Array of hints for efficient claiming + function claimWithdrawals(uint256[] calldata _requestIds, uint256[] calldata _hints) external; +} diff --git a/test/helpers/LiquidStaking.t.sol b/test/helpers/LiquidStaking.t.sol new file mode 100644 index 00000000..57249fcf --- /dev/null +++ b/test/helpers/LiquidStaking.t.sol @@ -0,0 +1,1183 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { Implementation, SignatureType, TestUser } from "../utils/Types.t.sol"; +import { Delegation, Caveat, Execution } from "../../src/utils/Types.sol"; +import { AllowedTargetsEnforcer } from "../../src/enforcers/AllowedTargetsEnforcer.sol"; +import { AllowedMethodsEnforcer } from "../../src/enforcers/AllowedMethodsEnforcer.sol"; +import { ValueLteEnforcer } from "../../src/enforcers/ValueLteEnforcer.sol"; +import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; +import { LogicalOrWrapperEnforcer } from "../../src/enforcers/LogicalOrWrapperEnforcer.sol"; +import { ILiquidStakingAggregator } from "./interfaces/ILiquidStakingAggregator.sol"; +import { LiquidStakingAdapter } from "../../src/helpers/LiquidStakingAdapter.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { IWithdrawalQueue } from "../../src/helpers/interfaces/IWithdrawalQueue.sol"; +import { EncoderLib } from "../../src/libraries/EncoderLib.sol"; +import { EIP7702StatelessDeleGator } from "../../src/EIP7702/EIP7702StatelessDeleGator.sol"; +import { ERC1271Lib } from "../../src/libraries/ERC1271Lib.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import { console2 } from "forge-std/console2.sol"; +import "forge-std/Test.sol"; + +/// @notice Interface for Lido's stETH token +interface IstETH { + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); +} + +/// @notice Interface for Rocket Pool's rETH token +interface IrETH { + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); + + /// @notice Burn rETH for ETH (when deposit pool has liquidity) + function burn(uint256 _rethAmount) external; + + /// @notice Get the current ETH value of an amount of rETH + function getEthValue(uint256 _rethAmount) external view returns (uint256); +} + +// @dev Do not remove this comment below +/// forge-config: default.evm_version = "shanghai" + +/** + * @title LiquidStakingTest + * @notice Tests for MetaMask's liquid staking functionality including both + * direct interactions and delegation-based interactions + */ +contract LiquidStakingTest is BaseTest { + ////////////////////////////// State & Constants ////////////////////////////// + + // MetaMask Liquid Staking contract address on mainnet + address public constant LIQUID_STAKING_AGGREGATOR = 0x1f6692E78dDE07FF8da75769B6d7c716215bC7D0; + + // Token addresses on mainnet + address public constant STETH_ADDRESS = 0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84; + address public constant RETH_ADDRESS = 0xae78736Cd615f374D3085123A210448E74Fc6393; + address public constant WSTETH_ADDRESS = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + + // Real mainnet addresses that have interacted with the vault + address private constant LIDO_WITHDRAWAL_REQUESTER_ADDRESS = 0xBBE3188a1e6Bfe7874F069a9164A923725B8Bd68; + address private constant LIDO_WITHDRAWAL_CLAIMER_ADDRESS = 0x95c79a359835C7471969A67d6bE35EE2B5d46ea8; + + address private constant ROCKET_POOL_WITHDRAWAL_BURNER_ADDRESS = 0xBA9deC4B0c3485F3509Ab2f582F9387094f04Fb5; + + // Lido withdrawal queue + address public constant LIDO_WITHDRAWAL_QUEUE = 0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1; + + ILiquidStakingAggregator public liquidStaking; + IstETH public stETH; + IrETH public rETH; + IWithdrawalQueue public withdrawalQueue; + + AllowedTargetsEnforcer public allowedTargetsEnforcer; + AllowedMethodsEnforcer public allowedMethodsEnforcer; + ValueLteEnforcer public valueLteEnforcer; + ERC20TransferAmountEnforcer public erc20TransferAmountEnforcer; + LogicalOrWrapperEnforcer public logicalOrWrapperEnforcer; + + TestUser public alice; + TestUser public bob; + + uint256 public constant DEPOSIT_AMOUNT = 1 ether; + uint256 public constant MAX_FEE_RATE = 0; + // uint256 public constant MAX_FEE_RATE = 1000; // 10% in basis points * 10 + + // Group indices for different liquid staking operations + uint256 private constant LIDO_DEPOSIT_GROUP = 0; + uint256 private constant ROCKETPOOL_DEPOSIT_GROUP = 1; + uint256 private constant LIDO_WITHDRAWAL_REQUEST_GROUP = 2; + uint256 private constant LIDO_WITHDRAWAL_CLAIM_GROUP = 3; + uint256 private constant ROCKETPOOL_WITHDRAWAL_GROUP = 4; + + //////////////////////// Setup //////////////////////// + + function setUpContracts() public { + IMPLEMENTATION = Implementation.Hybrid; + SIGNATURE_TYPE = SignatureType.RawP256; + + super.setUp(); + + // Set up contract interfaces + liquidStaking = ILiquidStakingAggregator(LIQUID_STAKING_AGGREGATOR); + stETH = IstETH(STETH_ADDRESS); + rETH = IrETH(RETH_ADDRESS); + withdrawalQueue = IWithdrawalQueue(LIDO_WITHDRAWAL_QUEUE); + + // Deploy enforcers + allowedTargetsEnforcer = new AllowedTargetsEnforcer(); + allowedMethodsEnforcer = new AllowedMethodsEnforcer(); + valueLteEnforcer = new ValueLteEnforcer(); + erc20TransferAmountEnforcer = new ERC20TransferAmountEnforcer(); + logicalOrWrapperEnforcer = new LogicalOrWrapperEnforcer(delegationManager); + + vm.label(address(allowedTargetsEnforcer), "AllowedTargetsEnforcer"); + vm.label(address(allowedMethodsEnforcer), "AllowedMethodsEnforcer"); + vm.label(address(valueLteEnforcer), "ValueLteEnforcer"); + vm.label(address(erc20TransferAmountEnforcer), "ERC20TransferAmountEnforcer"); + vm.label(address(logicalOrWrapperEnforcer), "LogicalOrWrapperEnforcer"); + vm.label(address(LIQUID_STAKING_AGGREGATOR), "Liquid Staking Aggregator"); + vm.label(STETH_ADDRESS, "stETH"); + vm.label(RETH_ADDRESS, "rETH"); + vm.label(LIDO_WITHDRAWAL_QUEUE, "Lido Withdrawal Queue"); + + // Set up test users with ETH + alice = users.alice; + bob = users.bob; + + vm.deal(address(alice.deleGator), 100 ether); + vm.deal(address(bob.deleGator), 100 ether); + } + + //////////////////////// Direct Interaction Tests //////////////////////// + + function test_depositToLido_direct() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791160); + setUpContracts(); + + uint256 initialBalance_ = address(alice.deleGator).balance; + uint256 initialStETHBalance_ = stETH.balanceOf(address(alice.deleGator)); + + vm.prank(address(alice.deleGator)); + liquidStaking.depositToLido{ value: DEPOSIT_AMOUNT }(MAX_FEE_RATE); + + // Check ETH was deducted + assertEq(address(alice.deleGator).balance, initialBalance_ - DEPOSIT_AMOUNT); + + // Check stETH was received (approximately equal due to fees and exchange rate) + uint256 finalStETHBalance_ = stETH.balanceOf(address(alice.deleGator)); + assertGt(finalStETHBalance_, initialStETHBalance_); + assertApproxEqRel(finalStETHBalance_, DEPOSIT_AMOUNT, 0.05e18); // 5% tolerance for fees + } + + function test_depositToRocketPool_direct() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791160); + setUpContracts(); + + uint256 initialBalance_ = address(alice.deleGator).balance; + uint256 initialRETHBalance_ = rETH.balanceOf(address(alice.deleGator)); + + vm.prank(address(alice.deleGator)); + liquidStaking.depositToRP{ value: DEPOSIT_AMOUNT }(MAX_FEE_RATE); + + // Check ETH was deducted + assertEq(address(alice.deleGator).balance, initialBalance_ - DEPOSIT_AMOUNT); + + // Check rETH was received + uint256 finalRETHBalance_ = rETH.balanceOf(address(alice.deleGator)); + assertGt(finalRETHBalance_, initialRETHBalance_); + + // rETH is repricing token, so amount received will be less than 1:1 + assertLt(finalRETHBalance_, DEPOSIT_AMOUNT); + assertGt(finalRETHBalance_, 0); + } + + //////////////////////// Withdrawal Tests //////////////////////// + + /// @notice Test Lido withdrawal request creation + /// @dev This tests the withdrawal request creation process including NFT minting + function test_lidoWithdrawalRequest_direct() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791044); + setUpContracts(); + + uint256 stETHBalance_ = stETH.balanceOf(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + assertGt(stETHBalance_, 0, "LIDO_WITHDRAWAL_REQUESTER_ADDRESS should have stETH balance"); + + // Request withdrawal (using Lido's withdrawal queue) + uint256[] memory amounts_ = new uint256[](1); + amounts_[0] = 1000 ether; + address _owner = LIDO_WITHDRAWAL_REQUESTER_ADDRESS; + IWithdrawalQueue.PermitInput memory permit_ = IWithdrawalQueue.PermitInput({ + value: 1000 ether, + deadline: 1751056511, + v: 28, + r: 0x1ba4bbbaad41d68132e71af04cf876db76b8459e555e41c368f6389d951a5990, + s: 0x635830efd45ef7057e5ef4b95b9e7c23db9893c04bf2928b11aea51a699d2483 + }); + + vm.prank(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + uint256[] memory requestIds_ = withdrawalQueue.requestWithdrawalsWithPermit(amounts_, _owner, permit_); + + assertEq(requestIds_.length, 1, "Should return exactly one request ID"); + assertEq(requestIds_[0], 84433, "Request ID mismatch"); + + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses_ = withdrawalQueue.getWithdrawalStatus(requestIds_); + assertEq(statuses_[0].amountOfStETH, 1000 ether, "stETH amount mismatch"); + assertGt(statuses_[0].amountOfShares, 0, "shares amount mismatch"); + assertEq(statuses_[0].owner, LIDO_WITHDRAWAL_REQUESTER_ADDRESS, "owner address mismatch"); + assertEq(statuses_[0].timestamp, block.timestamp, "timestamp mismatch"); + assertEq(statuses_[0].isFinalized, false, "withdrawal should not be finalized"); + assertEq(statuses_[0].isClaimed, false, "withdrawal should not be claimed"); + } + + /// @notice Test Lido withdrawal completion + /// @dev This tests the actual withdrawal process after being in the queue + function test_lidoWithdrawalCompletion_direct() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791243); + setUpContracts(); + + // Simulate user having a withdrawal request that's ready to be claimed + uint256[] memory requestIds_ = new uint256[](1); + requestIds_[0] = 68185; // Known finalized request ID + + // Get withdrawal status to verify it's ready + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses_ = withdrawalQueue.getWithdrawalStatus(requestIds_); + assertEq(statuses_[0].isFinalized, true, "Withdrawal should be finalized"); + assertEq(statuses_[0].isClaimed, false, "Withdrawal should not be claimed yet"); + assertGt(statuses_[0].amountOfStETH, 0, "Should have stETH to withdraw"); + + uint256 initialETHBalance_ = LIDO_WITHDRAWAL_CLAIMER_ADDRESS.balance; + + // Calculate hints for efficient withdrawal (in practice, this would come from an oracle) + uint256[] memory hints_ = new uint256[](1); + hints_[0] = 618; // Simplified hint for testing + + // Execute the withdrawal claim + vm.prank(LIDO_WITHDRAWAL_CLAIMER_ADDRESS); + withdrawalQueue.claimWithdrawals(requestIds_, hints_); + + // Verify ETH was received + uint256 finalETHBalance_ = LIDO_WITHDRAWAL_CLAIMER_ADDRESS.balance; + assertGt(finalETHBalance_, initialETHBalance_, "Should receive ETH from withdrawal"); + + // Verify the withdrawal is now marked as claimed + statuses_ = withdrawalQueue.getWithdrawalStatus(requestIds_); + assertEq(statuses_[0].isClaimed, true, "Withdrawal should be marked as claimed"); + } + + /// @notice Test Rocket Pool rETH burning (direct redemption when possible) + function test_rocketPoolWithdrawal_direct() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22783949); + setUpContracts(); + + address burner_ = ROCKET_POOL_WITHDRAWAL_BURNER_ADDRESS; + uint256 rETHBalanceBefore_ = rETH.balanceOf(burner_); + assertGt(rETHBalanceBefore_, 0); + + uint256 initialETHBalance_ = burner_.balance; + uint256 burnAmount_ = 411000000000000000; + + vm.prank(burner_); + rETH.burn(burnAmount_); + + assertGt(burner_.balance, initialETHBalance_, "ETH balance should increase after burning"); + assertEq(rETH.balanceOf(burner_), rETHBalanceBefore_ - burnAmount_, "rETH balance should decrease by burn amount"); + } + + //////////////////////// Delegation Tests //////////////////////// + + /** + * @notice Test Lido deposit via delegation from Alice to Bob + * @dev Uses exact value and allowed targets to ensure ETH can only be deposited to Lido with specified fee rate + */ + function test_depositToLido_viaDelegation() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791160); + setUpContracts(); + + uint256 initialStETHBalance_ = stETH.balanceOf(address(alice.deleGator)); + + // Create caveat groups for Lido deposit operations + LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = _createLiquidStakingCaveatGroups(LIDO_DEPOSIT_GROUP); + + // Create selected group for Lido deposit operations + bytes[] memory caveatArgs_ = new bytes[](3); + caveatArgs_[0] = hex""; // No args for allowedTargetsEnforcer + caveatArgs_[1] = abi.encode(DEPOSIT_AMOUNT); // Value enforcer for exact ETH amount + caveatArgs_[2] = hex""; // No args for allowedMethodsEnforcer + LogicalOrWrapperEnforcer.SelectedGroup memory selectedGroup_ = + LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: LIDO_DEPOSIT_GROUP, caveatArgs: caveatArgs_ }); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: abi.encode(selectedGroup_), enforcer: address(logicalOrWrapperEnforcer), terms: abi.encode(groups_) }); + + Delegation memory delegation_ = Delegation({ + delegate: address(bob.deleGator), + delegator: address(alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + delegation_ = signDelegation(alice, delegation_); + + Execution memory execution_ = Execution({ + target: address(liquidStaking), + value: DEPOSIT_AMOUNT, + callData: abi.encodeWithSelector(ILiquidStakingAggregator.depositToLido.selector, MAX_FEE_RATE) + }); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + invokeDelegation_UserOp(bob, delegations_, execution_); + + uint256 finalStETHBalance_ = stETH.balanceOf(address(alice.deleGator)); + assertGt(finalStETHBalance_, initialStETHBalance_, "stETH balance should have increased after delegation"); + assertApproxEqRel(finalStETHBalance_, DEPOSIT_AMOUNT, 0.05e18); // 5% tolerance for fees + } + + /** + * @notice Test Rocket Pool deposit via delegation from Alice to Bob + * @dev Uses exact value and allowed targets to ensure ETH can only be deposited to Rocket Pool with specified fee rate + */ + function test_depositToRocketPool_viaDelegation() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791160); + setUpContracts(); + + uint256 initialRETHBalance_ = rETH.balanceOf(address(alice.deleGator)); + + // Create caveat groups for Rocket Pool deposit operations + LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = _createLiquidStakingCaveatGroups(ROCKETPOOL_DEPOSIT_GROUP); + + // Create selected group for Rocket Pool deposit operations + bytes[] memory caveatArgs_ = new bytes[](3); + caveatArgs_[0] = hex""; // No args for allowedTargetsEnforcer + caveatArgs_[1] = abi.encode(DEPOSIT_AMOUNT); // Value enforcer for exact ETH amount + caveatArgs_[2] = hex""; // No args for allowedMethodsEnforcer + LogicalOrWrapperEnforcer.SelectedGroup memory selectedGroup_ = + LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: ROCKETPOOL_DEPOSIT_GROUP, caveatArgs: caveatArgs_ }); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: abi.encode(selectedGroup_), enforcer: address(logicalOrWrapperEnforcer), terms: abi.encode(groups_) }); + + Delegation memory delegation_ = Delegation({ + delegate: address(bob.deleGator), + delegator: address(alice.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + delegation_ = signDelegation(alice, delegation_); + + Execution memory execution_ = Execution({ + target: address(liquidStaking), + value: DEPOSIT_AMOUNT, + callData: abi.encodeWithSelector(ILiquidStakingAggregator.depositToRP.selector, MAX_FEE_RATE) + }); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + invokeDelegation_UserOp(bob, delegations_, execution_); + + uint256 finalRETHBalance_ = rETH.balanceOf(address(alice.deleGator)); + assertGt(finalRETHBalance_, initialRETHBalance_, "rETH balance should have increased after delegation"); + assertLt(finalRETHBalance_, DEPOSIT_AMOUNT, "rETH received should be less than 1:1 due to repricing"); + assertGt(finalRETHBalance_, 0, "Should receive some rETH"); + } + + /** + * @notice Test Lido withdrawal request via delegation using real mainnet address + * @dev The withdrawal queue automatically handles token flows and request ownership. Uses vm.prank with a real address + * that historically had stETH at this block number. + */ + function test_lidoWithdrawalRequest_viaDelegation() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791044); + setUpContracts(); + + _assignImplementationAndVerify(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + + uint256 stETHBalance = stETH.balanceOf(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + assertGt(stETHBalance, 0, "LIDO_WITHDRAWAL_REQUESTER_ADDRESS should have stETH balance"); + + // Create caveat groups for Lido withdrawal request operations + LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = _createLiquidStakingCaveatGroups(LIDO_WITHDRAWAL_REQUEST_GROUP); + + // Create selected group for withdrawal request operations + bytes[] memory caveatArgs_ = new bytes[](3); + caveatArgs_[0] = hex""; // No args for allowedTargetsEnforcer + caveatArgs_[1] = hex""; // No args for allowedMethodsEnforcer + caveatArgs_[2] = hex""; // No args for valueLteEnforcer + LogicalOrWrapperEnforcer.SelectedGroup memory selectedGroup_ = + LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: LIDO_WITHDRAWAL_REQUEST_GROUP, caveatArgs: caveatArgs_ }); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: abi.encode(selectedGroup_), enforcer: address(logicalOrWrapperEnforcer), terms: abi.encode(groups_) }); + + Delegation memory delegation_ = Delegation({ + delegate: address(bob.deleGator), + delegator: LIDO_WITHDRAWAL_REQUESTER_ADDRESS, + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + delegation_ = _mockSignDelegation(delegation_); + + uint256[] memory amounts_ = new uint256[](1); + amounts_[0] = 1000 ether; + address _owner = LIDO_WITHDRAWAL_REQUESTER_ADDRESS; + IWithdrawalQueue.PermitInput memory permit_ = IWithdrawalQueue.PermitInput({ + value: 1000 ether, + deadline: 1751056511, + v: 28, + r: 0x1ba4bbbaad41d68132e71af04cf876db76b8459e555e41c368f6389d951a5990, + s: 0x635830efd45ef7057e5ef4b95b9e7c23db9893c04bf2928b11aea51a699d2483 + }); + + Execution memory execution_ = Execution({ + target: LIDO_WITHDRAWAL_QUEUE, + value: 0, + callData: abi.encodeWithSelector(IWithdrawalQueue.requestWithdrawalsWithPermit.selector, amounts_, _owner, permit_) + }); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + invokeDelegation_UserOp(bob, delegations_, execution_); + + // Verify withdrawal request was created + uint256[] memory requestIds_ = withdrawalQueue.getWithdrawalRequests(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + assertGt(requestIds_.length, 0, "Should have at least one withdrawal request"); + } + + /** + * @notice Test Lido withdrawal claim via delegation using real mainnet address + * @dev The withdrawal queue automatically sends claimed ETH to msg.sender (root delegator). Uses vm.prank with a real address + * that historically had claimable requests at this block number. + */ + function test_lidoWithdrawalCompletion_viaDelegation() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791243); + setUpContracts(); + + _assignImplementationAndVerify(LIDO_WITHDRAWAL_CLAIMER_ADDRESS); + + uint256 initialETHBalance_ = LIDO_WITHDRAWAL_CLAIMER_ADDRESS.balance; + + // Create caveat groups for Lido withdrawal claim operations + LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = _createLiquidStakingCaveatGroups(LIDO_WITHDRAWAL_CLAIM_GROUP); + + // Create selected group for withdrawal claim operations + bytes[] memory caveatArgs_ = new bytes[](3); + caveatArgs_[0] = hex""; // No args for allowedTargetsEnforcer + caveatArgs_[1] = hex""; // No args for valueLteEnforcer + caveatArgs_[2] = hex""; // No args for allowedMethodsEnforcer + LogicalOrWrapperEnforcer.SelectedGroup memory selectedGroup_ = + LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: LIDO_WITHDRAWAL_CLAIM_GROUP, caveatArgs: caveatArgs_ }); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: abi.encode(selectedGroup_), enforcer: address(logicalOrWrapperEnforcer), terms: abi.encode(groups_) }); + + Delegation memory delegation_ = Delegation({ + delegate: address(bob.deleGator), + delegator: LIDO_WITHDRAWAL_CLAIMER_ADDRESS, + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + delegation_ = _mockSignDelegation(delegation_); + + uint256[] memory requestIds_ = new uint256[](1); + requestIds_[0] = 68185; // Known finalized request ID + uint256[] memory hints_ = new uint256[](1); + hints_[0] = 618; // Simplified hint for testing + + Execution memory execution_ = Execution({ + target: LIDO_WITHDRAWAL_QUEUE, + value: 0, + callData: abi.encodeWithSelector(IWithdrawalQueue.claimWithdrawals.selector, requestIds_, hints_) + }); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + invokeDelegation_UserOp(bob, delegations_, execution_); + + uint256 finalETHBalance_ = LIDO_WITHDRAWAL_CLAIMER_ADDRESS.balance; + assertGt(finalETHBalance_, initialETHBalance_, "ETH balance should have increased after withdrawal claim"); + } + + /** + * @notice Test Rocket Pool withdrawal via delegation using real mainnet address + * @dev The rETH contract automatically sends ETH to msg.sender (root delegator). Uses vm.prank with a real address + * that historically had rETH at this block number. + */ + function test_rocketPoolWithdrawal_viaDelegation() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22783949); + setUpContracts(); + + _assignImplementationAndVerify(ROCKET_POOL_WITHDRAWAL_BURNER_ADDRESS); + + uint256 initialETHBalance_ = ROCKET_POOL_WITHDRAWAL_BURNER_ADDRESS.balance; + uint256 rETHBalanceBefore_ = rETH.balanceOf(ROCKET_POOL_WITHDRAWAL_BURNER_ADDRESS); + assertGt(rETHBalanceBefore_, 0, "Should have rETH to burn"); + + // Create caveat groups for Rocket Pool withdrawal operations + LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = _createLiquidStakingCaveatGroups(ROCKETPOOL_WITHDRAWAL_GROUP); + + // Create selected group for withdrawal operations + bytes[] memory caveatArgs_ = new bytes[](3); + caveatArgs_[0] = hex""; // No args for allowedTargetsEnforcer + caveatArgs_[1] = hex""; // No args for valueLteEnforcer + caveatArgs_[2] = hex""; // No args for allowedMethodsEnforcer + LogicalOrWrapperEnforcer.SelectedGroup memory selectedGroup_ = + LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: ROCKETPOOL_WITHDRAWAL_GROUP, caveatArgs: caveatArgs_ }); + + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: abi.encode(selectedGroup_), enforcer: address(logicalOrWrapperEnforcer), terms: abi.encode(groups_) }); + + Delegation memory delegation_ = Delegation({ + delegate: address(bob.deleGator), + delegator: ROCKET_POOL_WITHDRAWAL_BURNER_ADDRESS, + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: 0, + signature: hex"" + }); + + delegation_ = _mockSignDelegation(delegation_); + + uint256 burnAmount_ = 411000000000000000; + + Execution memory execution_ = + Execution({ target: RETH_ADDRESS, value: 0, callData: abi.encodeWithSelector(IrETH.burn.selector, burnAmount_) }); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + + invokeDelegation_UserOp(bob, delegations_, execution_); + + uint256 finalETHBalance_ = ROCKET_POOL_WITHDRAWAL_BURNER_ADDRESS.balance; + uint256 finalRETHBalance_ = rETH.balanceOf(ROCKET_POOL_WITHDRAWAL_BURNER_ADDRESS); + + assertGt(finalETHBalance_, initialETHBalance_, "ETH balance should increase after burning"); + assertEq(finalRETHBalance_, rETHBalanceBefore_ - burnAmount_, "rETH balance should decrease by burn amount"); + } + + /** + * @notice Test Lido withdrawal request via LiquidStakingAdapter using delegation + * @dev Tests the LiquidStakingAdapter.requestWithdrawalsByDelegation function with real mainnet address + * that historically had stETH at this block number. Creates two delegations: one for stETH transfer + * and another for calling the adapter function. + */ + function test_liquidStakingAdapter_requestWithdrawalsByDelegation() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791044); + setUpContracts(); + + _assignImplementationAndVerify(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + + LiquidStakingAdapter liquidStakingAdapter_ = new LiquidStakingAdapter( + address(alice.deleGator), // owner + address(delegationManager), + LIDO_WITHDRAWAL_QUEUE, + STETH_ADDRESS + ); + + uint256 initialStETHBalance_ = stETH.balanceOf(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + assertGt(initialStETHBalance_, 0, "LIDO_WITHDRAWAL_REQUESTER_ADDRESS should have stETH balance"); + + uint256 withdrawalAmount_ = 1000 ether; + + Delegation memory stETHTransferDelegation_ = + _createStETHTransferDelegation(LIDO_WITHDRAWAL_REQUESTER_ADDRESS, address(liquidStakingAdapter_)); + + Delegation memory adapterDelegation_ = _createAdapterMethodDelegation( + address(bob.deleGator), + LIDO_WITHDRAWAL_REQUESTER_ADDRESS, + address(liquidStakingAdapter_), + 1 // Different salt + ); + + Delegation[] memory stETHDelegations_ = new Delegation[](1); + stETHDelegations_[0] = stETHTransferDelegation_; + + uint256[] memory amounts_ = new uint256[](1); + amounts_[0] = withdrawalAmount_; + + Execution memory execution_ = Execution({ + target: address(liquidStakingAdapter_), + value: 0, + callData: abi.encodeWithSelector(LiquidStakingAdapter.requestWithdrawalsByDelegation.selector, stETHDelegations_, amounts_) + }); + + Delegation[] memory adapterDelegations_ = new Delegation[](1); + adapterDelegations_[0] = adapterDelegation_; + + uint256 stETHBalanceBeforeExecution_ = stETH.balanceOf(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + + invokeDelegation_UserOp(bob, adapterDelegations_, execution_); + + assertEq( + stETH.balanceOf(LIDO_WITHDRAWAL_REQUESTER_ADDRESS), + stETHBalanceBeforeExecution_ - withdrawalAmount_, + "stETH should be transferred from delegator" + ); + + assertEq(stETH.balanceOf(address(liquidStakingAdapter_)), 0, "Adapter should not hold stETH after withdrawal request"); + + uint256[] memory requestIds_ = withdrawalQueue.getWithdrawalRequests(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + assertGt(requestIds_.length, 0, "Should have at least one withdrawal request"); + + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses_ = withdrawalQueue.getWithdrawalStatus(requestIds_); + uint256 latestIndex_ = statuses_.length - 1; + + assertEq(statuses_[latestIndex_].amountOfStETH, withdrawalAmount_, "Withdrawal amount should match"); + assertEq( + statuses_[latestIndex_].owner, LIDO_WITHDRAWAL_REQUESTER_ADDRESS, "Original delegator should own the withdrawal request" + ); + assertEq(statuses_[latestIndex_].isFinalized, false, "Withdrawal should not be finalized yet"); + assertEq(statuses_[latestIndex_].isClaimed, false, "Withdrawal should not be claimed yet"); + } + + /** + * @notice Test LiquidStakingAdapter custom errors + */ + function test_liquidStakingAdapter_errors() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791044); + setUpContracts(); + + vm.expectRevert(LiquidStakingAdapter.InvalidZeroAddress.selector); + new LiquidStakingAdapter(address(alice.deleGator), address(0), LIDO_WITHDRAWAL_QUEUE, STETH_ADDRESS); + + vm.expectRevert(LiquidStakingAdapter.InvalidZeroAddress.selector); + new LiquidStakingAdapter(address(alice.deleGator), address(delegationManager), address(0), STETH_ADDRESS); + + vm.expectRevert(LiquidStakingAdapter.InvalidZeroAddress.selector); + new LiquidStakingAdapter(address(alice.deleGator), address(delegationManager), LIDO_WITHDRAWAL_QUEUE, address(0)); + + // Deploy valid adapter for further testing + LiquidStakingAdapter liquidStakingAdapter_ = + new LiquidStakingAdapter(address(alice.deleGator), address(delegationManager), LIDO_WITHDRAWAL_QUEUE, STETH_ADDRESS); + + // Test InvalidDelegationsLength error + Delegation[] memory emptyDelegations_ = new Delegation[](0); + uint256[] memory amounts_ = new uint256[](1); + amounts_[0] = 100 ether; + + vm.expectRevert(LiquidStakingAdapter.InvalidDelegationsLength.selector); + liquidStakingAdapter_.requestWithdrawalsByDelegation(emptyDelegations_, amounts_); + + Delegation[] memory tooManyDelegations_ = new Delegation[](2); + vm.expectRevert(LiquidStakingAdapter.InvalidDelegationsLength.selector); + liquidStakingAdapter_.requestWithdrawalsByDelegation(tooManyDelegations_, amounts_); + + // Test NoAmountsSpecified error + Delegation[] memory singleDelegation_ = new Delegation[](1); + uint256[] memory emptyAmounts_ = new uint256[](0); + + vm.expectRevert(LiquidStakingAdapter.NoAmountsSpecified.selector); + liquidStakingAdapter_.requestWithdrawalsByDelegation(singleDelegation_, emptyAmounts_); + } + + /** + * @notice Test LiquidStakingAdapter withdraw function + */ + function test_liquidStakingAdapter_withdraw() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791044); + setUpContracts(); + + LiquidStakingAdapter liquidStakingAdapter_ = + new LiquidStakingAdapter(address(alice.deleGator), address(delegationManager), LIDO_WITHDRAWAL_QUEUE, STETH_ADDRESS); + + uint256 testAmount_ = 10 ether; + BasicERC20 basicERC20_ = new BasicERC20(address(liquidStakingAdapter_), "stETH", "stETH", testAmount_); + console2.log("basicERC20", address(basicERC20_)); + // deal(address(basicERC20_), address(liquidStakingAdapter_), testAmount_); + + // Test onlyOwner modifier - should fail when called by non-owner + vm.prank(address(bob.deleGator)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(bob.deleGator))); + liquidStakingAdapter_.withdraw(IERC20(address(basicERC20_)), 0, address(bob.deleGator)); + + uint256 adapterBalance_ = basicERC20_.balanceOf(address(liquidStakingAdapter_)); + + // Test ERC20 token withdrawal (as owner) + uint256 bobInitialBalance_ = basicERC20_.balanceOf(address(bob.deleGator)); + + vm.prank(address(alice.deleGator)); + liquidStakingAdapter_.withdraw(IERC20(address(basicERC20_)), adapterBalance_, address(bob.deleGator)); + + assertEq(basicERC20_.balanceOf(address(liquidStakingAdapter_)), 0, "Adapter should have no basicERC20 after withdrawal"); + assertEq( + basicERC20_.balanceOf(address(bob.deleGator)), + bobInitialBalance_ + adapterBalance_, + "Bob should receive withdrawn basicERC20" + ); + } + + /** + * @notice Test requestWithdrawalsWithPermit function + */ + function test_liquidStakingAdapter_requestWithdrawalsWithPermit() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791044); + setUpContracts(); + + LiquidStakingAdapter liquidStakingAdapter_ = + new LiquidStakingAdapter(address(alice.deleGator), address(delegationManager), LIDO_WITHDRAWAL_QUEUE, STETH_ADDRESS); + + uint256 aliceStETHBalance_ = stETH.balanceOf(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + uint256 withdrawalAmount_ = 1000 ether; + require(aliceStETHBalance_ >= withdrawalAmount_, "Alice should have enough stETH"); + + // Create permit signature (using dummy values for testing) + IWithdrawalQueue.PermitInput memory permit_ = IWithdrawalQueue.PermitInput({ + value: withdrawalAmount_, + deadline: block.timestamp + 1 hours, + v: 27, + r: bytes32(uint256(1)), + s: bytes32(uint256(2)) + }); + + uint256[] memory amounts_ = new uint256[](1); + amounts_[0] = withdrawalAmount_; + + // Mock the permit call since we can't generate real permit signatures in tests + vm.mockCall( + STETH_ADDRESS, + abi.encodeWithSelector( + IERC20Permit.permit.selector, + LIDO_WITHDRAWAL_REQUESTER_ADDRESS, + address(liquidStakingAdapter_), + permit_.value, + permit_.deadline, + permit_.v, + permit_.r, + permit_.s + ), + "" + ); + + // Approve stETH transfer to adapter - mocked + vm.prank(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + stETH.approve(address(liquidStakingAdapter_), withdrawalAmount_); + + // Call the permit-based function + vm.prank(LIDO_WITHDRAWAL_REQUESTER_ADDRESS); + uint256[] memory requestIds_ = liquidStakingAdapter_.requestWithdrawalsWithPermit(amounts_, permit_); + + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses_ = withdrawalQueue.getWithdrawalStatus(requestIds_); + assertEq(statuses_[0].amountOfStETH, withdrawalAmount_, "stETH amount mismatch"); + assertGt(statuses_[0].amountOfShares, 0, "shares amount mismatch"); + assertEq(statuses_[0].owner, LIDO_WITHDRAWAL_REQUESTER_ADDRESS, "owner address mismatch"); + assertEq(statuses_[0].timestamp, block.timestamp, "timestamp mismatch"); + assertEq(statuses_[0].isFinalized, false, "withdrawal should not be finalized"); + assertEq(statuses_[0].isClaimed, false, "withdrawal should not be claimed"); + } + + /** + * @notice Test permit functionality in requestWithdrawalsWithPermit + * @dev This test verifies that permit and transferFrom work correctly even though + * the transaction will revert at the withdrawal queue stage due to wrong caller + */ + function test_liquidStakingAdapter_permitFunctionality() public { + vm.createSelectFork(vm.envString("MAINNET_RPC_URL"), 22791044); + setUpContracts(); + + LiquidStakingAdapter liquidStakingAdapter_ = new LiquidStakingAdapter( + address(users.alice.deleGator), address(delegationManager), LIDO_WITHDRAWAL_QUEUE, STETH_ADDRESS + ); + + uint256 withdrawalAmount_ = 1000 ether; + + // Create permit input with real signature + IWithdrawalQueue.PermitInput memory permit_ = _createPermitInput( + address(users.alice.deleGator), address(liquidStakingAdapter_), withdrawalAmount_, users.alice.privateKey + ); + + uint256[] memory amounts_ = new uint256[](1); + amounts_[0] = withdrawalAmount_; + + // Deposit ETH to get stETH for alice + vm.deal(address(users.alice.deleGator), 10000 ether); + vm.prank(address(users.alice.deleGator)); + liquidStaking.depositToLido{ value: 10000 ether }(0); + + // Expect the permit call to be made + vm.expectCall( + STETH_ADDRESS, + abi.encodeWithSelector( + IERC20Permit.permit.selector, + address(users.alice.deleGator), + address(liquidStakingAdapter_), + permit_.value, + permit_.deadline, + permit_.v, + permit_.r, + permit_.s + ) + ); + + // Expect the transferFrom call to be made + vm.expectCall( + STETH_ADDRESS, + abi.encodeWithSelector( + IERC20.transferFrom.selector, address(users.alice.deleGator), address(liquidStakingAdapter_), withdrawalAmount_ + ) + ); + + // Expect the safeIncreaseAllowance call to be made to the withdrawal queue + vm.expectCall(STETH_ADDRESS, abi.encodeWithSelector(IERC20.approve.selector, LIDO_WITHDRAWAL_QUEUE, type(uint256).max)); + + // Expect Transfer event from alice to adapter + vm.expectEmit(true, true, false, true, STETH_ADDRESS); + emit IERC20.Transfer(address(users.alice.deleGator), address(liquidStakingAdapter_), withdrawalAmount_); + + // Expect Approval event for withdrawal queue + vm.expectEmit(true, true, false, true, STETH_ADDRESS); + emit IERC20.Approval(address(liquidStakingAdapter_), LIDO_WITHDRAWAL_QUEUE, type(uint256).max); + + // Call the function as alice - this will work for permit/transfer but fail at withdrawal queue + vm.prank(address(users.alice.deleGator)); + + // We expect this to revert at the withdrawal queue stage, but permit and transfer should work + uint256[] memory requestIds_ = liquidStakingAdapter_.requestWithdrawalsWithPermit(amounts_, permit_); + + IWithdrawalQueue.WithdrawalRequestStatus[] memory statuses_ = withdrawalQueue.getWithdrawalStatus(requestIds_); + assertEq(statuses_[0].amountOfStETH, withdrawalAmount_, "stETH amount mismatch"); + assertGt(statuses_[0].amountOfShares, 0, "shares amount mismatch"); + assertEq(statuses_[0].owner, address(users.alice.deleGator), "owner address mismatch"); + assertEq(statuses_[0].timestamp, block.timestamp, "timestamp mismatch"); + assertEq(statuses_[0].isFinalized, false, "withdrawal should not be finalized"); + assertEq(statuses_[0].isClaimed, false, "withdrawal should not be claimed"); + } + + /** + * @notice Creates a permit input with a real EIP-712 signature + * @param _owner The address that owns the tokens + * @param _spender The address that will be allowed to spend the tokens + * @param _value The amount of tokens to permit + * @param _privateKey The private key to sign the permit with + * @return permit_ The permit input with signature + */ + function _createPermitInput( + address _owner, + address _spender, + uint256 _value, + uint256 _privateKey + ) + internal + view + returns (IWithdrawalQueue.PermitInput memory permit_) + { + // Get owner's nonce for permit + uint256 nonce_ = IERC20Permit(STETH_ADDRESS).nonces(_owner); + + // Create EIP-712 permit signature + bytes32 PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + // Get domain separator from stETH contract + bytes32 domainSeparator_; + try IERC20Permit(STETH_ADDRESS).DOMAIN_SEPARATOR() returns (bytes32 _domainSeparator) { + domainSeparator_ = _domainSeparator; + } catch { + // Fallback: construct domain separator manually if not available + domainSeparator_ = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256("Liquid staked Ether 2.0"), + keccak256("2"), + block.chainid, + STETH_ADDRESS + ) + ); + } + + bytes32 structHash_ = keccak256( + abi.encode( + PERMIT_TYPEHASH, + _owner, + _spender, + _value, + nonce_, + (block.timestamp + 1 hours) // deadline + ) + ); + + bytes32 digest_ = keccak256(abi.encodePacked("\x19\x01", domainSeparator_, structHash_)); + + // Sign with the provided private key + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(_privateKey, digest_); + + // Create permit input with real signature + permit_ = IWithdrawalQueue.PermitInput({ value: _value, deadline: (block.timestamp + 1 hours), v: v_, r: r_, s: s_ }); + } + + ////////////////////// Helper Functions ////////////////////// + + /** + * @notice Creates a delegation for stETH transfers from delegator to adapter + * @param _delegator The address that will delegate stETH transfer rights + * @param _adapterAddress The address of the LiquidStakingAdapter contract + * @return stETHTransferDelegation_ The signed delegation for stETH transfers + */ + function _createStETHTransferDelegation( + address _delegator, + address _adapterAddress + ) + internal + returns (Delegation memory stETHTransferDelegation_) + { + // Create adapter-specific caveat groups + LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = _createAdapterCaveatGroups(_adapterAddress); + + // Create selected group for stETH transfer operations (group 0) + bytes[] memory transferCaveatArgs_ = new bytes[](2); + transferCaveatArgs_[0] = hex""; // No args for allowedTargetsEnforcer + transferCaveatArgs_[1] = hex""; // No args for erc20TransferAmountEnforcer + LogicalOrWrapperEnforcer.SelectedGroup memory stETHTransferGroup_ = + LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: 0, caveatArgs: transferCaveatArgs_ }); + + Caveat[] memory transferCaveats_ = new Caveat[](1); + transferCaveats_[0] = Caveat({ + args: abi.encode(stETHTransferGroup_), + enforcer: address(logicalOrWrapperEnforcer), + terms: abi.encode(groups_) + }); + + stETHTransferDelegation_ = Delegation({ + delegate: _adapterAddress, + delegator: _delegator, + authority: ROOT_AUTHORITY, + caveats: transferCaveats_, + salt: 0, + signature: hex"" + }); + + stETHTransferDelegation_ = _mockSignDelegation(stETHTransferDelegation_); + } + + /** + * @notice Creates a delegation for calling adapter methods + * @param _delegate The address that will be allowed to call adapter methods + * @param _delegator The address that owns the adapter (delegator) + * @param _adapterAddress The address of the LiquidStakingAdapter contract + * @param _salt The salt for the delegation (to avoid collisions) + * @return adapterDelegation_ The signed delegation for adapter method calls + */ + function _createAdapterMethodDelegation( + address _delegate, + address _delegator, + address _adapterAddress, + uint256 _salt + ) + internal + returns (Delegation memory adapterDelegation_) + { + // Create adapter-specific caveat groups + LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_ = _createAdapterCaveatGroups(_adapterAddress); + + // Create selected group for adapter method calls (group 1) + bytes[] memory adapterCaveatArgs_ = new bytes[](2); + adapterCaveatArgs_[0] = hex""; // No args for allowedTargetsEnforcer + adapterCaveatArgs_[1] = hex""; // No args for allowedMethodsEnforcer + LogicalOrWrapperEnforcer.SelectedGroup memory adapterCallGroup_ = + LogicalOrWrapperEnforcer.SelectedGroup({ groupIndex: 1, caveatArgs: adapterCaveatArgs_ }); + + Caveat[] memory adapterCaveats_ = new Caveat[](1); + adapterCaveats_[0] = + Caveat({ args: abi.encode(adapterCallGroup_), enforcer: address(logicalOrWrapperEnforcer), terms: abi.encode(groups_) }); + + adapterDelegation_ = Delegation({ + delegate: _delegate, + delegator: _delegator, + authority: ROOT_AUTHORITY, + caveats: adapterCaveats_, + salt: _salt, + signature: hex"" + }); + + adapterDelegation_ = _mockSignDelegation(adapterDelegation_); + } + + /** + * @notice Creates caveat groups for different liquid staking operations + * @param _groupIndex The group index (0=lidoDeposit, 1=rocketPoolDeposit, 2=lidoWithdrawalRequest, 3=lidoWithdrawalClaim, + * 4=rocketPoolWithdrawal) + * @return groups_ Array of caveat groups + */ + function _createLiquidStakingCaveatGroups(uint256 _groupIndex) + internal + view + returns (LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_) + { + require(_groupIndex <= 4, "Invalid group index"); + + groups_ = new LogicalOrWrapperEnforcer.CaveatGroup[](5); + + // Group 0: Lido deposit operations + { + Caveat[] memory lidoDepositCaveats_ = new Caveat[](3); + lidoDepositCaveats_[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(liquidStaking)) }); + lidoDepositCaveats_[1] = Caveat({ + args: hex"", + enforcer: address(valueLteEnforcer), + terms: abi.encode(uint256(type(uint256).max)) // Allow any ETH value for flexibility + }); + lidoDepositCaveats_[2] = Caveat({ + args: hex"", + enforcer: address(allowedMethodsEnforcer), + terms: abi.encodePacked(ILiquidStakingAggregator.depositToLido.selector) + }); + groups_[0] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: lidoDepositCaveats_ }); + } + + // Group 1: Rocket Pool deposit operations + { + Caveat[] memory rpDepositCaveats = new Caveat[](3); + rpDepositCaveats[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(liquidStaking)) }); + rpDepositCaveats[1] = Caveat({ + args: hex"", + enforcer: address(valueLteEnforcer), + terms: abi.encode(uint256(type(uint256).max)) // Allow any ETH value for flexibility + }); + rpDepositCaveats[2] = Caveat({ + args: hex"", + enforcer: address(allowedMethodsEnforcer), + terms: abi.encodePacked(ILiquidStakingAggregator.depositToRP.selector) + }); + groups_[1] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: rpDepositCaveats }); + } + + // Group 2: Lido withdrawal request operations + { + Caveat[] memory lidoRequestCaveats = new Caveat[](3); + lidoRequestCaveats[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(LIDO_WITHDRAWAL_QUEUE) }); + lidoRequestCaveats[1] = Caveat({ + args: hex"", + enforcer: address(allowedMethodsEnforcer), + terms: abi.encodePacked(IWithdrawalQueue.requestWithdrawalsWithPermit.selector) + }); + lidoRequestCaveats[2] = Caveat({ + args: hex"", + enforcer: address(valueLteEnforcer), + terms: abi.encode(uint256(0)) // No ETH value for requestWithdrawalsWithPermit + }); + groups_[2] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: lidoRequestCaveats }); + } + + // Group 3: Lido withdrawal claim operations + { + Caveat[] memory lidoClaimCaveats = new Caveat[](3); + lidoClaimCaveats[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(LIDO_WITHDRAWAL_QUEUE) }); + lidoClaimCaveats[1] = Caveat({ + args: hex"", + enforcer: address(valueLteEnforcer), + terms: abi.encode(uint256(0)) // No ETH value for claims + }); + lidoClaimCaveats[2] = Caveat({ + args: hex"", + enforcer: address(allowedMethodsEnforcer), + terms: abi.encodePacked(IWithdrawalQueue.claimWithdrawals.selector) + }); + groups_[3] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: lidoClaimCaveats }); + } + + // Group 4: Rocket Pool withdrawal operations + { + Caveat[] memory rpWithdrawalCaveats = new Caveat[](3); + rpWithdrawalCaveats[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(RETH_ADDRESS) }); + rpWithdrawalCaveats[1] = Caveat({ + args: hex"", + enforcer: address(valueLteEnforcer), + terms: abi.encode(uint256(0)) // No ETH value for burns + }); + rpWithdrawalCaveats[2] = + Caveat({ args: hex"", enforcer: address(allowedMethodsEnforcer), terms: abi.encodePacked(IrETH.burn.selector) }); + groups_[4] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: rpWithdrawalCaveats }); + } + } + + /** + * @notice Creates caveat groups for LiquidStakingAdapter operations + * @param _adapterAddress The address of the LiquidStakingAdapter contract + * @return groups_ Array of caveat groups for adapter operations + */ + function _createAdapterCaveatGroups(address _adapterAddress) + internal + view + returns (LogicalOrWrapperEnforcer.CaveatGroup[] memory groups_) + { + groups_ = new LogicalOrWrapperEnforcer.CaveatGroup[](2); + + // Group 0: stETH transfer operations (for delegator to adapter transfers) + { + Caveat[] memory stETHTransferCaveats = new Caveat[](2); + stETHTransferCaveats[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(STETH_ADDRESS) }); + stETHTransferCaveats[1] = Caveat({ + args: hex"", + enforcer: address(erc20TransferAmountEnforcer), + terms: abi.encodePacked(STETH_ADDRESS, uint256(1000 ether)) // 1000 stETH max + }); + groups_[0] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: stETHTransferCaveats }); + } + + // Group 1: Adapter method calls + { + Caveat[] memory adapterMethodCaveats = new Caveat[](2); + adapterMethodCaveats[0] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(_adapterAddress) }); + adapterMethodCaveats[1] = Caveat({ + args: hex"", + enforcer: address(allowedMethodsEnforcer), + terms: abi.encodePacked(LiquidStakingAdapter.requestWithdrawalsByDelegation.selector) + }); + groups_[1] = LogicalOrWrapperEnforcer.CaveatGroup({ caveats: adapterMethodCaveats }); + } + } + + /** + * @notice Assigns EIP-7702 implementation to an address and verifies NAME() function + */ + function _assignImplementationAndVerify(address _account) internal { + vm.etch(_account, bytes.concat(hex"ef0100", abi.encodePacked(address(eip7702StatelessDeleGatorImpl)))); + + string memory name_ = EIP7702StatelessDeleGator(payable(_account)).NAME(); + assertEq(name_, "EIP7702StatelessDeleGator", "NAME() should return correct implementation name"); + } + + /** + * @notice Mocks signature validation for delegation testing + * @dev Required because it's not possible to produce real signatures from pranked addresses + */ + function _mockSignDelegation(Delegation memory _delegation) internal returns (Delegation memory delegation_) { + bytes32 delegationHash_ = EncoderLib._getDelegationHash(_delegation); + bytes32 domainHash_ = delegationManager.getDomainHash(); + bytes32 typedDataHash_ = MessageHashUtils.toTypedDataHash(domainHash_, delegationHash_); + + bytes memory dummySignature_ = + hex"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1b"; + + vm.mockCall( + address(_delegation.delegator), + abi.encodeWithSelector(IERC1271.isValidSignature.selector, typedDataHash_, dummySignature_), + abi.encode(ERC1271Lib.EIP1271_MAGIC_VALUE) + ); + + delegation_ = Delegation({ + delegate: _delegation.delegate, + delegator: _delegation.delegator, + authority: _delegation.authority, + caveats: _delegation.caveats, + salt: _delegation.salt, + signature: dummySignature_ + }); + } +} diff --git a/test/helpers/interfaces/ILiquidStakingAggregator.sol b/test/helpers/interfaces/ILiquidStakingAggregator.sol new file mode 100644 index 00000000..110ae6c0 --- /dev/null +++ b/test/helpers/interfaces/ILiquidStakingAggregator.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +/// @title Interface for a liquid staking aggregation contract +/// @author Jack Clancy - Consensys +interface ILiquidStakingAggregator { + /// @notice Deposit ETH to Lido and receive stETH + /// @param maxFeeRate Maximum fee rate to accept (in basis points * 10) + function depositToLido(uint256 maxFeeRate) external payable; + + /// @notice Deposit ETH to Rocket Pool and receive rETH + /// @param maxFeeRate Maximum fee rate to accept (in basis points * 10) + function depositToRP(uint256 maxFeeRate) external payable; + + /// @notice Deposits MATIC to Lido and forwards minted stMatic to caller + /// @param amount Amount of MATIC to deposit + /// @param maxFeeRate Maximum fee rate the caller is willing to accept + function depositToStMatic(uint256 amount, uint256 maxFeeRate) external; + + /// @notice Deposits MATIC to Stader and forwards minted MaticX to caller + /// @param amount Amount of MATIC to deposit + /// @param maxFeeRate Maximum fee rate the caller is willing to accept + function depositToMaticx(uint256 amount, uint256 maxFeeRate) external; + + /// @notice Updates the fee for staking transactions + /// @dev Fee is in 0.1bp increments. i.e. fee = 10 is setting to 1bp + /// @param _newFee The new fee for future transactions + function updateFee(uint256 _newFee) external; + + /// @notice Updates the recipient of the fees collected by the contract + /// @param _newFeesRecipent The recipient of future fees + function updateFeesRecipient(address payable _newFeesRecipent) external; + + /// @notice Returns several RocketPool constants that the FE needs + /// @dev Deposit fee in wei. Number needs to be divided by 1e18 to get in percentage + /// @return Array containing [currentDeposits, depositFee, depositPoolCap, exchangeRate] + function fetchRPConstants() external view returns (uint256[4] memory); + + /// @notice Gets the current fee rate + /// @return Current fee in 1/10th of bps + function fee() external view returns (uint256); + + /// @notice Gets the current fees recipient + /// @return Address of the current fees recipient + function feesRecipient() external view returns (address payable); +}