SuperRare RareStaking Merkle Drain
Exploit Transactions
0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1Victim Addresses
0x3f4d749675b3e48bccd932033808a7079328eb48Ethereum0xba5bde662c17e2adff1075610382b9b691296350EthereumLoss Breakdown
Similar Incidents
RubicProxy USDC Drain
33%USDTStaking Approval Drain
32%OxODex Stale Withdrawal Drain
31%Hypr Bridge Reinit Drain
31%Minimal-Proxy Pool Reinitializer Drain
31%Dexible selfSwap allowance drain
30%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:
- Observe the pre-state at block
23016422: the staking proxy still used vulnerable implementation0xffb512...b9ec, held11907874713019104529057960RARE, and hadcurrentRound = 2. - Send transaction
0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1from EOA0x5b9b4b4dafbcfceea7afba56958fcbb37d82d4a2. - Deploy helper contract
0x08947cedf35f9669012bda6fda9d03c399b017abinside that transaction. - From the helper, call
updateMerkleRoot(0x93f3c0d0d71a7c606fe87524887594a106b44c65d46fa72a42d80bd6259ade7e)on the staking proxy. The delegatecall into RareStakingV1 incrementscurrentRoundto3and stores the forged root. - 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. - 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
- Exploit transaction
0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1metadata and trace. - Balance diff for
0xd813751bfb98a51912b8394b5856ae4515be6a9c6e5583e06b41d9255ba6e3c1, showing the RARE transfer from staking proxy to helper contract. - Verified RareStakingV1 implementation source at
0xffb512b9176d527c5d32189c3e310ed4ab2bb9ec. - Verified SuperRareToken implementation source at
0x31acaaea0529894e7c3a5c70d3f9ee6d7804684f. - Independent RPC reads at block
23016422confirming the pre-state and proxy implementation.