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/val 1456 tw vebo part #927

Draft
wants to merge 3 commits into
base: feat/val-1456-triggerable-withdrawals-vebo
Choose a base branch
from
Draft
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
154 changes: 154 additions & 0 deletions contracts/0.8.9/oracle/ValidatorsExitBus.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-FileCopyrightText: 2023 Lido <info@lido.fi>
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.9;

import { AccessControlEnumerable } from "../utils/access/AccessControlEnumerable.sol";
import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol";
import { ILidoLocator } from "../../common/interfaces/ILidoLocator.sol";

interface IWithdrawalVault {
function addFullWithdrawalRequests(bytes[] calldata pubkeys) external payable;

function getWithdrawalRequestFee() external view returns (uint256);
}


contract ValidatorsExitBus is AccessControlEnumerable {
using UnstructuredStorage for bytes32;

/// @dev Errors
error KeyWasNotUnpacked(uint256 keyIndex, uint256 lastUnpackedKeyIndex);
error ZeroAddress();
error FeeNotEnough(uint256 minFeePerRequest, uint256 requestCount, uint256 msgValue);

/// Part of report data
struct ExitRequestData {
/// @dev Total number of validator exit requests in this report. Must not be greater
/// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport.
uint256 requestsCount;

/// @dev Format of the validator exit requests data. Currently, only the
/// DATA_FORMAT_LIST=1 is supported.
uint256 dataFormat;

/// @dev Validator exit requests data. Can differ based on the data format,
/// see the constant defining a specific data format below for more info.
bytes data;
}

// TODO: make type optimization
struct DeliveryHistory {
uint256 blockNumber;
/// @dev Key index in exit request array
uint256 lastDeliveredKeyIndex;

// TODO: timestamp
}
// TODO: make type optimization
struct RequestStatus {
// Total items count in report (by default type(uint32).max, update on first report unpack)
uint256 totalItemsCount;
// Total processed items in report (by default 0)
uint256 deliveredItemsCount;
// Vebo contract version at the time of hash submittion
uint256 contractVersion;
DeliveryHistory[] deliverHistory;
}

/// @notice The list format of the validator exit requests data. Used when all
/// requests fit into a single transaction.
///
/// Each validator exit request is described by the following 64-byte array:
///
/// MSB <------------------------------------------------------- LSB
/// | 3 bytes | 5 bytes | 8 bytes | 48 bytes |
/// | moduleId | nodeOpId | validatorIndex | validatorPubkey |
///
/// All requests are tightly packed into a byte array where requests follow
/// one another without any separator or padding, and passed to the `data`
/// field of the report structure.
///
/// Requests must be sorted in the ascending order by the following compound
/// key: (moduleId, nodeOpId, validatorIndex).
///
uint256 public constant DATA_FORMAT_LIST = 1;

/// Length in bytes of packed request
uint256 internal constant PACKED_REQUEST_LENGTH = 64;

/// Hash constant for mapping exit requests storage
bytes32 internal constant EXIT_REQUESTS_HASHES_POSITION =
keccak256("lido.ValidatorsExitBus.reportHashes");

bytes32 private constant LOCATOR_CONTRACT_POSITION = keccak256("lido.ValidatorsExitBus.locatorContract");

function _initialize_v2(address locatorAddr) internal {
_setLocatorAddress(locatorAddr);
}

function _setLocatorAddress(address addr) internal {
if (addr == address(0)) revert ZeroAddress();

LOCATOR_CONTRACT_POSITION.setStorageAddress(addr);
}

function triggerExitHashVerify(ExitRequestData calldata exitRequestData, uint256[] calldata keyIndexes) external payable {
bytes32 dataHash = keccak256(abi.encode(exitRequestData));
RequestStatus storage requestStatus = _storageExitRequestsHashes()[dataHash];

address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress();
address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault();
uint256 fee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee();
uint requestsFee = keyIndexes.length * fee;

if (msg.value < requestsFee) {
revert FeeNotEnough(fee, keyIndexes.length, msg.value);
}

uint256 refund = msg.value - requestsFee;

uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1;

uint256 offset;
bytes calldata data = exitRequestData.data;
bytes[] memory pubkeys = new bytes[](keyIndexes.length);

assembly {
offset := data.offset
}

for (uint256 i = 0; i < keyIndexes.length; i++) {
if (keyIndexes[i] > lastDeliveredKeyIndex) {
revert KeyWasNotUnpacked(keyIndexes[i], lastDeliveredKeyIndex);
}
uint256 requestOffset = offset + keyIndexes[i] * 64;

bytes calldata pubkey;

assembly {
pubkey.offset := add(requestOffset, 16)
pubkey.length := 48
}
pubkeys[i] = pubkey;

}

IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests(pubkeys);

if (refund > 0) {
(bool success, ) = msg.sender.call{value: refund}("");
require(success, "Refund failed");
}

}

/// Storage helpers
function _storageExitRequestsHashes() internal pure returns (
mapping(bytes32 => RequestStatus) storage r
) {
bytes32 position = EXIT_REQUESTS_HASHES_POSITION;
assembly {
r.slot := position
}
}
}
80 changes: 36 additions & 44 deletions contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { PausableUntil } from "../utils/PausableUntil.sol";
import { UnstructuredStorage } from "../lib/UnstructuredStorage.sol";

