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: triggerable withdrawals vebo part (val-1456) #927

Draft
wants to merge 19 commits into
base: feat/withdrawal-credentials
Choose a base branch
from

Conversation

Amuhar
Copy link
Contributor

@Amuhar Amuhar commented Jan 21, 2025

A short summary of the changes.

Context

What the reviewer needs to know

Problem

What problem this PR solves, link relevant issue if it exists

Solution

Your proposed solution

@tamtamchik tamtamchik changed the title Feat/val 1456 tw vebo part feat: triggerable withdrawals vebo part (val-1456) Jan 28, 2025
@tamtamchik tamtamchik added solidity issues/tasks related to smart contract code valset Updates from the ValSet Tech team labels Jan 28, 2025
@Amuhar Amuhar changed the base branch from feat/val-1456-triggerable-withdrawals-vebo to feat/val-1556-move-vebo-legacy-tests-WC-lib February 10, 2025 08:59
@Amuhar Amuhar changed the base branch from feat/val-1556-move-vebo-legacy-tests-WC-lib to feat/withdrawal-credentials February 17, 2025 08:38
@Amuhar Amuhar changed the base branch from feat/withdrawal-credentials to develop February 17, 2025 09:25
@Amuhar Amuhar changed the base branch from develop to feat/withdrawal-credentials February 17, 2025 09:26
Copy link

github-actions bot commented Feb 20, 2025

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/ValidatorsExitBus.sol                      41       3  92.68%   103, 131, 148
contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol                99       2  97.98%   148, 325
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                                                           3084      32  98.96%

Diff against master

Filename                                              Stmts    Miss  Cover
--------------------------------------------------  -------  ------  --------
contracts/0.8.9/WithdrawalVault.sol                     +19       0  +100.00%
contracts/0.8.9/oracle/ValidatorsExitBus.sol            +41      +3  +92.68%
contracts/0.8.9/oracle/ValidatorsExitBusOracle.sol       +8     -89  +97.98%
TOTAL                                                   +68     -86  +2.87%

Results for commit: 3e732bf

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

if (keyIndexes[i] > lastDeliveredKeyIndex) {
revert KeyWasNotUnpacked(keyIndexes[i], lastDeliveredKeyIndex);
}
uint256 requestOffset = keyIndexes[i] * PACKED_REQUEST_LENGTH + 16;
Copy link
Contributor

@mkurayan mkurayan Feb 20, 2025

Choose a reason for hiding this comment

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

A minor suggestion to check requestOffset is not out of range.

        if (requestOffset + PUBLIC_KEY_LENGTH > data.length) {
            revert OutOfRangeDataAccess();
        }

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

@F4ever F4ever Feb 22, 2025

Choose a reason for hiding this comment

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

Let's define what is 16.

MSB <---------------------------------------------------- LSB
| 24 bits: moduleId | 40 bits: nodeOpId | 64 bits: valIndex |

Or even define and replace it with constants.
Like keyIndexes[i] * PACKED_REQUEST_LENGTH + PACKED_MODULE_ID_LENGHT + PACKED_NODE_OP_ID_LENGTH + PACKED_VALIDATOR_INDEX_SIZE

// TODO: timestamp
}
// TODO: make type optimization
struct RequestStatus {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we optimize storage?

Current:

    mapping(bytes32 => RequestStatus) requests;

    struct DeliveryHistory {
      uint256 blockNumber;
      uint256 lastDeliveredKeyIndex;
    }

    struct RequestStatus {
      uint256 totalItemsCount;
      uint256 deliveredItemsCount;
      uint256 contractVersion;
      DeliveryHistory[] deliverHistory;
    } 

Suggested:

    mapping(bytes32 => RequestStatusPacked) requestsBasic; 
    mapping(bytes32 => DeliveryHistoryPacked[]) deliveryHistories;

    struct RequestStatusPacked {
        uint16 contractVersion;
        uint16 totalItemsCount;
        uint16 deliveredItemsCount;

        uint16 deliverHistoryLength;
        
        uint32 latestDeliveredBlockNumber;
        uint32 latestDeliveredTimestamp;
    }

