All incidents

SuperRare RareStaking Merkle Drain

Share
Jul 28, 2025 08:21 UTCAttackLoss: 11,907,874.71 RAREPending manual check1 exploit txWindow: Atomic
Estimated Impact
11,907,874.71 RARE
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Jul 28, 2025 08:21 UTC → Jul 28, 2025 08:21 UTC

Exploit Transactions

TX 1Ethereum
0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1
Jul 28, 2025 08:21 UTCExplorer

Victim Addresses

0x3f4d749675b3e48bccd932033808a7079328eb48Ethereum
0xba5bde662c17e2adff1075610382b9b691296350Ethereum

Loss Breakdown

11,907,874.71RARE

Similar Incidents

Root Cause Analysis

SuperRare RareStaking Merkle Drain

1. Incident Overview TL;DR

At Ethereum block 23016423, EOA 0x5b9b4b4dafbcfceea7afba56958fcbb37d82d4a2 sent transaction 0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1, deployed helper contract 0x08947cedf35f9669012bda6fda9d03c399b017ab, rewrote SuperRare RareStaking's claim Merkle root, and immediately drained the staking contract's entire RARE balance. The victim staking proxy 0x3f4d749675b3e48bccd932033808a7079328eb48 lost 11907874713019104529057960 raw RARE units, and the helper contract received the full amount.

The root cause was a broken access-control check in the vulnerable RareStakingV1 implementation 0xffb512b9176d527c5d32189c3e310ed4ab2bb9ec. Its updateMerkleRoot(bytes32) function used a tautological require condition, so any caller could replace currentClaimRoot with a leaf they controlled. Because claim(uint256,bytes32[]) accepted an empty proof when the root equaled keccak256(abi.encodePacked(recipient, amount)), the attacker could forge a one-leaf tree for their own address and drain all tokens held by the staking contract.

2. Key Background

RareStakingV1 distributes claimable RARE using a Merkle root stored on-chain in currentClaimRoot. A user claims by calling claim(amount, proof), which computes leaf = keccak256(abi.encodePacked(msg.sender, amount)) and accepts the claim when MerkleProof.verify(proof, currentClaimRoot, leaf) succeeds.

That design is safe only if currentClaimRoot can be updated exclusively by trusted protocol actors. The staking proxy 0x3f4d749675b3e48bccd932033808a7079328eb48 was still running implementation 0xffb512b9176d527c5d32189c3e310ed4ab2bb9ec immediately before the exploit. Independent RPC validation at block 23016422 confirmed:

currentRound()      = 2
currentClaimRoot()  = 0x9bddda3825a4928a2bf9c0919e5179e621a7f8784dcff371d3b52d67807725b1
implementation      = 0xffb512b9176d527c5d32189c3e310ed4ab2bb9ec
RARE balance        = 11907874713019104529057960

The RARE token itself is proxy 0xba5bde662c17e2adff1075610382b9b691296350, backed by implementation 0x31acaaea0529894e7c3a5c70d3f9ee6d7804684f. The exploit did not require any bug in the token. It only relied on RareStaking being allowed to transfer the tokens it already held.

3. Vulnerability Analysis & Root Cause Summary

This incident is a direct access-control failure in the staking distribution contract. The vulnerable function was updateMerkleRoot(bytes32), which should have allowed only the contract owner or a designated authorized address to install a new claim root. Instead, the verified source used a logically inverted authorization check:

function updateMerkleRoot(bytes32 newRoot) external override {
    require(
        (msg.sender != owner() ||
            msg.sender != address(0xc2F394a45e994bc81EfF678bDE9172e10f7c8ddc)),
        "Not authorized to update merkle root"
    );
    if (newRoot == bytes32(0)) revert EmptyMerkleRoot();
    currentClaimRoot = newRoot;
    currentRound++;
    emit NewClaimRootAdded(newRoot, currentRound, block.timestamp);
}

Because the condition uses != on both addresses joined by ||, it evaluates to true for every possible caller. A sender cannot simultaneously equal both owner() and 0xc2F394..., so at least one side of the || is always true. As a result, updateMerkleRoot never rejected an unprivileged caller.

Once that guard failed, the rest of the exploit was mechanically straightforward. claim trusted the mutable currentClaimRoot, and verifyEntitled defined entitlement as keccak256(abi.encodePacked(recipient, value)). For a single-leaf Merkle tree, an empty proof is valid when the root already equals the claimant leaf. That let the attacker choose amount = rare.balanceOf(staking), compute forgedRoot = keccak256(abi.encodePacked(attacker, amount)), install that root, and then call claim(amount, []) to transfer the entire staking balance out of the contract.