import { BaseOracle } from "./BaseOracle.sol";
import { ValidatorsExitBus } from "./ValidatorsExitBus.sol";


interface IOracleReportSanityChecker {
function checkExitBusOracleReport(uint256 _exitRequestsCount) external view;
}


contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
contract ValidatorsExitBusOracle is BaseOracle, PausableUntil, ValidatorsExitBus {
using UnstructuredStorage for bytes32;
using SafeCast for uint256;

Expand Down Expand Up @@ -109,6 +110,11 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
_initialize(consensusContract, consensusVersion, lastProcessingRefSlot);
}

function finalizeUpgrade_v2() external {
_updateContractVersion(2);
_initialize_v2(address(LOCATOR));
}

/// @notice Resume accepting validator exit requests
///
/// @dev Reverts with `PausedExpected()` if contract is already resumed
Expand Down Expand Up @@ -161,40 +167,9 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
/// Requests data
///

/// @dev Total number of validator exit requests in this report. Must not be greater
/// than limit checked in OracleReportSanityChecker.checkExitBusOracleReport.
uint256 requestsCount;

/// @dev Format of the validator exit requests data. Currently, only the
/// DATA_FORMAT_LIST=1 is supported.
uint256 dataFormat;

/// @dev Validator exit requests data. Can differ based on the data format,
/// see the constant defining a specific data format below for more info.
bytes data;
ExitRequestData exitRequestData;
}

/// @notice The list format of the validator exit requests data. Used when all
/// requests fit into a single transaction.
///
/// Each validator exit request is described by the following 64-byte array:
///
/// MSB <------------------------------------------------------- LSB
/// | 3 bytes | 5 bytes | 8 bytes | 48 bytes |
/// | moduleId | nodeOpId | validatorIndex | validatorPubkey |
///
/// All requests are tightly packed into a byte array where requests follow
/// one another without any separator or padding, and passed to the `data`
/// field of the report structure.
///
/// Requests must be sorted in the ascending order by the following compound
/// key: (moduleId, nodeOpId, validatorIndex).
///
uint256 public constant DATA_FORMAT_LIST = 1;

/// Length in bytes of packed request
uint256 internal constant PACKED_REQUEST_LENGTH = 64;

/// @notice Submits report data for processing.
///
/// @param data The data. See the `ReportData` structure's docs for details.
Expand All @@ -216,10 +191,12 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
{
_checkMsgSenderIsAllowedToSubmitData();
_checkContractVersion(contractVersion);
bytes32 exitRequestDataHash = keccak256(abi.encode(data.exitRequestData));
// it's a waste of gas to copy the whole calldata into mem but seems there's no way around
_checkConsensusData(data.refSlot, data.consensusVersion, keccak256(abi.encode(data)));
_checkConsensusData(data.refSlot, data.consensusVersion, keccak256(abi.encode(data.consensusVersion, data.refSlot, exitRequestDataHash)));
_startProcessing();
_handleConsensusReportData(data);
_storeOracleExitRequestHash(exitRequestDataHash, data, contractVersion);
}

/// @notice Returns the total number of validator exit requests ever processed
Expand Down Expand Up @@ -328,36 +305,37 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
}

function _handleConsensusReportData(ReportData calldata data) internal {
if (data.dataFormat != DATA_FORMAT_LIST) {
revert UnsupportedRequestsDataFormat(data.dataFormat);
if (data.exitRequestData.dataFormat != DATA_FORMAT_LIST) {
revert UnsupportedRequestsDataFormat(data.exitRequestData.dataFormat);
}

if (data.data.length % PACKED_REQUEST_LENGTH != 0) {
if (data.exitRequestData.data.length % PACKED_REQUEST_LENGTH != 0) {
revert InvalidRequestsDataLength();
}

// TODO: next iteration will check ref slot deliveredReportAmount
IOracleReportSanityChecker(LOCATOR.oracleReportSanityChecker())
.checkExitBusOracleReport(data.requestsCount);
.checkExitBusOracleReport(data.exitRequestData.requestsCount);

if (data.data.length / PACKED_REQUEST_LENGTH != data.requestsCount) {
if (data.exitRequestData.data.length / PACKED_REQUEST_LENGTH != data.exitRequestData.requestsCount) {
revert UnexpectedRequestsDataLength();
}

_processExitRequestsList(data.data);
_processExitRequestsList(data.exitRequestData.data);

_storageDataProcessingState().value = DataProcessingState({
refSlot: data.refSlot.toUint64(),
requestsCount: data.requestsCount.toUint64(),
requestsProcessed: data.requestsCount.toUint64(),
requestsCount: data.exitRequestData.requestsCount.toUint64(),
requestsProcessed: data.exitRequestData.requestsCount.toUint64(),
dataFormat: uint16(DATA_FORMAT_LIST)
});

if (data.requestsCount == 0) {
if (data.exitRequestData.requestsCount == 0) {
return;
}

TOTAL_REQUESTS_PROCESSED_POSITION.setStorageUint256(
TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.requestsCount
TOTAL_REQUESTS_PROCESSED_POSITION.getStorageUint256() + data.exitRequestData.requestsCount
);
}

Expand Down Expand Up @@ -439,6 +417,20 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
return (moduleId << 40) | nodeOpId;
}

function _storeOracleExitRequestHash(bytes32 exitRequestHash, ReportData calldata report, uint256 contractVersion) internal {
if (report.exitRequestData.requestsCount == 0) {
return;
}

mapping(bytes32 => RequestStatus) storage hashes = _storageExitRequestsHashes();

RequestStatus storage request = hashes[exitRequestHash];
request.totalItemsCount = report.exitRequestData.requestsCount;
request.deliveredItemsCount = report.exitRequestData.requestsCount;
request.contractVersion = contractVersion;
request.deliverHistory.push(DeliveryHistory({blockNumber: block.number, lastDeliveredKeyIndex: report.exitRequestData.requestsCount - 1}));
}

///
/// Storage helpers
///
Expand Down
14 changes: 14 additions & 0 deletions test/0.8.9/contracts/WithdrawalValut_MockForVebo.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
pragma solidity 0.8.9;

contract WithdrawalVault__MockForVebo {

event AddFullWithdrawalRequestsCalled(bytes[] pubkeys);

function addFullWithdrawalRequests(bytes[] calldata pubkeys) external {
emit AddFullWithdrawalRequestsCalled(pubkeys);
}

function getWithdrawalRequestFee() external view returns (uint256) {
return 1;
}
}
Loading
Loading