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 12 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
49 changes: 39 additions & 10 deletions contracts/0.8.9/WithdrawalVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "@openzeppelin/contracts-v4.4/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts-v4.4/token/ERC20/utils/SafeERC20.sol";

import {Versioned} from "./utils/Versioned.sol";
import {WithdrawalRequests} from "./lib/WithdrawalRequests.sol";

interface ILido {
/**
Expand All @@ -27,6 +28,7 @@ contract WithdrawalVault is Versioned {

ILido public immutable LIDO;
address public immutable TREASURY;
address public immutable VALIDATORS_EXIT_BUS;
mkurayan marked this conversation as resolved.
Show resolved Hide resolved

// Events
/**
Expand All @@ -42,26 +44,24 @@ contract WithdrawalVault is Versioned {
event ERC721Recovered(address indexed requestedBy, address indexed token, uint256 tokenId);

// Errors
error LidoZeroAddress();
error TreasuryZeroAddress();
error ZeroAddress();
error NotLido();
error NotValidatorExitBus();
error NotEnoughEther(uint256 requested, uint256 balance);
error ZeroAmount();

/**
* @param _lido the Lido token (stETH) address
* @param _treasury the Lido treasury address (see ERC20/ERC721-recovery interfaces)
*/
constructor(ILido _lido, address _treasury) {
if (address(_lido) == address(0)) {
revert LidoZeroAddress();
}
if (_treasury == address(0)) {
revert TreasuryZeroAddress();
}
constructor(address _lido, address _treasury, address _validatorsExitBus) {
_assertNonZero(_lido);
_assertNonZero(_treasury);
_assertNonZero(_validatorsExitBus);

LIDO = _lido;
LIDO = ILido(_lido);
TREASURY = _treasury;
VALIDATORS_EXIT_BUS = _validatorsExitBus;
}

/**
Expand All @@ -70,6 +70,12 @@ contract WithdrawalVault is Versioned {
*/
function initialize() external {
_initializeContractVersionTo(1);
_updateContractVersion(2);
}

function finalizeUpgrade_v2() external {
_checkContractVersion(1);
_updateContractVersion(2);
}

/**
Expand Down Expand Up @@ -122,4 +128,27 @@ contract WithdrawalVault is Versioned {

_token.transferFrom(address(this), TREASURY, _tokenId);
}

/**
* @dev Adds full withdrawal requests for the provided public keys.
* The validator will fully withdraw and exit its duties as a validator.
* @param pubkeys An array of public keys for the validators requesting full withdrawals.
*/
function addFullWithdrawalRequests(
bytes[] calldata pubkeys
) external payable {
if(msg.sender != address(VALIDATORS_EXIT_BUS)) {
revert NotValidatorExitBus();
}

WithdrawalRequests.addFullWithdrawalRequests(pubkeys);
}

function getWithdrawalRequestFee() external view returns (uint256) {
return WithdrawalRequests.getWithdrawalRequestFee();
}

function _assertNonZero(address _address) internal pure {
if (_address == address(0)) revert ZeroAddress();
}
}
122 changes: 122 additions & 0 deletions contracts/0.8.9/lib/WithdrawalRequests.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2023 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.9;

library WithdrawalRequests {
address constant WITHDRAWAL_REQUEST = 0x0c15F14308530b7CDB8460094BbB9cC28b9AaaAA;

error MismatchedArrayLengths(uint256 keysCount, uint256 amountsCount);
error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue);

error WithdrawalRequestFeeReadFailed();
error InvalidPubkeyLength(bytes pubkey);
error WithdrawalRequestAdditionFailed(bytes pubkey, uint256 amount);
error NoWithdrawalRequests();
error PartialWithdrawalRequired(bytes pubkey);

event WithdrawalRequestAdded(bytes pubkey, uint256 amount);

/**
* @dev Adds full withdrawal requests for the provided public keys.
* The validator will fully withdraw and exit its duties as a validator.
* @param pubkeys An array of public keys for the validators requesting full withdrawals.
*/
function addFullWithdrawalRequests(
bytes[] calldata pubkeys
) internal {
uint256 keysCount = pubkeys.length;
uint64[] memory amounts = new uint64[](keysCount);

_addWithdrawalRequests(pubkeys, amounts);
}

/**
* @dev Adds partial withdrawal requests for the provided public keys with corresponding amounts.
* A partial withdrawal is any withdrawal where the amount is greater than zero.
* This allows withdrawal of any balance exceeding 32 ETH (e.g., if a validator has 35 ETH, up to 3 ETH can be withdrawn).
* However, the protocol enforces a minimum balance of 32 ETH per validator, even if a higher amount is requested.
* @param pubkeys An array of public keys for the validators requesting withdrawals.
* @param amounts An array of corresponding withdrawal amounts for each public key.
*/
function addPartialWithdrawalRequests(
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
bytes[] calldata pubkeys,
uint64[] calldata amounts
) internal {
uint256 keysCount = pubkeys.length;
if (keysCount != amounts.length) {
revert MismatchedArrayLengths(keysCount, amounts.length);
}

uint64[] memory _amounts = new uint64[](keysCount);
for (uint256 i = 0; i < keysCount; i++) {
if (amounts[i] == 0) {
revert PartialWithdrawalRequired(pubkeys[i]);
}

_amounts[i] = amounts[i];
}

_addWithdrawalRequests(pubkeys, _amounts);
}

/**
* @dev Retrieves the current withdrawal request fee.
* @return The minimum fee required per withdrawal request.
*/
function getWithdrawalRequestFee() internal view returns (uint256) {
(bool success, bytes memory feeData) = WITHDRAWAL_REQUEST.staticcall("");

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

return abi.decode(feeData, (uint256));
}

function _addWithdrawalRequests(
bytes[] calldata pubkeys,
uint64[] memory amounts
) internal {
uint256 keysCount = pubkeys.length;
if (keysCount == 0) {
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).

if (minFeePerRequest * keysCount > msg.value) {
revert FeeNotEnough(minFeePerRequest, keysCount, msg.value);
}

uint256 feePerRequest = msg.value / keysCount;
uint256 unallocatedFee = msg.value % keysCount;
uint256 prevBalance = address(this).balance - msg.value;


for (uint256 i = 0; i < keysCount; ++i) {
bytes memory pubkey = pubkeys[i];
uint64 amount = amounts[i];

if(pubkey.length != 48) {
revert InvalidPubkeyLength(pubkey);
}

uint256 feeToSend = feePerRequest;

if (i == keysCount - 1) {
feeToSend += unallocatedFee;
}

bytes memory callData = abi.encodePacked(pubkey, amount);
mkurayan marked this conversation as resolved.
Show resolved Hide resolved
(bool success, ) = WITHDRAWAL_REQUEST.call{value: feeToSend}(callData);

if (!success) {
revert WithdrawalRequestAdditionFailed(pubkey, amount);
}

emit WithdrawalRequestAdded(pubkey, amount);
}

assert(address(this).balance == prevBalance);
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
}
Fixed Show fixed Hide fixed
}
22 changes: 22 additions & 0 deletions test/0.8.9/contracts/WithdrawalCredentials_Harness.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
pragma solidity 0.8.9;

