Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b8dbe3e
deposit contract
ameya-deshmukh Nov 11, 2025
c0f454b
sforge fmt
ameya-deshmukh Nov 11, 2025
a6fcac7
matthias updated contract
ameya-deshmukh Nov 11, 2025
57040c7
ok
ameya-deshmukh Nov 11, 2025
bb9de34
hmmm
ameya-deshmukh Nov 11, 2025
9b98a19
as any for now, resolve type issue later
ameya-deshmukh Nov 11, 2025
fb10d9a
type issue
ameya-deshmukh Nov 11, 2025
cbccef1
ok better
ameya-deshmukh Nov 11, 2025
16b7cfd
read functions + prettier
ameya-deshmukh Nov 12, 2025
13f82dc
fix case
ameya-deshmukh Nov 12, 2025
5868531
better
ameya-deshmukh Nov 12, 2025
baa9b1b
resolveJsonModule true
ameya-deshmukh Nov 12, 2025
37561fa
prettier
ameya-deshmukh Nov 12, 2025
4e4ee12
add in json to types
ameya-deshmukh Nov 12, 2025
7159c5e
type error returns
ameya-deshmukh Nov 12, 2025
6e7c6ab
as any for now
ameya-deshmukh Nov 12, 2025
e2fc7eb
extend shieldedpublic and wallet client actions
ameya-deshmukh Nov 12, 2025
c5e9f59
lint
ameya-deshmukh Nov 12, 2025
fc022ee
rm unnecessary config types
ameya-deshmukh Nov 13, 2025
435bef7
add missing event etc
ameya-deshmukh Nov 13, 2025
f3272cd
lint
ameya-deshmukh Nov 13, 2025
8241ac0
rm json from tsconfig+types config
ameya-deshmukh Nov 13, 2025
d4abec4
DRY
ameya-deshmukh Nov 13, 2025
82e5382
testing
ameya-deshmukh Nov 13, 2025
5fbfa6f
ok all tests pass
ameya-deshmukh Nov 13, 2025
1a40e24
better
ameya-deshmukh Nov 13, 2025
8310a52
Merge branch 'main' into ameya/deposit-contract
ameya-deshmukh Nov 24, 2025
0300ca0
default deposit contract
cdrappi Nov 25, 2025
41d7380
Merge branch 'main' into ameya/deposit-contract
cdrappi Nov 25, 2025
efabeeb
new vser
cdrappi Nov 25, 2025
b2e30c5
newlines
cdrappi Nov 25, 2025
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
1 change: 1 addition & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
src = "src"
out = "out"
libs = ["lib"]
via_ir = true
193 changes: 193 additions & 0 deletions contracts/src/DepositContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// ┏━━━┓━┏┓━┏┓━━┏━━━┓━━┏━━━┓━━━━┏━━━┓━━━━━━━━━━━━━━━━━━━┏┓━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━━━━━━━┏┓━
// ┃┏━━┛┏┛┗┓┃┃━━┃┏━┓┃━━┃┏━┓┃━━━━┗┓┏┓┃━━━━━━━━━━━━━━━━━━┏┛┗┓━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━━━━━━┏┛┗┓
// ┃┗━━┓┗┓┏┛┃┗━┓┗┛┏┛┃━━┃┃━┃┃━━━━━┃┃┃┃┏━━┓┏━━┓┏━━┓┏━━┓┏┓┗┓┏┛━━━━┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓━┏━━┓┗┓┏┛
// ┃┏━━┛━┃┃━┃┏┓┃┏━┛┏┛━━┃┃━┃┃━━━━━┃┃┃┃┃┏┓┃┃┏┓┃┃┏┓┃┃━━┫┣┫━┃┃━━━━━┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┗━┓┃━┃┏━┛━┃┃━
// ┃┗━━┓━┃┗┓┃┃┃┃┃┃┗━┓┏┓┃┗━┛┃━━━━┏┛┗┛┃┃┃━┫┃┗┛┃┃┗┛┃┣━━┃┃┃━┃┗┓━━━━┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┗┓┃┗━┓━┃┗┓
// ┗━━━┛━┗━┛┗┛┗┛┗━━━┛┗┛┗━━━┛━━━━┗━━━┛┗━━┛┃┏━┛┗━━┛┗━━┛┗┛━┗━┛━━━━┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━━┛┗━━┛━┗━┛
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.30;

