Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions documents/Adapters.md
Original file line number Diff line number Diff line change
@@ -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
96 changes: 92 additions & 4 deletions script/coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -39,4 +127,4 @@ then
--output-directory "$folder_path" \
"$folder_path/filtered-lcov.info"
open "$folder_path/index.html"
fi
fi
195 changes: 195 additions & 0 deletions src/helpers/LiquidStakingAdapter.sol
Original file line number Diff line number Diff line change
@@ -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_;
}
}
Loading
Loading