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

feat: withdrawal credentials #904

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open

Conversation

mkurayan
Copy link
Contributor

@mkurayan mkurayan commented Dec 20, 2024

Summary

This PR introduces an early-stage prototype for a potential implementation of EIP-7685: General Purpose Execution Layer Requests. Specifically, it implements EIP-7002: Execution Layer Triggerable Withdrawals within the Lido WithdrawalVault contract, which serves as the withdrawal credentials address for Lido validators.

Approach

The implementation follows the first approach outlined in the ADR for Withdrawal Credentials Contract. This approach was selected due to the following advantages:

  • Clear and specific interfaces: Each request type has a dedicated, well-defined interface.
  • Custom logic and parameter validation: Request methods can validate parameters and incorporate tailored logic as needed.
  • Granularity and modularity: WithdrawalCredentials contracts, such as the existing Lido WithdrawalVault and future Vaults, can expose only the necessary subset of request handlers. This allows for unique permission models and specialized logic.

Implementation Details

In this iteration, the WithdrawalVault contract supports only full withdrawal requests. Partial withdrawals are not included because they are not part of the proposed Triggerable Withdrawal V1 implementation within the Lido protocol.

The following pseudo-code demonstrates the approach:

library TriggerableWithdrawals {
    // Address of the EIP-7002 pre-deployed contract.
    address constant WITHDRAWAL_REQUEST = 0x0c15...;

    function addFullWithdrawalRequests(
        bytes[] calldata pubkeys,
        uint256 totalWithdrawalFee
    ) internal {
        // Implementation omitted for brevity.
        // WITHDRAWAL_REQUEST.call{value: fee}(withdrawalRequests);
    }
    
    function addPartialWithdrawalRequests(
        bytes[] calldata pubkeys,
        uint64[] calldata amounts,
        uint256 totalWithdrawalFee
    ) internal {
        // Implementation omitted for brevity.
        // WITHDRAWAL_REQUEST.call{value: fee}(withdrawalRequests);
    }
}

contract WithdrawalVault {
    function addFullWithdrawalRequests(
        bytes[] calldata pubkeys
    ) external payable {
        if (msg.sender != address(VALIDATORS_EXIT_BUS)) {
            revert NotValidatorExitBus();
        }

        TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, msg.value);
    }
}

Key Considerations

The modular design of separate libraries for different withdrawal request types ensures that withdrawal credentials contracts can import and use only the minimal functionality required. For example:

  • Contract A: Supports only full withdrawal requests.
  • Contract B: Supports full withdrawals, partial withdrawals, and consolidation requests.

Notes

This PR is an initial prototype and is subject to further iterations based on feedback and evolving requirements.

@mkurayan mkurayan requested a review from a team as a code owner December 20, 2024 09:26
Copy link

github-actions bot commented Dec 20, 2024

badge

Hardhat Unit Tests Coverage Summary

Filename                                                       Stmts    Miss  Cover    Missing
-----------------------------------------------------------  -------  ------  -------  ---------
contracts/0.4.24/Lido.sol                                        212       0  100.00%
contracts/0.4.24/StETH.sol                                        72       0  100.00%
contracts/0.4.24/StETHPermit.sol                                  15       0  100.00%
contracts/0.4.24/lib/Packed64x4.sol                                5       0  100.00%
contracts/0.4.24/lib/SigningKeys.sol                              36       0  100.00%
contracts/0.4.24/lib/StakeLimitUtils.sol                          37       0  100.00%
contracts/0.4.24/nos/NodeOperatorsRegistry.sol                   512       0  100.00%
contracts/0.4.24/oracle/LegacyOracle.sol                          72       0  100.00%
contracts/0.4.24/utils/Pausable.sol                                9       0  100.00%
contracts/0.4.24/utils/Versioned.sol                               5       0  100.00%
contracts/0.6.12/WstETH.sol                                       17       0  100.00%
contracts/0.8.4/WithdrawalsManagerProxy.sol                       61       0  100.00%
contracts/0.8.9/BeaconChainDepositor.sol                          21       2  90.48%   48, 51
contracts/0.8.9/Burner.sol                                        71       0  100.00%
contracts/0.8.9/DepositSecurityModule.sol                        128       0  100.00%
contracts/0.8.9/EIP712StETH.sol                                   16       0  100.00%
contracts/0.8.9/LidoExecutionLayerRewardsVault.sol                16       0  100.00%
contracts/0.8.9/LidoLocator.sol                                   18       0  100.00%
contracts/0.8.9/OracleDaemonConfig.sol                            28       0  100.00%
contracts/0.8.9/StakingRouter.sol                                316       0  100.00%
contracts/0.8.9/WithdrawalQueue.sol                               88       0  100.00%
contracts/0.8.9/WithdrawalQueueBase.sol                          146       0  100.00%
contracts/0.8.9/WithdrawalQueueERC721.sol                         89       0  100.00%
contracts/0.8.9/WithdrawalVault.sol                               40       0  100.00%
contracts/0.8.9/lib/Math.sol                                       4       0  100.00%
contracts/0.8.9/lib/PositiveTokenRebaseLimiter.sol                22       0  100.00%
contracts/0.8.9/lib/UnstructuredRefStorage.sol                     2       0  100.00%
contracts/0.8.9/oracle/AccountingOracle.sol                      190       2  98.95%   189-190
contracts/0.8.9/oracle/BaseOracle.sol                             89       1  98.88%   397
contracts/0.8.9/oracle/HashConsensus.sol                         263       1  99.62%   1005
contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol                91      91  0.00%    96-461
contracts/0.8.9/proxy/OssifiableProxy.sol                         17       0  100.00%
contracts/0.8.9/sanity_checks/OracleReportSanityChecker.sol      232       0  100.00%
contracts/0.8.9/utils/DummyEmptyContract.sol                       0       0  100.00%
contracts/0.8.9/utils/PausableUntil.sol                           31       0  100.00%
contracts/0.8.9/utils/Versioned.sol                               11       0  100.00%
contracts/0.8.9/utils/access/AccessControl.sol                    23       0  100.00%
contracts/0.8.9/utils/access/AccessControlEnumerable.sol           9       0  100.00%
contracts/testnets/sepolia/SepoliaDepositAdapter.sol              21      21  0.00%    49-100
TOTAL                                                           3035     118  96.11%

