Reproduced Exploit

Omo: unrestricted onERC721Received locks 1000

depositPosition moves the NFT with transferFrom, so onERC721Received is not part of a real deposit. The hook still has no caller check. An attacker adds token id 2. topOffToAgent reverts on that id and the 1000 already deposited cannot leave.

Jan 2025•Other•dos•2 min read

Chain

Other

Category

dos

Date

Jan 2025

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: 53318-c-07-unrestricted-omoagentonerc721received-allows-permanent. Standalone Foundry PoC and full write-up: 53318-c-07-unrestricted-omoagentonerc721received-allows-permanent_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/dos/frozen-funds · vuln/access-control/missing-auth

Key info#

ProtocolOmo
FindingPashov Audit Group C-07, AuditVault 53318
Vulnerable codeOmoAgent.onERC721Received
HarmA fake position id makes topOffToAgent revert, so 1000 of backing stays in the agent. The fixed hook does not track the id and pays the 1000.
Chainlocal synthetic

TL;DR#

depositPosition moves the NFT with transferFrom, so onERC721Received is not part of a real deposit. The hook still has no caller check. An attacker adds token id 2. topOffToAgent reverts on that id and the 1000 already deposited cannot leave.

The vulnerable code#

SOLIDITY
            // @> VULN: any caller adds tokenId; depositPosition uses transferFrom so this hook never runs on a real deposit
            _addPositionId(tokenId);

Root cause#

The hook writes _positionIds for any caller. A later loop requires every id to have a nonzero value.

Preconditions#

The agent already holds position 1 backed by 1000. The attacker can call the hook directly.

Attack walkthrough#

  1. Deposit position 1 with 1000 of backing. See output.txt.
  2. Call onERC721Received with token id 2.
  3. _addPositionId stores id 2.
  4. positionCount is 2.
  5. topOffToAgent reverts on the unknown id.
  6. The agent balance is still 1000. The fixed hook leaves the count at 1 and pays the 1000.

Diagrams#

flowchart TD A["Deposit position 1 with 1000"] --> B["Hook stores fake id 2"] B["Hook stores fake id 2"] --> C["topOff walks both ids"] C["topOff walks both ids"] --> D["Unknown id reverts"] D["Unknown id reverts"] --> E["1000 stays in the agent"]

Impact#

The owner's 1000 cannot be withdrawn. The fixed hook returns the selector and does not record the id, so topOff pays 1000.

Remediation#

Do not call _addPositionId from onERC721Received. The deposit path already records the id.

How to reproduce#

BASH
cd 53318-c-07-unrestricted-omoagentonerc721received-allows-permanent_exp
forge test -vv

Sources & further analysis#

Reproductions & code

Alerts & third-party analyses

  • Web3Sec X hacked database: search.
  • Rekt leaderboard: search.
  • Solodit incident search: search.

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.