// This interface is designed to be compatible with the Vyper version.
/// @notice This is the Ethereum 2.0 deposit contract interface.
/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs
interface IDepositContract {
/// @notice A processed deposit event.
event DepositEvent(
bytes node_pubkey,
bytes consensus_pubkey,
bytes withdrawal_credentials,
bytes amount,
bytes node_signature,
bytes consensus_signature,
bytes index
);

/// @notice Submit a Phase 0 DepositData object.
/// @param node_pubkey An ED25519 public key.
/// @param consensus_pubkey A BLS12-381 public key.
/// @param withdrawal_credentials Commitment to a public key for withdrawals.
/// @param node_signature An ED25519 signature.
/// @param consensus_signature A BLS12-381 signature.
/// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object.
/// Used as a protection against malformed input.
function deposit(
bytes calldata node_pubkey,
bytes calldata consensus_pubkey,
bytes calldata withdrawal_credentials,
bytes calldata node_signature,
bytes calldata consensus_signature,
bytes32 deposit_data_root
) external payable;

/// @notice Query the current deposit root hash.
/// @return The deposit root hash.
function get_deposit_root() external view returns (bytes32);

/// @notice Query the current deposit count.
/// @return The deposit count encoded as a little endian 64-bit number.
function get_deposit_count() external view returns (bytes memory);
}

// Based on official specification in https://eips.ethereum.org/EIPS/eip-165
interface ERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceId The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceId` and
/// `interfaceId` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceId) external pure returns (bool);
}

// This is a rewrite of the Vyper Eth2.0 deposit contract in Solidity.
// It tries to stay as close as possible to the original source code.
/// @notice This is the Ethereum 2.0 deposit contract interface.
/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs
contract DepositContract is IDepositContract, ERC165 {
uint constant DEPOSIT_CONTRACT_TREE_DEPTH = 32;
// NOTE: this also ensures `deposit_count` will fit into 64-bits
uint constant MAX_DEPOSIT_COUNT = 2**DEPOSIT_CONTRACT_TREE_DEPTH - 1;

bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] branch;
uint256 deposit_count;

bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] zero_hashes;

constructor() {
// Compute hashes in empty sparse Merkle tree
for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH - 1; height++)
zero_hashes[height + 1] = sha256(abi.encodePacked(zero_hashes[height], zero_hashes[height]));
}

function get_deposit_root() override external view returns (bytes32) {
bytes32 node;
uint size = deposit_count;
for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) {
if ((size & 1) == 1)
node = sha256(abi.encodePacked(branch[height], node));
else
node = sha256(abi.encodePacked(node, zero_hashes[height]));
size /= 2;
}
return sha256(abi.encodePacked(
node,
to_little_endian_64(uint64(deposit_count)),
bytes24(0)
));
}

function get_deposit_count() override external view returns (bytes memory) {
return to_little_endian_64(uint64(deposit_count));
}

function deposit(
bytes calldata node_pubkey,
bytes calldata consensus_pubkey,
bytes calldata withdrawal_credentials,
bytes calldata node_signature,
bytes calldata consensus_signature,
bytes32 deposit_data_root
) override external payable {
// Extended ABI length checks since dynamic types are used.
require(node_pubkey.length == 32, "DepositContract: invalid node_pubkey length");
require(consensus_pubkey.length == 48, "DepositContract: invalid consensus_pubkey length");
require(withdrawal_credentials.length == 32, "DepositContract: invalid withdrawal_credentials length");
require(node_signature.length == 64, "DepositContract: invalid node_signature length");
require(consensus_signature.length == 96, "DepositContract: invalid consensus_signature length");

// Check deposit amount
require(msg.value >= 1 ether, "DepositContract: deposit value too low");
require(msg.value % 1 gwei == 0, "DepositContract: deposit value not multiple of gwei");
uint deposit_amount = msg.value / 1 gwei;
require(deposit_amount <= type(uint64).max, "DepositContract: deposit value too high");

// Emit `DepositEvent` log
bytes memory amount = to_little_endian_64(uint64(deposit_amount));
emit DepositEvent(
node_pubkey,
consensus_pubkey,
withdrawal_credentials,
amount,
node_signature,
consensus_signature,
to_little_endian_64(uint64(deposit_count))
);

// Compute deposit data root (`DepositData` hash tree root)
bytes32 consensus_pubkey_hash = sha256(abi.encodePacked(consensus_pubkey, bytes16(0)));
Copy link

Choose a reason for hiding this comment

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

Bug: SSZ Hashing Mismatch Breaks Deposit Verification

The consensus_pubkey (48 bytes padded to 64) is hashed as a single 64-byte value, but SSZ requires splitting into two 32-byte chunks that are hashed separately before combining. This inconsistency with the consensus_signature handling (which correctly splits chunks) will cause incorrect hash tree root computation and deposit verification failures.

Fix in Cursor Fix in Web

bytes32 pubkey_root = sha256(abi.encodePacked(node_pubkey, consensus_pubkey_hash));
bytes32 node_signature_hash = sha256(node_signature);
bytes32 consensus_signature_hash = sha256(abi.encodePacked(
sha256(abi.encodePacked(consensus_signature[:64])),
sha256(abi.encodePacked(consensus_signature[64:], bytes32(0)))
));
bytes32 signature_root = sha256(abi.encodePacked(node_signature_hash, consensus_signature_hash));
bytes32 node = sha256(abi.encodePacked(
sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)),
sha256(abi.encodePacked(amount, bytes24(0), signature_root))
));

// Verify computed and expected deposit data roots match
require(node == deposit_data_root, "DepositContract: reconstructed DepositData does not match supplied deposit_data_root");

// Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`)
require(deposit_count < MAX_DEPOSIT_COUNT, "DepositContract: merkle tree full");

