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

chore(cli): Enforce valid withdrawal address set for add-premined-deposit & create-validator #2174

Merged
merged 39 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7a29a1b
enforce withdrawal address
calbera Nov 24, 2024
9dc7cc2
undo
calbera Nov 24, 2024
6a7e2fb
undo
calbera Nov 24, 2024
f4569b3
create-validator
calbera Nov 24, 2024
26cff08
Merge branch 'main' of github.com:berachain/beacon-kit into withdrawa…
calbera Nov 28, 2024
ac9e59a
val set optimization
calbera Nov 28, 2024
03b6fda
gen
calbera Nov 28, 2024
7d20585
Merge branch 'main' of github.com:berachain/beacon-kit into withdrawa…
calbera Nov 29, 2024
41d1a33
undo
calbera Nov 29, 2024
4a334f4
gen
calbera Nov 29, 2024
7cd92fd
berachain deposit contract
calbera Nov 29, 2024
835eb81
Merge branch 'main' into withdrawal-cli
calbera Nov 29, 2024
34871dd
fix deposit contract tests
calbera Nov 29, 2024
1ec0176
Fix e2de
calbera Nov 29, 2024
5032533
gen new deposit
calbera Nov 29, 2024
9753679
slither
calbera Nov 29, 2024
737ccb9
remove unneeded test
calbera Nov 29, 2024
c5ee10f
gen
calbera Nov 29, 2024
41579df
remove
calbera Nov 29, 2024
d824058
bet
calbera Nov 29, 2024
3220996
Default withdrawal of 0
calbera Nov 29, 2024
c5b07ce
remove forced gas limit from e2e
calbera Nov 29, 2024
2c5b519
Merge branch 'main' into withdrawal-cli
calbera Dec 3, 2024
dcf7187
Merge branch 'main' into withdrawal-cli
calbera Dec 3, 2024
9df3a71
Merge branch 'main' of github.com:berachain/beacon-kit into withdrawa…
calbera Dec 4, 2024
b2f1273
clean tests
calbera Dec 4, 2024
519b13e
Merge branch 'main' of github.com:berachain/beacon-kit into withdrawa…
calbera Dec 5, 2024
3e940a9
use deposit contract with MIN_DEPOSIT_AMOUNT fix
calbera Dec 5, 2024
8da430b
removed defaults
calbera Dec 5, 2024
39037a2
Remove flag for required values
calbera Dec 5, 2024
a02bb08
allow 0 values for amount & address
calbera Dec 5, 2024
64446b4
Fix deposit contract unit test
calbera Dec 5, 2024
5198666
Deposit contract renaming
calbera Dec 5, 2024
bb38963
lint
calbera Dec 5, 2024
2b7dcc6
Merge branch 'main' of github.com:berachain/beacon-kit into withdrawa…
calbera Dec 6, 2024
081ad11
fix e2e test by using different pubkeys
calbera Dec 6, 2024
5dbb29a
bet
calbera Dec 6, 2024
1fb5989
restore 80084 genesis files
calbera Dec 6, 2024
7cbfc84
Merge branch 'main' of github.com:berachain/beacon-kit into withdrawa…
calbera Dec 6, 2024
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
140 changes: 113 additions & 27 deletions contracts/src/staking/DepositContract.sol
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import { IDepositContract } from "./IDepositContract.sol";
import { ERC165 } from "./IERC165.sol";
import { IDepositContract } from "./IDepositContract.sol";

