Skip to content

Commit e99ceeb

Browse files
committed
refactor: deposit logistics
1 parent 7b5d5e8 commit e99ceeb

File tree

4 files changed

+156
-62
lines changed

4 files changed

+156
-62
lines changed

contracts/0.8.25/lib/DepositLogistics.sol

+73-37
Original file line numberDiff line numberDiff line change
@@ -13,85 +13,112 @@ import { IDepositContract } from "../interfaces/IDepositContract.sol";
1313
* @dev Provides functionality to process multiple validator deposits to the Beacon Chain deposit contract
1414
*/
1515
library DepositLogistics {
16-
uint256 internal constant SIGNATURE_LENGTH = 96;
17-
uint256 internal constant PUBLIC_KEY_LENGTH = 48;
18-
uint256 internal constant SIZE_LENGTH = 8;
16+
/**
17+
* @notice Byte length of the BLS12-381 public key (a.k.a. validator public key)
18+
*/
19+
uint256 internal constant PUBKEY_LENGTH = 48;
20+
21+
/**
22+
* @notice Byte length of the BLS12-381 signature of the deposit message
23+
*/
24+
uint256 internal constant SIG_LENGTH = 96;
25+
26+
/**
27+
* @notice Byte length of the deposit amount (value) in gwei
28+
*/
29+
uint256 internal constant AMOUNT_LENGTH = 8;
30+
31+
/**
32+
* @notice Error thrown when the number of deposits is zero
33+
*/
34+
error ZeroDeposits();
1935

20-
error ZeroKeyCount();
36+
/**
37+
* @notice Error thrown when the length of the pubkeys array does not match the expected length
38+
*/
2139
error PubkeysLengthMismatch(uint256 actual, uint256 expected);
22-
error SignaturesLengthMismatch(uint256 actual, uint256 expected);
23-
error SizesLengthMismatch(uint256 actual, uint256 expected);
40+
41+
/**
42+
* @notice Error thrown when the length of the signatures array does not match the expected length
43+
*/
44+
error SigsLengthMismatch(uint256 actual, uint256 expected);
45+
46+
/**
47+
* @notice Error thrown when the length of the concatenated amounts does not match the expected length
48+
*/
49+
error AmountsLengthMismatch(uint256 actual, uint256 expected);
2450

2551
/**
2652
* @notice Processes multiple validator deposits to the Beacon Chain deposit contract
2753
* @param _depositContract The deposit contract interface
28-
* @param _keyCount Number of validator keys to process
54+
* @param _deposits Number of validator keys to process
2955
* @param _creds Withdrawal credentials for the validators
3056
* @param _pubkeys Concatenated validator public keys
3157
* @param _sigs Concatenated validator signatures
32-
* @param _sizes Array of deposit sizes in gwei
33-
* @dev Each validator requires a 48-byte public key, 96-byte signature, and 8-byte deposit size
58+
* @param _amounts Concatenated deposit amounts in gwei in byte format
3459
*/
3560
function processDeposits(
3661
IDepositContract _depositContract,
37-
uint256 _keyCount,
62+
uint256 _deposits,
3863
bytes memory _creds,
3964
bytes memory _pubkeys,
4065
bytes memory _sigs,
41-
bytes memory _sizes
66+
bytes memory _amounts
4267
) internal {
43-
if (_keyCount == 0) revert ZeroKeyCount();
44-
if (_pubkeys.length != PUBLIC_KEY_LENGTH * _keyCount) revert PubkeysLengthMismatch(_pubkeys.length, PUBLIC_KEY_LENGTH * _keyCount);
45-
if (_sigs.length != SIGNATURE_LENGTH * _keyCount) revert SignaturesLengthMismatch(_sigs.length, SIGNATURE_LENGTH * _keyCount);
46-
if (_sizes.length != SIZE_LENGTH * _keyCount) revert SizesLengthMismatch(_sizes.length, SIZE_LENGTH * _keyCount);
47-
48-
bytes memory pubkey = Memory.alloc(PUBLIC_KEY_LENGTH);
49-
bytes memory signature = Memory.alloc(SIGNATURE_LENGTH);
50-
bytes memory size = Memory.alloc(SIZE_LENGTH);
51-
52-
for (uint256 i; i < _keyCount; i++) {
53-
Memory.copy(_pubkeys, pubkey, i * PUBLIC_KEY_LENGTH, 0, PUBLIC_KEY_LENGTH);
54-
Memory.copy(_sigs, signature, i * SIGNATURE_LENGTH, 0, SIGNATURE_LENGTH);
55-
Memory.copy(_sizes, size, i * SIZE_LENGTH, 0, SIZE_LENGTH);
56-
57-
uint256 sizeInWei = uint256(uint64(bytes8(size))) * 1 gwei;
58-
bytes32 root = _computeRoot(_creds, pubkey, signature, size);
59-
60-
_depositContract.deposit{value: sizeInWei}(pubkey, _creds, signature, root);
68+
if (_deposits == 0) revert ZeroDeposits();
69+
if (_pubkeys.length != PUBKEY_LENGTH * _deposits) revert PubkeysLengthMismatch(_pubkeys.length, PUBKEY_LENGTH * _deposits);
70+
if (_sigs.length != SIG_LENGTH * _deposits) revert SigsLengthMismatch(_sigs.length, SIG_LENGTH * _deposits);
71+
if (_amounts.length != AMOUNT_LENGTH * _deposits) revert AmountsLengthMismatch(_amounts.length, AMOUNT_LENGTH * _deposits);
72+
73+
// Allocate memory for pubkey, sig, and amount to be reused for each deposit
74+
bytes memory pubkey = Memory.alloc(PUBKEY_LENGTH);
75+
bytes memory sig = Memory.alloc(SIG_LENGTH);
76+
bytes memory amount = Memory.alloc(AMOUNT_LENGTH);
77+
78+
for (uint256 i; i < _deposits; i++) {
79+
// Copy pubkey, sig, and amount to the allocated memory slots
80+
Memory.copy(_pubkeys, pubkey, i * PUBKEY_LENGTH, 0, PUBKEY_LENGTH);
81+
Memory.copy(_sigs, sig, i * SIG_LENGTH, 0, SIG_LENGTH);
82+
Memory.copy(_amounts, amount, i * AMOUNT_LENGTH, 0, AMOUNT_LENGTH);
83+
84+
uint256 amountInWei = _gweiBytesToWei(amount);
85+
bytes32 root = _computeRoot(_creds, pubkey, sig, amount);
86+
87+
_depositContract.deposit{value: amountInWei}(pubkey, _creds, sig, root);
6188
}
6289
}
6390

6491
/**
6592
* @notice Computes the deposit data root hash
6693
* @param _creds Withdrawal credentials
6794
* @param _pubkey BLS12-381 public key
68-
* @param _signature BLS12-381 signature
69-
* @param _size Deposit size in gwei
95+
* @param _sig BLS12-381 signature
96+
* @param _amount Deposit amount in gwei
7097
* @return bytes32 The computed deposit data root hash
7198
* @dev Implements the deposit data root calculation as specified in the Beacon Chain deposit contract
7299
*/
73100
function _computeRoot(
74101
bytes memory _creds,
75102
bytes memory _pubkey,
76-
bytes memory _signature,
77-
bytes memory _size
103+
bytes memory _sig,
104+
bytes memory _amount
78105
) internal pure returns (bytes32) {
79106
bytes32 pubkeyRoot = keccak256(abi.encodePacked(_pubkey, bytes16(0)));
80107

81108
bytes32 sigRoot = keccak256(
82109
abi.encodePacked(
83-
keccak256(abi.encodePacked(Memory.slice(_signature, 0, 64))),
84-
keccak256(abi.encodePacked(Memory.slice(_signature, 64, SIGNATURE_LENGTH - 64), bytes32(0)))
110+
keccak256(abi.encodePacked(Memory.slice(_sig, 0, 64))),
111+
keccak256(abi.encodePacked(Memory.slice(_sig, 64, SIG_LENGTH - 64), bytes32(0)))
85112
)
86113
);
87114

88-
bytes memory sizeInGweiLE64 = _toLittleEndian(_size);
115+
bytes memory amountInGweiLE64 = _toLittleEndian(_amount);
89116

90117
return
91118
keccak256(
92119
abi.encodePacked(
93120
keccak256(abi.encodePacked(pubkeyRoot, _creds)),
94-
keccak256(abi.encodePacked(sizeInGweiLE64, bytes24(0), sigRoot))
121+
keccak256(abi.encodePacked(amountInGweiLE64, bytes24(0), sigRoot))
95122
)
96123
);
97124
}
@@ -109,5 +136,14 @@ library DepositLogistics {
109136
result[i] = _value[_value.length - i - 1];
110137
}
111138
}
139+
140+
/**
141+
* @notice Converts a gwei value in bytes to a uint256 wei value
142+
* @param _value The gwei value in bytes
143+
* @return result The converted uint256 wei value
144+
*/
145+
function _gweiBytesToWei(bytes memory _value) internal pure returns (uint256) {
146+
return uint256(uint64(bytes8(_value))) * 1 gwei;
147+
}
112148
}
113149

