Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support third party withdrawals with queueWithdrawalsWithSignature #676

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
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
118 changes: 103 additions & 15 deletions src/contracts/core/DelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,68 @@ contract DelegationManager is
return withdrawalRoots;
}

/**
* Allows a third party to withdraw shares on behalf of a staker with their signature.
* 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 fully withdrawn after a delay.
*/
function queueWithdrawalsWithSignature(
QueuedWithdrawalWithSignatureParams[] calldata queuedWithdrawalWithSigParams
)
external
onlyWhenNotPaused(PAUSED_ENTER_WITHDRAWAL_QUEUE)
returns (bytes32[] memory)
{
bytes32[] memory withdrawalRoots = new bytes32[](queuedWithdrawalWithSigParams.length);
address operator = delegatedTo[msg.sender];

for (uint256 i = 0; i < queuedWithdrawalWithSigParams.length; i++) {
require(
queuedWithdrawalWithSigParams[i].strategies.length == queuedWithdrawalWithSigParams[i].shares.length,
"DelegationManager.queueWithdrawalsWithSignature: input length mismatch"
);
require(
queuedWithdrawalWithSigParams[i].expiry >= block.timestamp,
"DelegationManager.queueWithdrawalsWithSignature: signature expired"
);

uint256 nonce = withdrawerNonce[queuedWithdrawalWithSigParams[i].staker];

bytes32 digestHash = calculateQueueWithdrawalDigestHash(
queuedWithdrawalWithSigParams[i].staker,
queuedWithdrawalWithSigParams[i].strategies,
queuedWithdrawalWithSigParams[i].shares,
nonce,
queuedWithdrawalWithSigParams[i].expiry
);

unchecked {
withdrawerNonce[queuedWithdrawalWithSigParams[i].staker] = nonce + 1;
}

EIP1271SignatureUtils.checkSignature_EIP1271(
queuedWithdrawalWithSigParams[i].staker,
digestHash,
queuedWithdrawalWithSigParams[i].signature
);

// Remove shares from staker's strategies and place strategies/shares in queue.
// If the staker is delegated to an operator, the operator's delegated shares are also reduced
// NOTE: This will fail if the staker doesn't have the shares implied by the input parameters
withdrawalRoots[i] = _removeSharesAndQueueWithdrawal({
staker: queuedWithdrawalWithSigParams[i].staker,
operator: operator,
withdrawer: queuedWithdrawalWithSigParams[i].withdrawer,
strategies: queuedWithdrawalWithSigParams[i].strategies,
shares: queuedWithdrawalWithSigParams[i].shares
});
}
return withdrawalRoots;
}

/**
* @notice Used to complete the specified `withdrawal`. The caller must match `withdrawal.withdrawer`
* @param withdrawal The Withdrawal to complete.
Expand Down Expand Up @@ -394,9 +456,9 @@ contract DelegationManager is
// forgefmt: disable-next-item
// subtract strategy shares from delegate's shares
_decreaseOperatorShares({
operator: operator,
staker: staker,
strategy: strategy,
operator: operator,
staker: staker,
strategy: strategy,
shares: shares
});
}
Expand Down Expand Up @@ -490,18 +552,18 @@ contract DelegationManager is
// forgefmt: disable-next-item
// calculate the digest hash
bytes32 approverDigestHash = calculateDelegationApprovalDigestHash(
staker,
operator,
_delegationApprover,
approverSalt,
staker,
operator,
_delegationApprover,
approverSalt,
approverSignatureAndExpiry.expiry
);

// forgefmt: disable-next-item
// actually check that the signature is valid
EIP1271SignatureUtils.checkSignature_EIP1271(
_delegationApprover,
approverDigestHash,
_delegationApprover,
approverDigestHash,
approverSignatureAndExpiry.signature
);
}
Expand All @@ -515,9 +577,9 @@ contract DelegationManager is
for (uint256 i = 0; i < strategies.length;) {
// forgefmt: disable-next-item
_increaseOperatorShares({
operator: operator,
staker: staker,
strategy: strategies[i],
operator: operator,
staker: staker,
strategy: strategies[i],
shares: shares[i]
});

Expand Down Expand Up @@ -675,9 +737,9 @@ contract DelegationManager is
if (operator != address(0)) {
// forgefmt: disable-next-item
_decreaseOperatorShares({
operator: operator,
staker: staker,
strategy: strategies[i],
operator: operator,
staker: staker,
strategy: strategies[i],
shares: shares[i]
});
}
Expand Down Expand Up @@ -985,6 +1047,32 @@ contract DelegationManager is
return approverDigestHash;
}

function calculateQueueWithdrawalDigestHash(
address staker,
IStrategy[] memory strategies,
uint256[] memory shares,
uint256 stakerNonce,
uint256 expiry
) public view returns (bytes32) {

// calculate the struct hash
bytes32 structHash = keccak256(abi.encode(
QUEUE_WITHDRAWAL_TYPEHASH,
staker,
strategies,
shares,
stakerNonce,
expiry
));
// calculate the digest hash
bytes32 digestHash = keccak256(abi.encodePacked(
"\x19\x01",
domainSeparator(),
structHash
));
return digestHash;
}

/**
* @dev Recalculates the domain separator when the chainid changes due to a fork.
*/
Expand Down
7 changes: 7 additions & 0 deletions src/contracts/core/DelegationManagerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ abstract contract DelegationManagerStorage is IDelegationManager {
"DelegationApproval(address delegationApprover,address staker,address operator,bytes32 salt,uint256 expiry)"
);