// Add deposit data root to Merkle tree (update a single `branch` node)
deposit_count += 1;
uint size = deposit_count;
for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) {
if ((size & 1) == 1) {
branch[height] = node;
return;
}
node = sha256(abi.encodePacked(branch[height], node));
size /= 2;
}
// As the loop should always end prematurely with the `return` statement,
// this code should be unreachable. We assert `false` just to be safe.
assert(false);
}

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

function to_little_endian_64(uint64 value) internal pure returns (bytes memory ret) {
ret = new bytes(8);
bytes8 bytesValue = bytes8(value);
// Byteswapping during copying to bytes.
ret[0] = bytesValue[7];
ret[1] = bytesValue[6];
ret[2] = bytesValue[5];
ret[3] = bytesValue[4];
ret[4] = bytesValue[3];
ret[5] = bytesValue[2];
ret[6] = bytesValue[1];
ret[7] = bytesValue[0];
}
}
15 changes: 9 additions & 6 deletions contracts/test/ShieldedDelegationAccount.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,12 @@ contract ShieldedDelegationAccountTest is Test, ShieldedDelegationAccount {
/// @param keyIndex Index of the key to use
/// @param cipher Encrypted data to be executed
/// @return signature The signature bytes
function _signExecuteDigestWithKey(address payable account, uint32 keyIndex, bytes memory cipher, uint256 privateKey)
internal
view
returns (bytes memory signature)
{
function _signExecuteDigestWithKey(
address payable account,
uint32 keyIndex,
bytes memory cipher,
uint256 privateKey
) internal view returns (bytes memory signature) {
uint256 keyNonce = ShieldedDelegationAccount(account).getKeyNonce(keyIndex);
Key memory key = ShieldedDelegationAccount(account).getKey(keyIndex);
bytes32 domainSeparator = _getDomainSeparator();
Expand Down Expand Up @@ -299,7 +300,9 @@ contract ShieldedDelegationAccountTest is Test, ShieldedDelegationAccount {
/// @param keyIndex The key index to use
/// @param calls The encoded calls to execute
/// @param privateKey The private key to sign with
function _executeViaKey(address payable account, uint32 keyIndex, bytes memory calls, uint256 privateKey) internal {
function _executeViaKey(address payable account, uint32 keyIndex, bytes memory calls, uint256 privateKey)
internal
{
// Encrypt the calls
(uint96 nonce, bytes memory cipher) = ShieldedDelegationAccount(account).encrypt(calls);

Expand Down
4 changes: 2 additions & 2 deletions packages/seismic-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "seismic-react",
"version": "1.0.50",
"version": "1.0.51",
"description": "React components for Seismic.",
"type": "module",
"main": "./dist/_cjs/index.js",
Expand Down Expand Up @@ -52,7 +52,7 @@
"typescript": ">=5.0.4",
"viem": "2.x",
"wagmi": "^2.0.0",
"seismic-viem": ">=1.0.50"
"seismic-viem": ">=1.0.51"
},
"peerDependenciesMeta": {
"typescript": {
Expand Down
1 change: 1 addition & 0 deletions packages/seismic-viem-tests/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type {

export { testSeismicTxEncoding } from '@sviem-tests/tests/encoding.ts'
export { testSeismicTx } from '@sviem-tests/tests/contract/contract.ts'
export { testDepositContract } from '@sviem-tests/tests/contract/depositContract.ts'
export {
testContractTreadIsntSeismicTx,
testShieldedWalletClientTreadIsntSeismicTx,
Expand Down
Loading