Diff against master

Filename                               Stmts    Miss  Cover
-----------------------------------  -------  ------  --------
contracts/0.8.9/WithdrawalVault.sol      +19       0  +100.00%
TOTAL                                    +19       0  +0.02%

Results for commit: 6cdc711

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

contracts/0.8.9/lib/WithdrawalRequests.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/WithdrawalRequests.sol Outdated Show resolved Hide resolved
contracts/0.8.9/WithdrawalVault.sol Outdated Show resolved Hide resolved
revert NoWithdrawalRequests();
}

uint256 minFeePerRequest = getWithdrawalRequestFee();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't fee increase with each request?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the fee will not increase with each request. Inside the transaction, all requests will have the same fee.

EIP 7002 uses block-by-block behavior.

If block N processes X requests, then at the end of block N the number of withdrawal requests that the chain has processed relative to the “targeted” number increases by X - TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK, and so the fee in block N+1 increases by a factor of e**((X - TARGET_WITHDRAWAL_REQUESTS_PER_BLOCK) / WITHDRAWAL_REQUEST_FEE_UPDATE_FRACTION).

contracts/0.8.9/lib/WithdrawalRequests.sol Outdated Show resolved Hide resolved
Copy link
Member

@folkyatina folkyatina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still see some potential for improvement

contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
Add role ADD_FULL_WITHDRAWAL_REQUEST_ROLE for full withdrawal requests.
Access pubkeys and amounts directly instead of copying them to memory.
pass pubkeys as array of bytes
Comment on lines 146 to 174
function addFullWithdrawalRequests(
bytes calldata pubkeys
) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) {
uint256 prevBalance = address(this).balance - msg.value;

uint256 minFeePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee();
uint256 totalFee = pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH * minFeePerRequest;

if(totalFee > msg.value) {
revert InsufficientTriggerableWithdrawalFee(
msg.value,
totalFee,
pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH
);
}

TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, minFeePerRequest);

uint256 refund = msg.value - totalFee;
if (refund > 0) {
(bool success, ) = msg.sender.call{value: refund}("");

if (!success) {
revert TriggerableWithdrawalRefundFailed();
}
}

assert(address(this).balance == prevBalance);
}

Check warning

Code scanning / Slither

Divide before multiply Medium

Comment on lines 146 to 174
function addFullWithdrawalRequests(
bytes calldata pubkeys
) external payable onlyRole(ADD_FULL_WITHDRAWAL_REQUEST_ROLE) {
uint256 prevBalance = address(this).balance - msg.value;

uint256 minFeePerRequest = TriggerableWithdrawals.getWithdrawalRequestFee();
uint256 totalFee = pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH * minFeePerRequest;

if(totalFee > msg.value) {
revert InsufficientTriggerableWithdrawalFee(
msg.value,
totalFee,
pubkeys.length / TriggerableWithdrawals.PUBLIC_KEY_LENGTH
);
}

TriggerableWithdrawals.addFullWithdrawalRequests(pubkeys, minFeePerRequest);

uint256 refund = msg.value - totalFee;
if (refund > 0) {
(bool success, ) = msg.sender.call{value: refund}("");

if (!success) {
revert TriggerableWithdrawalRefundFailed();
}
}

assert(address(this).balance == prevBalance);
}

Check warning

Code scanning / Slither

Dangerous strict equalities Medium

Copy link
Member

@tamtamchik tamtamchik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Minor comments about library placement, constant WITHDRAWAL_REQUEST (may be immutable), and some style fixes suggestions.

contracts/0.8.9/WithdrawalVault.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/0.8.9/WithdrawalVault.sol Outdated Show resolved Hide resolved
contracts/0.8.9/WithdrawalVault.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/0.8.9/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/0.8.9/WithdrawalVault.sol Outdated Show resolved Hide resolved
@tamtamchik tamtamchik changed the title Feat/withdrawal credentials feat: withdrawal credentials Jan 28, 2025
@tamtamchik tamtamchik added solidity issues/tasks related to smart contract code tests labels Jan 28, 2025
@tamtamchik tamtamchik added next-upgrade Things to pickup for the next protocol upgrade valset Updates from the ValSet Tech team labels Jan 28, 2025
Copy link
Member

@tamtamchik tamtamchik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some suggestions to errors naming, to prevent clashing

contracts/common/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
contracts/common/lib/TriggerableWithdrawals.sol Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
next-upgrade Things to pickup for the next protocol upgrade solidity issues/tasks related to smart contract code tests valset Updates from the ValSet Tech team vaults
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants