We do not have a reliable USD price for the recorded assets yet.
0x5a63da39b5b83fccdd825fed0226f330f802e995b8e49e19fbdd246876c67e1f0x2648f5592c09a260c601acde44e7f8f2944944fbEthereum0x2b9c2b20de30661c6e549082b48ac8b695d7f270EthereumOn Ethereum mainnet, RuggedMarket at 0x2648f5592c09a260c601acde44e7f8f2944944fb held Rugged ERC404-style NFT ids in its own custody after users called stakeNFTs(uint256[]). In seed transaction 0x5a63da39b5b83fccdd825fed0226f330f802e995b8e49e19fbdd246876c67e1f at block 19262234, an unprivileged adversary cluster used the public targetedPurchase(uint256[],UniversalRouterExecute) path to withdraw 20 selected ids from market custody, including victim-deposited id 454 and ten other ids previously deposited by third-party staker 0x2b9c2b20de30661c6e549082b48ac8b695d7f270.
The root cause is a custody-accounting failure in RuggedMarket. stakeNFTs() moves concrete NFT ids into market custody but records only fungible amountStaked units for the depositor, while _targetedPurchase() later transfers caller-selected ids out of address(this) without checking which depositor those ids belong to. As a result, user-staked inventory was sold as if it were ordinary protocol-owned inventory.
Rugged is an ERC404-style hybrid asset at 0xbe33f57f41a20b2f00dec91dcc1169597f36221f. Whole-token unit transitions map to NFT-like ids, so both fungible balance accounting and per-id ownership matter. RuggedMarket supported two public flows that matter here:
stakeNFTs(uint256[]): move selected ids from the user into market custody and credit the caller with of per id.1 etheramountStakedtargetedPurchase(uint256[],UniversalRouterExecute): swap ETH into RUG through Uniswap Universal Router and then deliver caller-selected ids from market custody for 1.1 RUG per requested id.The critical design requirement is that an NFT custody system must preserve token-id ownership semantics. A market can safely account for fungible stake amounts only if it never later exposes a path that lets arbitrary users choose specific ids held in shared custody.
This incident is an ATTACK-class ACT opportunity caused by missing token-id ownership tracking inside RuggedMarket. The verified implementation at 0xfe380fe1db07e531e3519b9ae3ea9f7888ce20c6 accepts specific ids in stakeNFTs(uint256[]) and transfers them into address(this), but the function stores only aggregate stake units in stakers[msg.sender].amountStaked. No mapping ties deposited ids back to the depositor.
That missing state becomes exploitable in targetedPurchase(uint256[],UniversalRouterExecute). After the market receives at least _tokenIds.length * 1.1 ether RUG, it calls _targetedPurchase(_tokenIds), which loops over caller-supplied ids and executes ruggedToken.transferFrom(address(this), msg.sender, _tokenIds[i]). The function checks only that each requested id is within the ERC721-style range; it performs no depositor-specific authorization and no separation between protocol inventory and user-custodied inventory.
The violated invariant is straightforward: if user U deposited NFT id n through stakeNFTs(), then another unprivileged caller V must not be able to withdraw id n unless the protocol verifies that V is authorized against U's custody record for that id. The first code-level breakpoint is _targetedPurchase, where the market transfers concrete ids from shared custody to the attacker without any such check.
The incident is fully explained by the interaction between three deterministic facts.
First, the verified RuggedMarket source shows the broken custody model:
function stakeNFTs(uint256[] memory _tokenIds) external nonReentrant {
for (uint256 i = 0; i < _tokenIds.length; i++) {
ruggedToken.transferFrom(msg.sender, address(this), _tokenIds[i]);
}
...
uint256 _amount = _tokenIds.length * 1 ether;
staker.amountStaked += _amount;
totalStaked += _amount;
}
function _targetedPurchase(uint256[] memory _tokenIds) private {
for (uint256 i = 0; i < _tokenIds.length; i++) {
ruggedToken.transferFrom(address(this), msg.sender, _tokenIds[i]);
}
marketFees += _tokenIds.length * 0.1 ether;
}
Snippet origin: verified incident-time RuggedMarket source from Sourcify.
Second, the victim deposit transaction 0x16186bdc57614a979b6d35dfbd5a53dc62fe4933b4c925519fe8f97b272034cc at block 19253430 proves that victim staker 0x2b9c2b20de30661c6e549082b48ac8b695d7f270 moved many Rugged ids into RuggedMarket custody through stakeNFTs(uint256[]). The normalized receipt includes, among others, the transfer of id 454:
{
"function_name": "stakeNFTs(uint256[])",
"from": "0x2b9c2b20de30661c6e549082b48ac8b695d7f270",
"to": "0x2648f5592c09a260c601acde44e7f8f2944944fb",
"transfer_logs": [
{
"token": "0xbe33f57f41a20b2f00dec91dcc1169597f36221f",
"from": "0x2b9c2b20de30661c6e549082b48ac8b695d7f270",
"to": "0x2648f5592c09a260c601acde44e7f8f2944944fb",
"token_id": "454"
}
]
}
Snippet origin: normalized receipt for the victim stakeNFTs() transaction.
Third, the market exposed the purchase path publicly. Transaction 0x0237660fb1f81272c19f00ffa57aeaba997c518095d946c76f24cf1e6d1a0f0c at block 19262058 shows unrelated EOA 0x6f5d2dfdb346129e3db380510d097645783ee88c successfully calling targetedPurchase(uint256[],swapParam) and receiving an id directly from market custody:
{
"function_name": "targetedPurchase(uint256[],swapParam)",
"from": "0x6f5d2dfdb346129e3db380510d097645783ee88c",
"market_related_transfers": [
{
"from": "0x2648f5592c09a260c601acde44e7f8f2944944fb",
"to": "0x6f5d2dfdb346129e3db380510d097645783ee88c",
"token_id": "7745"
}
]
}
Snippet origin: normalized receipt for a public pre-incident targeted purchase.
With that public path available, the seed exploit transaction 0x5a63da39b5b83fccdd825fed0226f330f802e995b8e49e19fbdd246876c67e1f at block 19262234 withdrew a hand-picked batch of 20 ids from RuggedMarket custody into helper contract 0x9bb0ca1e54025232e18f3874f972a851a910e9cb. The normalized receipt shows victim-deposited id 454 among the transferred ids:
{
"from": "0x2648f5592c09a260c601acde44e7f8f2944944fb",
"to": "0x9bb0ca1e54025232e18f3874f972a851a910e9cb",
"token_id": "454",
"log_index": 563
}
Snippet origin: normalized receipt for the seed exploit transaction.
The same transaction also drained nine additional ids whose provenance was reconstructed separately. The provenance artifact shows ids 9721, 5163, 2347, 3145, 2740, 1878, 4945, 7991, and 8565 entering RuggedMarket in prior helper transaction 0x77add418b562dc0c960b818ddde8bde17a41be08a185d4adb586ae086f620faa and later leaving in the seed exploit. That proves the exploit path operated over whatever ids were sitting in shared market custody, regardless of whether they originated from a third-party staker or attacker-seeded inventory.
The adversary flow has three stages.
First, the victim inventory entered market custody. In tx 0x16186bdc57614a979b6d35dfbd5a53dc62fe4933b4c925519fe8f97b272034cc, victim staker 0x2b9c2b20de30661c6e549082b48ac8b695d7f270 called stakeNFTs(uint256[]) and transferred a large set of ids, including the later-stolen id 454, into RuggedMarket.
Second, the adversary EOA 0x9733303117504c146a4e22261f2685ddb79780ef prepared additional market inventory. In tx 0x77add418b562dc0c960b818ddde8bde17a41be08a185d4adb586ae086f620faa, its earlier helper 0x7df33e3596526d1fa1de60d5f15ef75979182a45 caused nine more ids to enter RuggedMarket custody. Those ids were later withdrawn together with the victim-deposited ids.
Third, the public drain occurred. In tx 0x418ed367d6d2a512534209671d88f7f0040cabb24c067e8722cdde4a60ad5a73, the adversary deployed fresh helper 0x9bb0ca1e54025232e18f3874f972a851a910e9cb. In tx 0x5a63da39b5b83fccdd825fed0226f330f802e995b8e49e19fbdd246876c67e1f, that helper invoked RuggedMarket’s public purchase path, paid the aggregate 1.1 RUG per-id price through the embedded Universal Router swap flow, and received 20 selected ids from market custody. No owner key, role, whitelist, or victim-specific approval was needed; the only gating condition was paying the aggregate price for whichever ids were already sitting in address(this).
The adversary-related accounts are defensibly linked by on-chain activity:
0x9733303117504c146a4e22261f2685ddb79780ef: originating EOA that deployed and used the helper contracts.0x9bb0ca1e54025232e18f3874f972a851a910e9cb: fresh helper deployed immediately before the seed exploit.0x7df33e3596526d1fa1de60d5f15ef75979182a45: earlier helper used for prior inventory-seeding activity.The measurable impact in the seed transaction is the removal of 20 Rugged NFT ids from RuggedMarket custody. Eleven of those ids came from the third-party victim staker’s earlier stakeNFTs() transaction, and the remaining nine were additional ids that the same adversary had previously pushed into market custody. Under RuggedMarket’s own accounting model of 1 NFT id = 1 ether of Rugged stake units, the victim-observed loss recorded in root_cause.json is 11 RUG units, encoded as raw smallest-unit amount "11000000000000000000" with 18 decimals.
The critical effect is not merely that ids moved; it is that the victim’s positive stake accounting remained intact while custody over at least one of the victim’s deposited ids was lost. That mismatch demonstrates that RuggedMarket’s staking ledger could continue to represent a claim even after the underlying id had already been sold away through a public path.
https://repo.sourcify.dev/contracts/full_match/1/0xfe380fe1db07e531e3519b9ae3ea9f7888ce20c6/sources/src/Market.sol0x5a63da39b5b83fccdd825fed0226f330f802e995b8e49e19fbdd246876c67e1fstakeNFTs() tx: 0x16186bdc57614a979b6d35dfbd5a53dc62fe4933b4c925519fe8f97b272034cc0x77add418b562dc0c960b818ddde8bde17a41be08a185d4adb586ae086f620faa0x0237660fb1f81272c19f00ffa57aeaba997c518095d946c76f24cf1e6d1a0f0c9721, 5163, 2347, 3145, 2740, 1878, 4945, 7991, and 85650x2648f5592c09a260c601acde44e7f8f2944944fb