4. Detailed Root Cause Analysis

The protocol-level safety invariant was: only trusted protocol actors may update currentClaimRoot, and every successful claim must derive from a protocol-authorized Merkle distribution. The code-level breakpoint was the require statement in updateMerkleRoot, which encoded the intended authorization condition incorrectly and therefore admitted all callers.

The verified implementation also shows why the forged root directly enabled a drain:

function verifyEntitled(
    address recipient,
    uint256 value,
    bytes32[] memory proof
) public view override returns (bool) {
    bytes32 leaf = keccak256(abi.encodePacked(recipient, value));
    return verifyProof(leaf, proof);
}

function verifyProof(
    bytes32 leaf,
    bytes32[] memory proof
) internal view returns (bool) {
    return MerkleProof.verify(proof, currentClaimRoot, leaf);
}

And claim transferred tokens directly from the staking contract's own balance:

lastClaimedRound[_msgSender()] = currentRound;
_token.safeTransfer(_msgSender(), amount);
emit TokensClaimed(currentClaimRoot, _msgSender(), amount, currentRound);

The exploit trace for transaction 0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1 shows the two decisive calls:

RareStakingV1::updateMerkleRoot(0x93f3c0d0d71a7c606fe87524887594a106b44c65d46fa72a42d80bd6259ade7e)
  storage slot 0: 0x9bdd...25b1 -> 0x93f3...de7e
  currentRound: 2 -> 3

RareStakingV1::claim(11907874713019104529057960, [])
  emit Transfer(from: 0x3f4d749675b3e48bccd932033808a7079328eb48,
                to:   0x08947cedf35f9669012bda6fda9d03c399b017ab,
                value: 11907874713019104529057960)

The forged root was not arbitrary. It matched the helper contract and full drain amount exactly:

keccak256(abi.encodePacked(
  0x08947cedf35f9669012bda6fda9d03c399b017ab,
  11907874713019104529057960
)) =
0x93f3c0d0d71a7c606fe87524887594a106b44c65d46fa72a42d80bd6259ade7e

That is the same root written on-chain in the exploit trace. With currentClaimRoot set to that value, claim(amount, []) became valid for the attacker's helper contract without any legitimate Merkle distributor involvement.

5. Adversary Flow Analysis

The adversary flow was a single-transaction ACT sequence:

  1. Observe the pre-state at block 23016422: the staking proxy still used vulnerable implementation 0xffb512...b9ec, held 11907874713019104529057960 RARE, and had currentRound = 2.
  2. Send transaction 0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1 from EOA 0x5b9b4b4dafbcfceea7afba56958fcbb37d82d4a2.
  3. Deploy helper contract 0x08947cedf35f9669012bda6fda9d03c399b017ab inside that transaction.
  4. From the helper, call updateMerkleRoot(0x93f3c0d0d71a7c606fe87524887594a106b44c65d46fa72a42d80bd6259ade7e) on the staking proxy. The delegatecall into RareStakingV1 increments currentRound to 3 and stores the forged root.
  5. From the same helper, call claim(11907874713019104529057960, []). Because the forged root equals the helper's leaf for the full staking balance, the empty proof verifies and the transfer succeeds.
  6. Finish the transaction with the helper holding all RARE previously held by the staking contract.

The balance-diff artifact confirms the economic result. The staking proxy's RARE balance moved from 11907874713019104529057960 to 0, and helper 0x08947cedf35f9669012bda6fda9d03c399b017ab moved from 0 to 11907874713019104529057960. The sender EOA paid gas in ETH only; the profit was realized entirely in RARE.

6. Impact & Losses

The measurable loss was the full RARE balance held by the RareStaking proxy at exploit time:

{
  "token_symbol": "RARE",
  "amount": "11907874713019104529057960",
  "decimal": 18
}

At token precision 18, that corresponds to 11907874.713019104529057960 RARE. The impact was not limited to one claim. While the vulnerable implementation remained deployed, any unprivileged actor could replace the Merkle root and manufacture arbitrary claims against the staking contract's balance.

7. References

  1. Exploit transaction 0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1 metadata and trace.
  2. Balance diff for 0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1, showing the RARE transfer from staking proxy to helper contract.
  3. Verified RareStakingV1 implementation source at 0xffb512b9176d527c5d32189c3e310ed4ab2bb9ec.
  4. Verified SuperRareToken implementation source at 0x31acaaea0529894e7c3a5c70d3f9ee6d7804684f.
  5. Independent RPC reads at block 23016422 confirming the pre-state and proxy implementation.