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.
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) function
if > 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
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.
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:
Performance
Here is a model of gas costs evolution based on a mainnet fork:
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
What is the interaction between the Kiln "batch deposit" contract and the ETH staking contract?
The official Beacon chain deposit contract requires sending 32 ETH + the parameters to stake: pubkeys, withdrawal credentials, signatures and deposit_data_roots. This official contract requires 1 transaction per deposit, so to stake n validators requires doing n transactions.
The Kiln batch deposit contract receives n*32 ETH and an array of length n of the staking parameters for deposits (the same parameters as the official contrat: pubkeys, withdrawal credentials, signatures and deposit_data_roots).
What it then does is perform n deposits on the official deposit contract with 32 ETH each atomically within the same transaction, thus saving a significant amount of gas compared to the standard approach.
Please note:
The batch deposit contract never holds any funds
if a number different than a multiple of 32 ETH is sent or there are not enough parameters to cover it'll return the ETH back to the user
Is there a "transfer" of funds between the two contracts?
Every time a batch is sent to the deposit contract this contract does internal transactions to send these funds to the Beacon chain contract. All this is done in the same transaction, atomically, and in the same block so it never holds any funds. See the previous answer for more details.
Do the staking rewards ever "pass through" the Kiln batch deposit contract?
Never, it's just for depositing the 32 ETH + the parameters in one call to the official contract. Once this is done the rewards go directly to the withdrawal credentials of the validator (your end-user wallet).
Are there any "private" functions of the batch deposit contract that only Kiln can alter or owns?
No, there are no private functions. The contract is not upgradeable so it cannot be altered. The only callable function is batchDeposit.
The full source code is published above.
// 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);
}
}
}