DeFiHardhatFoundry
250,000 USDC
View results
Submission Details
Severity: high
Invalid

Merkle-tree leaf is hashed only once, this is susceptible to second preimage attacks

Summary

Merkle-tree leaves, which are single-hashed are vulnerable to second preimage attacks.

Vulnerability Details

The vulnerable code is in L2ContractMigrationFacet::verifyDepositsAndInternalBalances function, which generates a leaf and then verifies it with the merkle root, this function is called in a redeem function, which helps with unlocking deposits and bean-tokens on L2 networks:

function verifyDepositsAndInternalBalances(
address account,
AccountDepositData[] calldata deposits,
AccountInternalBalance[] calldata internalBalances,
uint256 ownerRoots,
bytes32[] calldata proof
) internal pure {
@> bytes32 leaf = keccak256(abi.encode(account, deposits, internalBalances, ownerRoots));
require(MerkleProof.verify(proof, MERKLE_ROOT, leaf), "Migration: invalid proof");
}

Generally second preimage attack in Merkle-trees is called when an intermediate node is presented as a leaf. More specifically if an attacker can present a "leaf" that the contract successfully verifies, but that "leaf" will not be the one in the original Merkle-tree.

I'm providing a link with additional explanation of the attack path here: https://crypto.stackexchange.com/questions/81981/how-does-second-pre-image-attack-on-merkle-signature-scheme-work

An example, consider the following merkle-tree:

h01234567 - this will be the root hash
/ \
/ \
h0123 h4567
/ \ / \
h01 h23 h45 h67
/ \ / \ / \ / \
h0 h1 h2 h3 h4 h5 h6 h7

Suppose a user must create a proof for leaf 1 (h1), so the proof must be h0, h23, h4567, since the higher based hashes are concatenating with the child values.

Attack scenario:

  1. Based on how hash proofs are generated, if an attacker provides h23 and h4567 as a proof, the contract will consider h01 as the leaf, which is result of h(h0 + h1).

  2. So if the proof is h23, h4567 it will be accepted as a valid one. Thus, a shortened version will also be accepted, if the first value is passed in the original proof (user's proof) as a leaf.

  3. An attacker will succeed to generate like a "fake-valid" leaf, and a proof which will be valid for the contract.

  4. Attacker can steal bean-funds, this way.

Impact

  • Impact: High, theft of funds from honest users is involved

  • Likelihood: High, as an attacker will be incentivized to execute the attack

Tools Used

Manual Review

Recommendations

As OpenZeppelin suggests, double hash the leaf:

function verifyDepositsAndInternalBalances(
address account,
AccountDepositData[] calldata deposits,
AccountInternalBalance[] calldata internalBalances,
uint256 ownerRoots,
bytes32[] calldata proof
) internal pure {
- bytes32 leaf = keccak256(abi.encode(account, deposits, internalBalances, ownerRoots));
+ bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, deposits, internalBalances, ownerRoots))));
require(MerkleProof.verify(proof, MERKLE_ROOT, leaf), "Migration: invalid proof");
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Appeal created

dimah7 Submitter
about 1 year ago
inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.