Assessment reports>IBC Eureka>Critical findings>Merkle verification could be bypassed
Category: Coding Mistakes

Merkle verification could be bypassed

Critical Severity
Critical Impact
High Likelihood

Description

As per the sync-protocol specifications, the update.next_sync_committee should be validated via the Merkle proof if the next_sync_committee_branch is not empty, as shown below:

if not is_sync_committee_update(update):
    assert update.next_sync_committee == SyncCommittee()
else:
    if update_attested_period == store_period and is_next_sync_committee_known(store):
        assert update.next_sync_committee == store.next_sync_committee
    assert is_valid_normalized_merkle_branch(
        leaf=hash_tree_root(update.next_sync_committee),
        branch=update.next_sync_committee_branch,
        gindex=next_sync_committee_gindex_at_slot(update.attested_header.beacon.slot)
        root=update.attested_header.beacon.state_root,
    )

But in the implementation of ethereum-light-client, the Merkle verification is executed in the case update.next_sync_committee and trusted_consensus_state.next_sync_committee() are both available. As the trusted_consensus_state.next_sync_committee() (see Finding ref) comes from user input too, it might be possible to prove the incorrect next_sync_committee.

if let (Some(next_sync_committee), Some(stored_next_sync_committee)) = (
    &update.next_sync_committee,
    trusted_consensus_state.next_sync_committee(),
) {
    if update_attested_period == stored_period {
        ensure!(
            next_sync_committee == stored_next_sync_committee,
            EthereumIBCError::NextSyncCommitteeMismatch {
                expected: stored_next_sync_committee.aggregate_pubkey,
                found: next_sync_committee.aggregate_pubkey,
            }
        );
    }
    // This validates the given next sync committee against the attested header's state root.
    validate_merkle_branch(
        next_sync_committee.tree_hash_root(),
        update.next_sync_committee_branch.unwrap_or_default().into(),
        NEXT_SYNC_COMMITTEE_BRANCH_DEPTH,
        get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX),
        update.attested_header.beacon.state_root,
    )
    .map_err(|e| EthereumIBCError::ValidateNextSyncCommitteeFailed(Box::new(e)))?;
}

Impact

As the sync committee is used to validate the attested_header, an incorrect header could be provided, which could be used to prove an incorrect finalized_root and state root. This could be used to update the light client with incorrect states.

Recommendations

We recommend following the specification and validating the Merkle proof in cases similar to how it is validated in the specification.

Remediation

This issue has been acknowledged by Interchain Labs, and a fix was implemented in commit d46a8e55.

Zellic © 2025Back to top ↑