ETH Batch Deposit Contract
The Kiln Ethereum Batch Deposit Contract is a minimal implementation of deposit transaction batching. The goal is to deposit N validations keys in one transaction with N * 32 ETH as value.
V2
Contract instances
The Batch Deposit Contract is available and verified at:
testnet (Hoodi): 0x00ae9b96Ef8D5D54cFCC02d9A1Ccc19ACD688B72
Batch Deposit
You can interact with the batchDeposit
function of the contract to deposit multiple validator keys deposit data, with the multiple of 32 ETH as transaction value.
We recommend you use it to send N deposit data with N * 32 ETH where N < 512.
if <= 64 validators:
call the
batchDeposit(bytes publicKeys, bytes withdrawalCreds, bytes signatures, bytes32[] dataRoots)
functionif > 64 validators, up to 512 validators
call the
bigBatchDeposit(bytes publicKeys, bytes withdrawalCreds, bytes signatures, bytes32[] dataRoots)
function
publicKeys
is the concatenation of the public keys of the validators
withdrawalCreds
is the concatenation of the withdrawal credentials of the validators
signatures
is the concatenation of the signatures of the validator deposits
dataRoots
is a list of the data roots for the validator deposits
ABI:
[{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"depositAdd","type":"address"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"batchDeposit","inputs":[{"name":"publicKeys","type":"bytes"},{"name":"withdrawalCreds","type":"bytes"},{"name":"signatures","type":"bytes"},{"name":"dataRoots","type":"bytes32[]"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"batchDepositCustom","inputs":[{"name":"publicKeys","type":"bytes"},{"name":"withdrawalCreds","type":"bytes"},{"name":"signatures","type":"bytes"},{"name":"dataRoots","type":"bytes32[]"},{"name":"amountPerValidator","type":"uint256"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"bigBatchDeposit","inputs":[{"name":"publicKeys","type":"bytes"},{"name":"withdrawalCreds","type":"bytes"},{"name":"signatures","type":"bytes"},{"name":"dataRoots","type":"bytes32[]"}],"outputs":[]},{"stateMutability":"payable","type":"function","name":"bigBatchDepositCustom","inputs":[{"name":"publicKeys","type":"bytes"},{"name":"withdrawalCreds","type":"bytes"},{"name":"signatures","type":"bytes"},{"name":"dataRoots","type":"bytes32[]"},{"name":"amountPerValidator","type":"uint256"}],"outputs":[]}]
Source Files
Source code is available here.
Security
Spearbit audited the Batch Deposit Contract in Sep 2023, report available here. No security issue was found during the audit.
Performance
Here is a model of gas costs evolution based on a mainnet fork:
Batch Deposit 5
Gas: 186 730 (37 346 / validator)
Batch Deposit 10
Gas: 323 494 (32 349 / validator)
Batch Deposit 20
Gas: 619 505 (30 975 / validator)
Batch Deposit 30
Gas: 888 375 (29 612 / validator)
Batch Deposit 50
Gas: 1 462 756 (29 255 / validator)
Batch Deposit 100
Gas: 2 892 896 (28 929 / validator)
Batch Deposit 200
Gas: 5 710 927 (28 554 / validator)
Batch Deposit 300
Gas: 8 523 984 (28 413 / validator)
Batch Deposit 400
Gas: 11 340 350 (28 350 / validator)
The V2 is capped at 512 validators per transaction
V1 [DEPRECATED]
Contract instances
The Batch Deposit Contract is available at:
testnet (Holesky): 0x008f55a61d3b96a0bdab16f3b68f6c40e3ed6fda
testnet (Goerli): 0x5FaDfdb7eFffd3B4AA03f0F29d9200Cf5F191F31
NOTE: Goerli will no longer be supported by Kiln from November 9th 2023.
Batch Deposit
You can interact with the batchDeposit
function of the contract to deposit multiple validator keys deposit data, with the multiple of 32 ETH as transaction value.
We recommend you use it to send N deposit data with N * 32 ETH where N < 150.
function batchDeposit(
bytes[] calldata pubkeys,
bytes[] calldata withdrawal_credentials,
bytes[] calldata signatures,
bytes32[] calldata deposit_data_roots
) external payable
ABI:
{
"inputs": [
{
"internalType": "bytes[]",
"name": "pubkeys",
"type": "bytes[]"
},
{
"internalType": "bytes[]",
"name": "withdrawal_credentials",
"type": "bytes[]"
},
{
"internalType": "bytes[]",
"name": "signatures",
"type": "bytes[]"
},
{
"internalType": "bytes32[]",
"name": "deposit_data_roots",
"type": "bytes32[]"
}
],
"name": "batchDeposit",
"outputs": [],
"stateMutability": "payable",
"type": "function",
"payable": true
}
Source Files
The smart contract is relatively straightforward and does not maintain a state of the deposited keys. It simply exposes a function batchDeposit(bytes[] calldata pubkeys,bytes[] calldata withdrawal_credentials, bytes[] calldata signatures, bytes32[] calldata deposit_data_roots) external payable
one can call with N deposit data and N *32 ETH as tx value.
The Contract sources are available here:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "./IDeposit.sol";
// Based on Stakefish and Staked.us contracts.
//
// We don't need fees, but we want truffle & unit tests.
contract BatchDeposit {
using Address for address payable;
using SafeMath for uint256;
uint256 public constant kDepositAmount = 32 ether;
IDeposit private depositContract_;
event LogDepositLeftover(address to, uint256 amount);
event LogDepositSent(bytes pubkey, bytes withdrawal);
// We pass the address of a contract here because this will change
// from one environment to another. On mainnet and testnet we use
// the official addresses:
//
// - testnet: 0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b (https://goerli.etherscan.io/address/0xff50ed3d0ec03aC01D4C79aAd74928BFF48a7b2b)
// - mainnet: 0x00000000219ab540356cbb839cbe05303d7705fa (https://etherscan.io/address/0x00000000219ab540356cbb839cbe05303d7705fa)
//
// The migration script handles this.
constructor (address deposit_contract_address) {
depositContract_ = IDeposit(deposit_contract_address);
}
function batchDeposit(
bytes[] calldata pubkeys,
bytes[] calldata withdrawal_credentials,
bytes[] calldata signatures,
bytes32[] calldata deposit_data_roots
) external payable {
require(
pubkeys.length == withdrawal_credentials.length &&
pubkeys.length == signatures.length &&
pubkeys.length == deposit_data_roots.length,
"#BatchDeposit batchDeposit(): All parameter array's must have the same length."
);
require(
pubkeys.length > 0,
"#BatchDeposit batchDeposit(): All parameter array's must have a length greater than zero."
);
require(
msg.value >= kDepositAmount.mul(pubkeys.length),
"#BatchDeposit batchDeposit(): Ether deposited needs to be at least: 32 * (parameter `pubkeys[]` length)."
);
uint256 deposited = 0;
for (uint256 i = 0; i < pubkeys.length; i++) {
depositContract_.deposit{value: kDepositAmount}(
pubkeys[i],
withdrawal_credentials[i],
signatures[i],
deposit_data_roots[i]
);
emit LogDepositSent(pubkeys[i], withdrawal_credentials[i]);
deposited = deposited.add(kDepositAmount);
}
assert(deposited == kDepositAmount.mul(pubkeys.length));
uint256 ethToReturn = msg.value.sub(deposited);
if (ethToReturn > 0) {
emit LogDepositLeftover(msg.sender, ethToReturn);
Address.sendValue(payable(msg.sender), ethToReturn);
}
}
}
Performance
Here is a model of gas costs evolution based on a mainnet fork:
Batch Deposit 7
Gas: 221233
Batch Deposit 8
Gas: 251485
Batch Deposit 9
Gas: 279802
Batch Deposit 10
Gas: 312046
Batch Deposit 100
Gas: 3049083
Batch Deposit 200
Gas: 6103163
Batch Deposit 300
Gas: 9233661
Batch Deposit 400
Gas: 12419018
Batch Deposit 500
Gas: 15697211
We recommend to not batch deposit more than 150 keys to avoid transaction with too much gas and low chance to get included by the network.
Security
Batch Deposit Contract is based on Stake Fish batch deposit contract which has been audited. Some of Kiln partners have also internally audited the Batch Deposit Contract.
FAQs
Last updated
Was this helpful?