/**
* @title DepositContract
* @author Berachain Team
* @notice A contract that handles deposits of stake.
* @notice A contract that handles validators deposits.
* @dev Its events are used by the beacon chain to manage the staking process.
* @dev Its stake asset needs to be of 18 decimals to match the native asset.
* @dev This contract does not implement the deposit merkle tree.
*/
abstract contract DepositContract is IDepositContract, ERC165 {
contract DepositContract is IDepositContract, ERC165 {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* CONSTANTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev The minimum amount of stake that can be deposited to prevent dust.
/// @dev The minimum amount of `BERA` to deposit.
/// @dev This is 32 ether in Gwei since our deposit contract denominates in Gwei. 32e9 * 1e9 = 32e18.
uint64 internal constant MIN_DEPOSIT_AMOUNT_IN_GWEI = 32e9;

Expand All @@ -30,43 +29,63 @@ abstract contract DepositContract is IDepositContract, ERC165 {
/// @dev The length of the credentials, 1 byte prefix + 11 bytes padding + 20 bytes address = 32 bytes.
uint8 internal constant CREDENTIALS_LENGTH = 32;

/// @dev 1 day in seconds.
/// @dev This is the delay before a new operator can accept a change.
uint96 private constant ONE_DAY = 86_400;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev QueuedOperator is a struct that represents an operator address change request.
struct QueuedOperator {
uint96 queuedTimestamp;
address newOperator;
}

/// @dev depositCount represents the number of deposits that
/// have been made to the contract.
/// @dev The index of the next deposit will use this value.
uint64 public depositCount;

/// @dev The hash tree root of the genesis deposits.
/// @dev Should be set in deployment (predeploy state or constructor).
bytes32 public genesisDepositsRoot;

/// @dev The mapping of public keys to operator addresses.
mapping(bytes => address) private _operatorByPubKey;

calbera marked this conversation as resolved.
Show resolved Hide resolved
/// @dev The mapping of public keys to operator change requests.
mapping(bytes => QueuedOperator) public queuedOperator;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VIEWS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @inheritdoc ERC165
function supportsInterface(bytes4 interfaceId)
external
pure
override
returns (bool)
{
return interfaceId == type(ERC165).interfaceId
|| interfaceId == type(IDepositContract).interfaceId;
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return interfaceId == type(ERC165).interfaceId || interfaceId == type(IDepositContract).interfaceId;
}

/// @inheritdoc IDepositContract
function getOperator(bytes calldata pubkey) external view returns (address) {
return _operatorByPubKey[pubkey];
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* WRITES */
/* DEPOSIT */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @inheritdoc IDepositContract
function deposit(
bytes calldata pubkey,
bytes calldata credentials,
uint64 amount,
bytes calldata signature
bytes calldata signature,
address operator
)
public
payable
virtual
payable
calbera marked this conversation as resolved.
Show resolved Hide resolved
{
if (pubkey.length != PUBLIC_KEY_LENGTH) {
revert InvalidPubKeyLength();
Expand All @@ -80,22 +99,91 @@ abstract contract DepositContract is IDepositContract, ERC165 {
revert InvalidSignatureLength();
}

uint64 amountInGwei = _deposit(amount);
// Set operator on the first deposit.
// zero `_operatorByPubKey[pubkey]` means the pubkey is not registered.
if (_operatorByPubKey[pubkey] == address(0)) {
if (operator == address(0)) {
revert ZeroOperatorOnFirstDeposit();
}
_operatorByPubKey[pubkey] = operator;
emit OperatorUpdated(pubkey, operator, address(0));
}
// If not the first deposit, operator address must be 0.
// This prevents from the front-running of the first deposit to set the operator.
else if (operator != address(0)) {
revert OperatorAlreadySet();
}

uint64 amountInGwei = _deposit();

if (amountInGwei < MIN_DEPOSIT_AMOUNT_IN_GWEI) {
revert InsufficientDeposit();
calbera marked this conversation as resolved.
Show resolved Hide resolved
}

unchecked {
// slither-disable-next-line reentrancy-benign,reentrancy-events
emit Deposit(
pubkey, credentials, amountInGwei, signature, depositCount++
);
// slither-disable-next-line reentrancy-benign,reentrancy-events
emit Deposit(pubkey, credentials, amountInGwei, signature, depositCount++);
calbera marked this conversation as resolved.
Show resolved Hide resolved
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* OPERATOR CHANGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @inheritdoc IDepositContract
function requestOperatorChange(bytes calldata pubkey, address newOperator) external {
// Cache the current operator.
address currentOperator = _operatorByPubKey[pubkey];
// Only the current operator can request a change.
// This will also revert if the pubkey is not registered.
if (msg.sender != currentOperator) {
revert NotOperator();
}
// Revert if the new operator is zero address.
if (newOperator == address(0)) {
revert ZeroAddress();
}
QueuedOperator storage qO = queuedOperator[pubkey];
qO.newOperator = newOperator;
qO.queuedTimestamp = uint96(block.timestamp);
emit OperatorChangeQueued(pubkey, newOperator, currentOperator, block.timestamp);
}

/// @inheritdoc IDepositContract
function cancelOperatorChange(bytes calldata pubkey) external {
// Only the current operator can cancel the change.
if (msg.sender != _operatorByPubKey[pubkey]) {
revert NotOperator();
}
delete queuedOperator[pubkey];
emit OperatorChangeCancelled(pubkey);
}

/// @inheritdoc IDepositContract
function acceptOperatorChange(bytes calldata pubkey) external {
QueuedOperator storage qO = queuedOperator[pubkey];
(address newOperator, uint96 queuedTimestamp) = (qO.newOperator, qO.queuedTimestamp);

// Only the new operator can accept the change.
// This will revert if nothing is queued as newOperator will be zero address.
if (msg.sender != newOperator) {
revert NotNewOperator();
}
// Check if the queue delay has passed.
if (queuedTimestamp + ONE_DAY > uint96(block.timestamp)) {
revert NotEnoughTime();
}
// Cache the old operator.
address oldOperator = _operatorByPubKey[pubkey];
_operatorByPubKey[pubkey] = newOperator;
delete queuedOperator[pubkey];
emit OperatorUpdated(pubkey, newOperator, oldOperator);
}
calbera marked this conversation as resolved.
Show resolved Hide resolved

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev Validates the deposit amount and sends the native asset to the zero address.
function _deposit(uint64) internal virtual returns (uint64) {
function _deposit() internal virtual returns (uint64) {
if (msg.value % 1 gwei != 0) {
revert DepositNotMultipleOfGwei();
}
Expand All @@ -119,9 +207,7 @@ abstract contract DepositContract is IDepositContract, ERC165 {
function _safeTransferETH(address to, uint256 amount) internal {
/// @solidity memory-safe-assembly
assembly {
if iszero(
call(gas(), to, amount, codesize(), 0x00, codesize(), 0x00)
) {
if iszero(call(gas(), to, amount, codesize(), 0x00, codesize(), 0x00)) {
mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
revert(0x1c, 0x04)
}
Expand Down
129 changes: 104 additions & 25 deletions contracts/src/staking/IDepositContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,105 @@ pragma solidity 0.8.26;

/// @title IDepositContract
/// @author Berachain Team.
/// @dev This contract is used to create validator, deposit and withdraw stake from the Beacon chain.
interface IDepositContract {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EVENTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @dev Emitted when a deposit is made, which could mean a new validator or a top up of an existing one.
* @param pubkey the public key of the validator who is being deposited for if not a new validator.
* @param credentials the public key of the operator if new validator or the depositor if top up.
* @param amount the amount of stake being deposited, in Gwei.
* @param signature the signature of the deposit message, only checked when creating a new validator.
* @param index the index of the deposit.
*/
event Deposit(
bytes pubkey,
bytes credentials,
uint64 amount,
bytes signature,
uint64 index
);

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERRORS */
/* ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

// Signature: 0xe8966d7a
error NotEnoughTime();
// Signature: 0xd92e233d
error ZeroAddress();
// Signature: 0x7c214f04
error NotOperator();
calbera marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Error thrown when the deposit amount is too small, to prevent dust deposits.
// Signature: 0x0e1eddda
error InsufficientDeposit();

/// @dev Error thrown when the deposit amount is not a multiple of Gwei.
// Signature: 0x40567b38
error DepositNotMultipleOfGwei();

/// @dev Error thrown when the deposit amount is too high, since it is a uint64.
// Signature: 0x2aa66734
error DepositValueTooHigh();

/// @dev Error thrown when the public key length is not 48 bytes.
// Signature: 0x9f106472
error InvalidPubKeyLength();

/// @dev Error thrown when the withdrawal credentials length is not 32 bytes.
// Signature: 0xb39bca16
error InvalidCredentialsLength();

/// @dev Error thrown when the signature length is not 96 bytes.
// Signature: 0x4be6321b
error InvalidSignatureLength();

/// @dev Error thrown when the input operator is zero address on the first deposit.
// Signature: 0x51969a7a
error ZeroOperatorOnFirstDeposit();
calbera marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Error thrown when the operator is already set and caller passed non-zero operator.
// Signature: 0xc4142b41
error OperatorAlreadySet();

/// @dev Error thrown when the caller is not the current operator.
// Signature: 0x819a0d0b
error NotNewOperator();
calbera marked this conversation as resolved.
Show resolved Hide resolved

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EVENTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @dev Emitted when a deposit is made, which could mean a new validator or a top up of an existing one.
* @param pubkey the public key of the validator who is being deposited for if not a new validator.
* @param credentials the public key of the operator if new validator or the depositor if top up.
* @param amount the amount of stake being deposited, in Gwei.
* @param signature the signature of the deposit message, only checked when creating a new validator.
* @param index the index of the deposit.
*/
event Deposit(bytes pubkey, bytes credentials, uint64 amount, bytes signature, uint64 index);

/**
* @notice Emitted when the operator change of a validator is queued.
* @param pubkey The pubkey of the validator.
* @param queuedOperator The new queued operator address.
* @param currentOperator The current operator address.
* @param queuedTimestamp The timestamp when the change was queued.
*/
event OperatorChangeQueued(
bytes indexed pubkey, address queuedOperator, address currentOperator, uint256 queuedTimestamp
);
calbera marked this conversation as resolved.
Show resolved Hide resolved

/**
* @notice Emitted when the operator change of a validator is cancelled.
* @param pubkey The pubkey of the validator.
*/
event OperatorChangeCancelled(bytes indexed pubkey);

/**
* @notice Emitted when the operator of a validator is updated.
* @param pubkey The pubkey of the validator.
* @param newOperator The new operator address.
* @param previousOperator The previous operator address.
*/
event OperatorUpdated(bytes indexed pubkey, address newOperator, address previousOperator);
calbera marked this conversation as resolved.
Show resolved Hide resolved

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* VIEWS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/**
* @notice Get the operator address for a given pubkey.
* @dev Returns zero address if the pubkey is not registered.
* @param pubkey The pubkey of the validator.
* @return The operator address for the given pubkey.
*/
function getOperator(bytes calldata pubkey) external view returns (address);
calbera marked this conversation as resolved.
Show resolved Hide resolved

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* WRITES */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand All @@ -57,16 +112,40 @@ interface IDepositContract {
* @param pubkey is the consensus public key of the validator. If subsequent deposit, its ignored.
* @param credentials is the staking credentials of the validator. If this is the first deposit it is
* validator operator public key, if subsequent deposit it is the depositor's public key.
* @param amount is the amount of stake native/ERC20 token to be deposited, in Gwei.
* @param signature is the signature used only on the first deposit.
* @param operator is the address of the operator.
* @dev emits the Deposit event upon successful deposit.
* @dev Reverts if the operator is already set and caller passed non-zero operator.
*/
function deposit(
bytes calldata pubkey,
bytes calldata credentials,
uint64 amount,
bytes calldata signature
bytes calldata signature,
address operator
calbera marked this conversation as resolved.
Show resolved Hide resolved
)
external
payable;

/**
* @notice Request to change the operator of a validator.
* @dev Only the current operator can request a change.
* @param pubkey The pubkey of the validator.
* @param newOperator The new operator address.
*/
function requestOperatorChange(bytes calldata pubkey, address newOperator) external;

/**
* @notice Cancel the operator change of a validator.
* @dev Only the current operator can cancel the change.
* @param pubkey The pubkey of the validator.
*/
function cancelOperatorChange(bytes calldata pubkey) external;

/**
* @notice Accept the operator change of a validator.
* @dev Only the new operator can accept the change.
* @dev Reverts if the queue delay has not passed.
calbera marked this conversation as resolved.
Show resolved Hide resolved
* @param pubkey The pubkey of the validator.
*/
function acceptOperatorChange(bytes calldata pubkey) external;
calbera marked this conversation as resolved.
Show resolved Hide resolved
}
Loading
Loading