Reproduced Exploit

Virtuals — stale `promptMulti` cache burns and misroutes user payments

promptMulti tries to cache an agent token-bound account for consecutive identical agent IDs. It loads an address when the ID changes, but never assigns the new ID to prevAgentId. A first ID of zero therefore pays the zero address; a later zero can reuse a stale account loaded for a different ID.

Apr 2025Otherlogic3 min read

Chain

Other

Category

logic

Date

Apr 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: 61827-h-06-missing-prevagentidupdate-in-promptmulti-function-may-c. Standalone Foundry PoC and full write-up: 61827-h-06-missing-prevagentidupdate-in-promptmulti-function-may-c_exp in the evm-hack-registry mirror.


Vulnerability classes: vuln/logic/incorrect-state-transition · vuln/logic/state-update · vuln/logic/missing-check

Reproduction: self-contained Foundry PoC with no fork, RPC, or cheatcodes. Full trace: output.txt. Driver: test/61827-h-06-missing-prevagentidupdate-in-promptmulti-function-may-c_exp.sol.

AuditVault taxonomy: lang/solidity · sector/gaming · platform/code4rena · has/github · has/poc · severity/high · precondition/uninitialized · genome: lockup · permanent · data/uninitialized

Key info#

ImpactHIGH — a supplied batch can burn 10 payment units at address(0) and send another 30 units to a stale agent account.
ProtocolVirtuals
Vulnerable codeAgentInference.promptMulti
FindingCode4rena Virtuals, 2025-04 · #61827 (H-06) · reporter sergei2340
StatusAudit finding; local reduction preserves the cache invariant failure.
Compiler^0.8.24 (local reduction)

TL;DR#

promptMulti tries to cache an agent token-bound account for consecutive identical agent IDs. It loads an address when the ID changes, but never assigns the new ID to prevAgentId. A first ID of zero therefore pays the zero address; a later zero can reuse a stale account loaded for a different ID.

The local batch [0, 1, 0] with amounts [10, 20, 30] burns the first payment, then sends the third payment to agent 1’s account. The PoC asserts the zero-address loss and the unintended 50-unit balance at agent 1’s vault.

The vulnerable code#

SOLIDITY
if (prevAgentId != agentId) {
    agentTba = agentNft.getTBA(agentId);
    // @> VULN: missing `prevAgentId = agentId;`
}
token.transferFrom(msg.sender, agentTba, amount);

The cache key and cached value no longer describe the same entry after the first iteration.

Root cause#

The loop mutates the cached address but fails to update the corresponding cached identifier. The branch condition is then evaluated against an obsolete value, allowing uninitialized or stale recipient addresses to be used for transfers.

Preconditions#

  • A caller can provide at least two agent IDs and payment amounts.
  • The array contains zero first, or repeats an earlier ID after a different ID.
  • The caller has approved the payment token.

Attack walkthrough#

  1. The caller submits IDs [0, 1, 0] and amounts [10, 20, 30].
  2. The zero ID does not trigger an address load, so 10 units go to address(0).
  3. Agent 1 loads its vault and receives 20 units.
  4. The cache key remains zero; the final zero ID sends 30 more units to agent 1.

Diagrams#

flowchart TD A[Batch 0, 1, 0] --> B[ID 0 keeps zero TBA] B --> C[10 units burned] C --> D[ID 1 loads agent 1 TBA] D --> E[prevAgentId remains zero] E --> F[Final ID 0 sends 30 to stale agent 1 TBA]

Remediation#

Update the key whenever the cached address is loaded, and explicitly initialize the first iteration or reject sentinel ID zero if it is not a valid agent:

SOLIDITY
if (prevAgentId != agentId) {
    agentTba = agentNft.getTBA(agentId);
    prevAgentId = agentId;
}

How to reproduce#

BASH
cd /workspaces/RustroverProjects/audits/evm-hack-registry/61827-h-06-missing-prevagentidupdate-in-promptmulti-function-may-c_exp
forge test -vvv

Sources#

Reference: Code4rena Virtuals finding H-06, curated by AuditVault.


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.