IdolMain Reward Double-Claim
Exploit Transactions
Victim Addresses
0x439cac149b935ae1d726569800972e1669d17094Ethereum0xae7ab96520de3a18e5e111b5eaab095312d7fe84EthereumLoss Breakdown
Similar Incidents
Orion Pool Double-Count Exploit
34%PointFarm Reward Reentrancy
34%EHIVE Reward Inflation
33%AirdropGrapesToken ApeCoin Claim via NFTX BAYC Vault
33%DePay Router Double-Plugin Drain
31%Peapods Public Reward Swap
31%Root Cause Analysis
IdolMain Reward Double-Claim
1. Incident Overview TL;DR
An ACT attacker exploited IdolMain on Ethereum by moving a single Idol NFT into a future CREATE address, deploying a helper contract at that address, and repeatedly self-transferring the NFT to claim the same stETH reward snapshot multiple times. In seed transaction 0x5e989304b1fb61ea0652db4d0f9476b8882f27191c1f1d2841f8977cb8c5284c, the helper contract ultimately transferred 12950523927926154327 wei of stETH to adversary-controlled recipient 0x8152970a81f558d171a22390e298b34be8d40cf4.
The root cause is a logic bug in IdolMain::_beforeTokenTransfer. When _from == _to and the holder owns exactly one NFT, the function first claims rewards for _from, then deletes claimedSnapshots[_from], and then claims again for _to, which is the same address. That reopens the same rewardPerGod unit and makes the reward claim non-idempotent under self-transfer.
2. Key Background
IdolMain is an ERC721-like contract that also distributes stETH rewards to NFT holders. Reward accounting is driven by the global variable rewardPerGod, while each holder's claimed position is stored in claimedSnapshots[address]. A holder's pending reward is derived from its NFT balance multiplied by the difference between rewardPerGod and its claimed snapshot.
The transfer hook is security-critical because IdolMain performs reward settlement inside _beforeTokenTransfer, before the ownership transition is finalized. That means the hook must be correct for both ordinary transfers and edge cases such as self-transfer.
This incident also depends on a standard EVM property: an address that will later receive code via CREATE can already receive ERC721 tokens before deployment. The adversary used that property to preload the NFT into the future exploit contract address and then deployed code there in a later transaction.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability class is an application logic bug in transfer-time reward accounting. IdolMain assumes the sender and receiver of a transfer are distinct holders, but that assumption is false when a holder self-transfers an NFT. For a holder with exactly one Idol, _claimEthRewards(_from) updates claimedSnapshots[_from] to the current rewardPerGod, after which the hook immediately deletes the same snapshot because balanceOf(_from) == 1. If the transfer is a self-transfer, the receiver branch then treats the same address as an eligible recipient and calls _claimEthRewards(_to) again. Because the snapshot was just deleted, the second claim re-computes the same reward chunk as unclaimed and transfers stETH again.
The relevant victim-side code path is:
function _beforeTokenTransfer(address _from, address _to, uint256 _tokenId)
internal
virtual
override
onlyAllowedContracts(_to)
{
super._beforeTokenTransfer(_from, _to, _tokenId);
if (_from != address(0x0)) {
_claimEthRewards(_from);
if (balanceOf(_from) == 1) {
delete claimedSnapshots[_from];
}
}
if (balanceOf(_to) > 0) {
_claimEthRewards(_to);
} else {
claimedSnapshots[_to] = rewardPerGod;
}
}
function _claimEthRewards(address _user) internal nonReentrant {
uint256 currentRewards = getPendingStethReward(_user);
if (currentRewards > 0) {
allocatedStethRewards = allocatedStethRewards - currentRewards;
claimedSnapshots[_user] = rewardPerGod;
require(steth.transfer(_user, currentRewards));
}
}
The violated invariant is: a holder with one Idol must not be able to realize the same rewardPerGod increment more than once per owned NFT. The concrete breakpoint is the deletion of claimedSnapshots[_from] in the same self-transfer execution that later calls _claimEthRewards(_to) for the same address.
4. Detailed Root Cause Analysis
The ACT opportunity existed before block 21624237. At that point Idol #940 was owned by 0xe546480138d50bb841b204691c39cc514858d101, IdolMain.rewardPerGod() was already non-zero, and the future CREATE address 0x22d22134612c0741ebdb3b74a58842d6e74e3b16 had not yet been deployed.
The prerequisite transaction was 0xeb95b7a106c3379b38881a8d99b65a0319a28da2275eacb53f47aa1e7f63ab66 in block 21624237. Its calldata invokes IdolMain.transferFrom(0xe546..., 0x22d221..., 940), and the receipt contains the matching ERC721 Transfer event proving that Idol #940 moved into the future CREATE address before any code existed there.
input: 0x23b872dd...e546480138d50bb841b204691c39cc514858d101...22d22134612c0741ebdb3b74a58842d6e74e3b16...03ac
log Transfer(from: 0xe546480138d50bb841b204691c39cc514858d101,
to: 0x22d22134612c0741ebdb3b74a58842d6e74e3b16,
tokenId: 940)
Three blocks later, transaction 0x5e989304b1fb61ea0652db4d0f9476b8882f27191c1f1d2841f8977cb8c5284c deployed code at that prepared address. Because the NFT had already been transferred there, the constructor immediately controlled a single Idol and could execute the vulnerable self-transfer loop.
The seed exploit trace shows repeated calls of:
IdolMain::safeTransferFrom(
0x22d22134612C0741EBDb3B74a58842D6E74E3b16,
0x22d22134612C0741EBDb3B74a58842D6E74E3b16,
940
)
Each iteration emits a stETH transfer from IdolMain to the same exploit contract for one rewardPerGod chunk:
emit Transfer(
from: IdolMain: [0x439cac149B935AE1D726569800972E1669d17094],
to: 0x22d22134612C0741EBDb3B74a58842D6E74E3b16,
value: 31056412297185023
)
That trace behavior is exactly what the source-level bug predicts: the same address reclaims the same reward window because the snapshot is deleted and then recomputed during a no-op ownership transition.
Near the end of the same trace, the exploit realizes profit by forwarding the accumulated stETH to the adversary-controlled recipient:
Lido::balanceOf(0x22d22134612C0741EBDb3B74a58842D6E74E3b16) => 12950523927926154327
Lido::transfer(0x8152970a81f558d171a22390E298B34Be8d40CF4, 12950523927926154327)
emit Transfer(from: 0x22d22134612C0741EBDb3B74a58842D6E74E3b16,
to: 0x8152970a81f558d171a22390E298B34Be8d40CF4,
value: 12950523927926154327)
The exploit contract then returns Idol #940 to 0xe546480138d50bb841b204691c39cc514858d101, showing that the value extraction came from repeated reward claims rather than permanent seizure of the NFT.
5. Adversary Flow Analysis
The adversary flow has two transactions and no privileged prerequisites beyond owning one unlocked Idol NFT.
- In tx
0xeb95b7a106c3379b38881a8d99b65a0319a28da2275eacb53f47aa1e7f63ab66, EOA0xe546480138d50bb841b204691c39cc514858d101transfers Idol#940into future CREATE address0x22d22134612c0741ebdb3b74a58842d6e74e3b16. - In tx
0x5e989304b1fb61ea0652db4d0f9476b8882f27191c1f1d2841f8977cb8c5284c, the same EOA deploys a helper contract at that address. - The freshly deployed contract verifies it owns exactly one Idol and repeatedly executes
safeTransferFrom(address(this), address(this), 940)whileallocatedStethRewardsremains at least onerewardPerGodunit. - Each self-transfer reopens the same reward snapshot and causes another stETH transfer from IdolMain into the helper contract.
- After the loop, the helper transfers the accumulated stETH to recipient
0x8152970a81f558d171a22390e298b34be8d40cf4. - The helper returns Idol
#940to the original EOA, leaving the exploit contract with no meaningful retained stETH balance.
The identified adversary-related accounts are:
0xe546480138d50bb841b204691c39cc514858d101: deployer EOA and prerequisite NFT sender.0x22d22134612c0741ebdb3b74a58842d6e74e3b16: future CREATE address and exploit helper contract.0x8152970a81f558d171a22390e298b34be8d40cf4: stETH profit recipient hard-coded by the exploit.
The public victim components are:
- IdolMain at
0x439cac149b935ae1d726569800972e1669d17094 - stETH at
0xae7ab96520de3a18e5e111b5eaab095312d7fe84
6. Impact & Losses
The seed exploit transaction transferred 12950523927926154327 wei of stETH to the adversary-controlled profit recipient. Expressed with 18 decimals, that is approximately 12.950523927926154327 stETH.
This is more than a one-off accounting mistake for a single holder. Because the defect is permissionless and depends only on controlling one unlocked Idol while rewards are funded, any qualifying holder could repeatedly consume shared reward inventory intended for all Idol holders. The vulnerability therefore undermines the correctness of the protocol's reward distribution model and can drain allocatedStethRewards in reward-sized chunks until the remaining inventory falls below one rewardPerGod unit.
7. References
- Verified IdolMain source at
0x439cac149b935ae1d726569800972e1669d17094, especially_beforeTokenTransferand_claimEthRewards. - Prerequisite transaction
0xeb95b7a106c3379b38881a8d99b65a0319a28da2275eacb53f47aa1e7f63ab66and its receipt, proving transfer of Idol#940into the future CREATE address. - Seed exploit transaction
0x5e989304b1fb61ea0652db4d0f9476b8882f27191c1f1d2841f8977cb8c5284cmetadata and full execution trace, proving repeated self-transfers, repeated stETH payouts, profit forwarding, and NFT return. - Root cause validation result in
artifacts/validator/root_cause_challenge_result.json, which independently confirms the exploit mechanism and the corrected trace-backed profit amount.