Reproduced Exploit

Hinkal: Tree capacity guard uses `!=` instead of `<=`, letting an overflow overwrite an earlier commitment

Jan 1970Otheruntagged3 min read

Chain

Other

Category

untagged

Date

Jan 1970

Source

AuditVault

EVM Playground

Source-level debugger — step opcodes and Solidity in sync

evm-hack-analyzer

The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.

Loading fork state…

Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 60150-commitments-can-be-overwritten-by-overflowing-the-tree-quant. The historical source/toolchain is unavailable; this entry is documentation only and claims no executable Forge PoC.


Vulnerability classes: vuln/off-by-one · vuln/state-corruption · vuln/merkle-tree

Reproduction: a faithful minimal reproduction of the vulnerable finding — the vulnerable function is reproduced verbatim (marked @>) with faithful minimal doubles; local deploy, no fork.

Root cause#

The full-tree guard checks require(newIndex != 2**LEVELS) (line 59) instead of <=, so once the tree is full an insert with newIndex > 2**LEVELS passes. The overflowing leaf is then written at an index that aliases an earlier position, and its parent (index/2) wraps back into the leaf region (onto MINIMUM_INDEX) — overwriting a previously-stored commitment and invalidating that depositor's funds.

SOLIDITY
    // VERBATIM buggy capacity guard from the finding: uses `!=` instead of `<`.
    function insert(bytes32 leaf) public {
        uint256 newIndex = m_index;
        require(newIndex != uint256(2) ** LEVELS, "Tree is full."); // @>
        _insert(leaf);

Why it's exploitable here#

  • require(newIndex != 2**LEVELS) passes for any newIndex > 2**LEVELS, so the tree is not actually capacity-bounded.
  • The overflowing leaf's parent (index/2) aliases MINIMUM_INDEX, so a previously-stored commitment slot is overwritten.
  • The overwritten commitment is silently invalidated — that depositor's funds become unspendable.

Attack path#

flowchart TD A["Tree filled to its 16-leaf capacity"] --> B["Extra insert: newIndex > 2**LEVELS"] B --> C["Guard require(newIndex != 2**LEVELS) still passes"] C --> D["Overflow leaf aliases an earlier slot"] D --> E["Parent index wraps onto MINIMUM_INDEX"] E --> F["Earlier commitment overwritten / invalidated"]

Marked-line walkthrough (Playground)#

The EVM Playground pins each step to the exact executed source line in MerkleBase:

  1. Line 60 — the != guard (line 59) lets _insert run even though the tree is already full.
  2. Line 43VULN. nodes[newIndex] is set for the overflowing index, which aliases (overwrites) an earlier commitment's slot.
  3. Line 47 — the parent index (index/2) lands on MINIMUM_INDEX, corrupting a previously-stored commitment.

PoC#

Registry (Foundry, local deploy — exploit path + a fixed-variant control):

BASH
cd 60150-commitments-can-be-overwritten-by-overflowing-the-tree-qua_exp
forge test -vv

Expected: both tests PASS — the exploit test overflows the tree and asserts the first depositor's commitment slot was overwritten; the fixed <= guard reverts the overflowing insert. The browser EVM Playground is served at /hacks/60150-commitments-can-be-overwritten-by-overflowing-the-tree-qua/.

Remediation#

Use require(newIndex <= 2**LEVELS, "Tree is full") so the tree can never be overflowed.

References#


Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.