import {WithdrawalRequests} from "contracts/0.8.9/lib/WithdrawalRequests.sol";

contract WithdrawalCredentials_Harness {
function addFullWithdrawalRequests(
bytes[] calldata pubkeys
) external payable {
WithdrawalRequests.addFullWithdrawalRequests(pubkeys);
}

function addPartialWithdrawalRequests(
bytes[] calldata pubkeys,
uint64[] calldata amounts
) external payable {
WithdrawalRequests.addPartialWithdrawalRequests(pubkeys, amounts);
}

function getWithdrawalRequestFee() external view returns (uint256) {
return WithdrawalRequests.getWithdrawalRequestFee();
}
}
37 changes: 37 additions & 0 deletions test/0.8.9/contracts/predeployed/WithdrawalsPredeployed_Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.9;

/**
* @notice This is an mock of EIP-7002's pre-deploy contract.
*/
contract WithdrawalsPredeployed_Mock {
uint256 public fee;
bool public failOnAddRequest;
bool public failOnGetFee;

function setFailOnAddRequest(bool _failOnAddRequest) external {
failOnAddRequest = _failOnAddRequest;
}

function setFailOnGetFee(bool _failOnGetFee) external {
failOnGetFee = _failOnGetFee;
}

function setFee(uint256 _fee) external {
require(_fee > 0, "fee must be greater than 0");
fee = _fee;
}

fallback(bytes calldata input) external payable returns (bytes memory output){
if (input.length == 0) {
require(!failOnGetFee, "fail on get fee");

output = abi.encode(fee);
return output;
}

require(!failOnAddRequest, "fail on add request");

require(input.length == 56, "Invalid callData length");
}
}
43 changes: 43 additions & 0 deletions test/0.8.9/lib/withdrawalCredentials/withdrawalCredentials.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ethers } from "hardhat";

import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";

import { WithdrawalCredentials_Harness, WithdrawalsPredeployed_Mock } from "typechain-types";

import { Snapshot } from "test/suite";

import {
deployWithdrawalsPredeployedMock,
testFullWithdrawalRequestBehavior,
testPartialWithdrawalRequestBehavior,
} from "./withdrawalRequests.behavior";

describe("WithdrawalCredentials.sol", () => {
let actor: HardhatEthersSigner;

let withdrawalsPredeployed: WithdrawalsPredeployed_Mock;
let withdrawalCredentials: WithdrawalCredentials_Harness;

let originalState: string;

before(async () => {
[actor] = await ethers.getSigners();

withdrawalsPredeployed = await deployWithdrawalsPredeployedMock();
withdrawalCredentials = await ethers.deployContract("WithdrawalCredentials_Harness");
});

beforeEach(async () => (originalState = await Snapshot.take()));

afterEach(async () => await Snapshot.restore(originalState));

testFullWithdrawalRequestBehavior(
() => withdrawalCredentials.connect(actor),
() => withdrawalsPredeployed.connect(actor),
);

testPartialWithdrawalRequestBehavior(
() => withdrawalCredentials.connect(actor),
() => withdrawalsPredeployed.connect(actor),
);
});
Loading
Loading