/// @notice The EIP-712 typehash for the `QueueWithdrawal` struct used by the contract
bytes32 public constant QUEUE_WITHDRAWAL_TYPEHASH =
keccak256("QueueWithdrawal(address staker,address[] strategies,uint256[] shares,uint256 nonce,uint256 expiry)");

/**
* @notice Original EIP-712 Domain separator for this contract.
* @dev The domain separator may change in the event of a fork that modifies the ChainID.
Expand Down Expand Up @@ -69,6 +73,9 @@ abstract contract DelegationManagerStorage is IDelegationManager {
/// @notice Mapping: staker => number of signed messages (used in `delegateToBySignature`) from the staker that this contract has already checked.
mapping(address => uint256) public stakerNonce;

/// @notice Mapping: staker => number of signed messages (used in `queueWithdrawalWithSignature`) from the staker that this contract has already checked.
mapping(address => uint256) public withdrawerNonce;

/**
* @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` and `delegateToBySignature` functions. Note that these functions only process the delegationApprover's
Expand Down
35 changes: 35 additions & 0 deletions src/contracts/interfaces/IDelegationManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ interface IDelegationManager is ISignatureUtils {
address withdrawer;
}

struct QueuedWithdrawalWithSignatureParams {
// Array of strategies that the QueuedWithdrawal contains
IStrategy[] strategies;
// Array containing the amount of shares in each Strategy in the `strategies` array
uint256[] shares;
// The address of the withdrawer
address withdrawer;
// The address of the staker
address staker;
// signature of the staker
bytes signature;
// expiration timestamp of the signature
uint256 expiry;
}

// @notice Emitted when a new operator registers in EigenLayer and provides their OperatorDetails.
event OperatorRegistered(address indexed operator, OperatorDetails operatorDetails);

Expand Down Expand Up @@ -237,6 +252,18 @@ interface IDelegationManager is ISignatureUtils {
external
returns (bytes32[] memory);

/**
* Allows a third party to withdraw shares on behalf of a staker with their signature.
* 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 fully withdrawn after a delay.
*/
function queueWithdrawalsWithSignature(
QueuedWithdrawalWithSignatureParams[] calldata queuedWithdrawalWithSigParams
) external returns (bytes32[] memory);

/**
* @notice Used to complete the specified `withdrawal`. The caller must match `withdrawal.withdrawer`
* @param withdrawal The Withdrawal to complete.
Expand Down Expand Up @@ -421,6 +448,14 @@ interface IDelegationManager is ISignatureUtils {
uint256 expiry
) external view returns (bytes32);

function calculateQueueWithdrawalDigestHash(
address staker,
IStrategy[] memory strategies,
uint256[] memory shares,
uint256 stakerNonce,
uint256 expiry
) external view returns (bytes32);

/// @notice The EIP-712 typehash for the contract's domain
function DOMAIN_TYPEHASH() external view returns (bytes32);

Expand Down
18 changes: 15 additions & 3 deletions src/test/mocks/DelegationManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract DelegationManagerMock is IDelegationManager, Test {
mapping (address => address) public delegatedTo;

function registerAsOperator(OperatorDetails calldata /*registeringOperatorDetails*/, string calldata /*metadataURI*/) external pure {}

function updateOperatorMetadataURI(string calldata /*metadataURI*/) external pure {}

function updateAVSMetadataURI(string calldata /*metadataURI*/) external pure {}
Expand Down Expand Up @@ -85,7 +85,7 @@ contract DelegationManagerMock is IDelegationManager, Test {
function strategyWithdrawalDelayBlocks(IStrategy /*strategy*/) external pure returns (uint256) {
return 0;
}

function getOperatorShares(
address operator,
IStrategy[] memory strategies
Expand Down Expand Up @@ -119,6 +119,14 @@ contract DelegationManagerMock is IDelegationManager, Test {
uint256 /*expiry*/
) external view returns (bytes32) {}

function calculateQueueWithdrawalDigestHash(
address /*staker*/,
IStrategy[] memory /*strategies*/,
uint256[] memory /*shares*/,
uint256 /*stakerNonce*/,
uint256 /*expiry*/
) external view returns (bytes32) {}

function calculateStakerDigestHash(address /*staker*/, address /*operator*/, uint256 /*expiry*/)
external pure returns (bytes32 stakerDigestHash) {}

Expand Down Expand Up @@ -152,6 +160,10 @@ contract DelegationManagerMock is IDelegationManager, Test {
QueuedWithdrawalParams[] calldata queuedWithdrawalParams
) external returns (bytes32[] memory) {}

function queueWithdrawalsWithSignature(
QueuedWithdrawalWithSignatureParams[] calldata queuedWithdrawalWithSigParams
) external returns (bytes32[] memory) {}

function completeQueuedWithdrawal(
Withdrawal calldata withdrawal,
IERC20[] calldata tokens,
Expand All @@ -165,7 +177,7 @@ contract DelegationManagerMock is IDelegationManager, Test {
uint256[] calldata middlewareTimesIndexes,
bool[] calldata receiveAsTokens
) external {}

// onlyDelegationManager functions in StrategyManager
function addShares(
IStrategyManager strategyManager,
Expand Down
Loading