contracts/0.8.25/vaults/StakingVault.sol

+6-8
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ import {IDepositContract} from "../interfaces/IDepositContract.sol";
5656
*
5757
*/
5858
contract StakingVault is IStakingVault, IBeaconProxy, OwnableUpgradeable {
59-
using DepositLogistics for IDepositContract;
60-
6159
/**
6260
* @notice ERC-7201 storage namespace for the vault
6361
* @dev ERC-7201 namespace is used to prevent upgrade collisions
@@ -323,28 +321,29 @@ contract StakingVault is IStakingVault, IBeaconProxy, OwnableUpgradeable {
323321
* @param _numberOfDeposits Number of deposits to make
324322
* @param _pubkeys Concatenated validator public keys
325323
* @param _signatures Concatenated deposit data signatures
324+
* @param _amounts Concatenated deposit amounts in gwei
326325
* @dev Includes a check to ensure StakingVault is balanced before making deposits
327326
*/
328327
function depositToBeaconChain(
329328
uint256 _numberOfDeposits,
330329
bytes calldata _pubkeys,
331330
bytes calldata _signatures,
332-
bytes calldata _sizes
331+
bytes calldata _amounts
333332
) external {
334333
if (_numberOfDeposits == 0) revert ZeroArgument("_numberOfDeposits");
335334
if (!isBalanced()) revert Unbalanced();
336335
if (msg.sender != _getStorage().operator) revert NotAuthorized("depositToBeaconChain", msg.sender);
337336

338337
DepositLogistics.processDeposits(
339-
DEPOSIT_CONTRACT,
338+
IDepositContract(address(DEPOSIT_CONTRACT)),
340339
_numberOfDeposits,
341340
bytes.concat(withdrawalCredentials()),
342341
_pubkeys,
343342
_signatures,
344-
_sizes
343+
_amounts
345344
);
346345

347-
emit DepositedToBeaconChain(msg.sender, _numberOfDeposits, _numberOfDeposits * 32 ether);
346+
emit DepositedToBeaconChain(msg.sender, _numberOfDeposits);
348347
}
349348

350349
/**
@@ -441,9 +440,8 @@ contract StakingVault is IStakingVault, IBeaconProxy, OwnableUpgradeable {
441440
* @notice Emitted when ether is deposited to `DepositContract`
442441
* @param sender Address that initiated the deposit
443442
* @param deposits Number of validator deposits made
444-
* @param amount Total amount of ether deposited
445443
*/
446-
event DepositedToBeaconChain(address indexed sender, uint256 deposits, uint256 amount);
444+
event DepositedToBeaconChain(address indexed sender, uint256 deposits);
447445

448446
/**
449447
* @notice Emitted when a validator exit request is made

lib/units.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
import { parseEther as ether, parseUnits } from "ethers";
1+
import { BytesLike, parseEther as ether, parseUnits } from "ethers";
22

33
export const ONE_ETHER = ether("1.0");
44

5+
function gwei(value: string) {
6+
return parseUnits(value, 9);
7+
}
8+
9+
function etherToGweiBytes(etherValue: string, bytes: number): string {
10+
return "0x" + (ether(etherValue) / gwei("1")).toString(16).padStart(bytes * 2, "0");
11+
}
12+
513
const shares = (value: bigint) => parseUnits(value.toString(), "ether");
614

715
const shareRate = (value: bigint) => parseUnits(value.toString(), 27);
816

9-
export { ether, shares, shareRate };
17+
export { ether, etherToGweiBytes, gwei, shares, shareRate };

test/0.8.25/vaults/staking-vault/staking-vault.test.ts

+67-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect } from "chai";
2-
import { AbiCoder, BytesLike, hexlify, keccak256, ZeroAddress, zeroPadValue } from "ethers";
2+
import { AbiCoder, BytesLike, hexlify, keccak256, ZeroAddress, zeroPadBytes, zeroPadValue } from "ethers";
33
import { ethers } from "hardhat";
44

55
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
@@ -15,9 +15,10 @@ import {
1515
VaultHub__MockForStakingVault,
1616
} from "typechain-types";
1717

18-
import { de0x, ether, findEvents, impersonate } from "lib";
18+
import { de0x, ether, etherToGweiBytes, findEvents, gwei, impersonate } from "lib";
1919

2020
import { Snapshot } from "test/suite";
21+
import { randomBytes } from "crypto";
2122

2223
const MAX_INT128 = 2n ** 127n - 1n;
2324
const MAX_UINT128 = 2n ** 128n - 1n;
@@ -314,26 +315,77 @@ describe.only("StakingVault", () => {
314315
).to.be.revertedWithCustomError(stakingVault, "Unbalanced");
315316
});
316317

317-
it("makes deposits to the beacon chain and emits the DepositedToBeaconChain event", async () => {
318+
it("makes one 32 eth deposit to the beacon chain and emits the DepositedToBeaconChain event", async () => {
318319
await stakingVault.fund({ value: ether("32") });
319320

320-
const pubkey = "0x" + "ab".repeat(48);
321-
const signature = "0x" + "ef".repeat(96);
322-
const size = "0x0000000773594000"; // 32 eth in gwei
323-
324-
const depositDataRoot = computeDepositDataRoot(
325-
await stakingVault.withdrawalCredentials(),
326-
pubkey,
327-
signature,
328-
size,
329-
);
321+
const pubkey = hexlify(randomBytes(48));
322+
const signature = hexlify(randomBytes(96));
323+
const size = etherToGweiBytes("32", 8);
324+
325+
const depositDataRoot = getRoot(await stakingVault.withdrawalCredentials(), pubkey, signature, size);
330326

331327
await expect(stakingVault.connect(operator).depositToBeaconChain(1, pubkey, signature, size))
332328
.to.emit(stakingVault, "DepositedToBeaconChain")
333-
.withArgs(operator, 1, ether("32"))
329+
.withArgs(operator, 1)
334330
.and.to.emit(depositContract, "DepositEvent")
335331
.withArgs(pubkey, await stakingVault.withdrawalCredentials(), signature, depositDataRoot);
336332
});
333+
334+
it("makes multiple deposits with different amounts to the beacon chain and emits the DepositedToBeaconChain event", async () => {
335+
const deposits = 5;
336+
337+
// deposit 1
338+
const pubkey1 = hexlify(randomBytes(48));
339+
const sig1 = hexlify(randomBytes(96));
340+
const amountBigInt1 = ether("1");
341+
const amountGwei1 = etherToGweiBytes("1", 8);
342+
343+
// deposit 2
344+
const pubkey2 = hexlify(randomBytes(48));
345+
const sig2 = hexlify(randomBytes(96));
346+
const amountBigInt2 = ether("10");
347+
const amountGwei2 = etherToGweiBytes("10", 8);
348+
349+
// deposit 3
350+
const pubkey3 = hexlify(randomBytes(48));
351+
const sig3 = hexlify(randomBytes(96));
352+
const amountBigInt3 = ether("32");
353+
const amountGwei3 = etherToGweiBytes("32", 8);
354+
355+
// deposit 4
356+
const pubkey4 = hexlify(randomBytes(48));
357+
const sig4 = hexlify(randomBytes(96));
358+
const amountBigInt4 = ether("35");
359+
const amountGwei4 = etherToGweiBytes("35", 8);
360+
361+
// deposit 5
362+
const pubkey5 = hexlify(randomBytes(48));
363+
const sig5 = hexlify(randomBytes(96));
364+
const amountBigInt5 = ether("50");
365+
const amountGwei5 = etherToGweiBytes("50", 8);
366+
367+
await stakingVault.fund({ value: amountBigInt1 + amountBigInt2 + amountBigInt3 + amountBigInt4 + amountBigInt5 });
368+
369+
let pubkeys = pubkey1 + de0x(pubkey2) + de0x(pubkey3) + de0x(pubkey4) + de0x(pubkey5);
370+
let sigs = sig1 + de0x(sig2) + de0x(sig3) + de0x(sig4) + de0x(sig5);
371+
let amounts = amountGwei1 + de0x(amountGwei2) + de0x(amountGwei3) + de0x(amountGwei4) + de0x(amountGwei5);
372+
373+
const creds = await stakingVault.withdrawalCredentials();
374+
375+
await expect(stakingVault.connect(operator).depositToBeaconChain(deposits, pubkeys, sigs, amounts))
376+
.to.emit(stakingVault, "DepositedToBeaconChain")
377+
.withArgs(operator, deposits)
378+
.and.to.emit(depositContract, "DepositEvent")
379+
.withArgs(pubkey1, creds, sig1, getRoot(creds, pubkey1, sig1, amountGwei1))
380+
.and.to.emit(depositContract, "DepositEvent")
381+
.withArgs(pubkey2, creds, sig2, getRoot(creds, pubkey2, sig2, amountGwei2))
382+
.and.to.emit(depositContract, "DepositEvent")
383+
.withArgs(pubkey3, creds, sig3, getRoot(creds, pubkey3, sig3, amountGwei3))
384+
.and.to.emit(depositContract, "DepositEvent")
385+
.withArgs(pubkey4, creds, sig4, getRoot(creds, pubkey4, sig4, amountGwei4))
386+
.and.to.emit(depositContract, "DepositEvent")
387+
.withArgs(pubkey5, creds, sig5, getRoot(creds, pubkey5, sig5, amountGwei5));
388+
});
337389
});
338390

339391
context("requestValidatorExit", () => {
@@ -493,7 +545,7 @@ describe.only("StakingVault", () => {
493545
return [stakingVault_, vaultHub_, vaultFactory_, stakingVaultImplementation_, depositContract_];
494546
}
495547

496-
function computeDepositDataRoot(creds: string, pubkey: string, signature: string, size: string) {
548+
function getRoot(creds: string, pubkey: string, signature: string, size: string) {
497549
// strip everything of the 0x prefix to make 0x explicit when slicing
498550
creds = creds.slice(2);
499551
pubkey = pubkey.slice(2);

0 commit comments

Comments
 (0)