    struct DeliveryHistoryPacked {
      uint16 lastDeliveredKeyIndex;
      uint32 blockNumber;
      uint32 blockTimestamp;
    }

I suspect that our reports will typically be processed in a single pass, so storing an array of DeliveryHistory with one entry per report in storage seems excessive. Instead, we store deliverHistoryLength and information about the last report processing (which is usually the only one) in RequestStatusPacked. This way, in 99% of cases, we don't need to keep an additional history array in storage.

Copy link
Member

@F4ever F4ever Feb 22, 2025

Choose a reason for hiding this comment

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

Contract version should be uint256. It's defined in contract Versioned.
Also I'd prefer do such optimization in last iteration. I don't have strong expiriens in solidity, but seems it would make logic more complex

Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO it will not overcomplicate implementation but save a lot of gas.

@mkurayan
Copy link
Contributor

I found it a bit inconvenient to jump between ValidatorsExitBus.sol and ValidatorsExitBusOracle.sol. For example, the description of the report data format (DATA_FORMAT_LIST) is outlined in ValidatorsExitBusOracle.sol, but when data in DATA_FORMAT_LIST is processed in ValidatorsExitBus.sol, you have to switch between files to understand the context.

IMHO, the logic in these files seems tightly coupled, and separating it into two files does not simplify things.

I suggest combining ValidatorsExitBus.sol and ValidatorsExitBusOracle.sol into a single file.

require(success, "Refund failed");
revert TriggerableWithdrawalRefundFailed();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Small suggestion.

    1. Save the initial balance before processing withdrawal requests.
    uint256 prevBalance = address(this).balance - msg.value;

    2. After finishing processing and making a refund, check invariant
    assert(address(this).balance == prevBalance);

@@ -109,6 +111,10 @@ contract ValidatorsExitBusOracle is BaseOracle, PausableUntil {
_initialize(consensusContract, consensusVersion, lastProcessingRefSlot);
}

function finalizeUpgrade_v2() external {
Copy link
Member

Choose a reason for hiding this comment

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

This method should be part of VEB


bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH);

for (uint256 i = 0; i < keyIndexes.length; i++) {
Copy link
Member

Choose a reason for hiding this comment

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

I propose to add here check that
require(keyIndexes[i] < requestStatus.totalItemsCount)

Comment on lines +76 to +138
function triggerExits(ExitRequestData calldata request, uint256[] calldata keyIndexes) external payable {
uint256 prevBalance = address(this).balance - msg.value;
RequestStatus storage requestStatus = _storageExitRequestsHashes()[keccak256(abi.encode(request.data, request.dataFormat))];
bytes calldata data = request.data;

if (requestStatus.contractVersion == 0) {
revert ExitHashWasNotSubmitted();
}

address locatorAddr = LOCATOR_CONTRACT_POSITION.getStorageAddress();
address withdrawalVaultAddr = ILidoLocator(locatorAddr).withdrawalVault();
uint256 withdrawalFee = IWithdrawalVault(withdrawalVaultAddr).getWithdrawalRequestFee();

if (msg.value < keyIndexes.length * withdrawalFee ) {
revert InsufficientPayment(withdrawalFee, keyIndexes.length, msg.value);
}

uint256 lastDeliveredKeyIndex = requestStatus.deliveredItemsCount - 1;

bytes memory pubkeys = new bytes(keyIndexes.length * PUBLIC_KEY_LENGTH);

for (uint256 i = 0; i < keyIndexes.length; i++) {
if (keyIndexes[i] >= requestStatus.totalItemsCount) {
revert KeyIndexOutOfRange(keyIndexes[i], requestStatus.totalItemsCount);
}

if (keyIndexes[i] > lastDeliveredKeyIndex) {
revert KeyWasNotDelivered(keyIndexes[i], lastDeliveredKeyIndex);
}

///
/// | 3 bytes | 5 bytes | 8 bytes | 48 bytes |
/// | moduleId | nodeOpId | validatorIndex | validatorPubkey |
/// 16 bytes - part without pubkey
uint256 requestPublicKeyOffset = keyIndexes[i] * PACKED_REQUEST_LENGTH + 16;
uint256 destOffset = i * PUBLIC_KEY_LENGTH;

assembly {
let dest := add(pubkeys, add(32, destOffset))
calldatacopy(
dest,
add(data.offset, requestPublicKeyOffset),
PUBLIC_KEY_LENGTH
)
}
}

IWithdrawalVault(withdrawalVaultAddr).addFullWithdrawalRequests{value: keyIndexes.length * withdrawalFee}(pubkeys);

uint256 refund = msg.value - keyIndexes.length * withdrawalFee;

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

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

emit MadeRefund(msg.sender, refund);
}

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

Check warning

Code scanning / Slither

Dangerous strict equalities Medium

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
solidity issues/tasks related to smart contract code valset Updates from the ValSet Tech team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants