Reproduced Exploit
LooksRare "Infiltration" — _killWoundedAgents kills a healed agent
1. _killWoundedAgents(roundId, ...) kills every agent in its list whose status == AgentStatus.Wounded — but it never checks woundedAt == roundId. 2. A player's agent is wounded at round 1. The player pays LOOKS tokens to heal it at round 3 — this succeeds, and the agent's status returns to
Chain
Other
Category
logic
Date
Oct 2023
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
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.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 27586-h-1-killwoundedagents-sherlock-looksrare-git. Standalone Foundry PoC and full write-up: 27586-h-1-killwoundedagents-sherlock-looksrare-git_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/wrong-condition · vuln/state/stale-status-check · vuln/loss-of-funds/direct-drain
Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only
forge-std— no fork, no RPC, noanvil_state. Full trace: output.txt. PoC: test/27586-h-1-killwoundedagents-sherlock-looksrare-git_exp.sol.
Key info#
| Impact | HIGH — a player who successfully heals a wounded agent (paying LOOKS tokens) still loses the agent later, wasting the heal fee and forcing them out of the game |
| Protocol | LooksRare "Infiltration" — an on-chain NFT survival game |
| Vulnerable code | Infiltration._killWoundedAgents — if (agents[index].status == AgentStatus.Wounded) { ... kill ... } |
| Bug class | Wrong condition: checks only the agent's current status, never WHEN it was wounded |
| Finding | Sherlock — LooksRare "Infiltration", 2023-10 · #27586 (H-1) · reporter cergyk (dupes: ast3ros, klaus, mstpr-brainbot) |
| Report | sherlock-audit/2023-10-looksrare-judging#21 |
| Source | AuditVault |
| Status | Audit finding — caught in review (fixed via PR #148 / PR #164), not exploited on-chain. Reproduced here as a standalone local PoC. |
| Compiler | ^0.8.24 (PoC) |
This is an audit finding, not a historical on-chain incident. The real
Infiltration game uses Chainlink VRF to draw wounds/deaths each round; the PoC
keeps the vulnerable status-only check in _killWoundedAgents verbatim
and reduces round advancement / wounding / healing to direct calls so the same
bug fires deterministically and cheatcode-free.
TL;DR#
_killWoundedAgents(roundId, ...)kills every agent in its list whosestatus == AgentStatus.Wounded— but it never checkswoundedAt == roundId.- A player's agent is wounded at round 1. The player pays LOOKS tokens to
healit at round 3 — this succeeds, and the agent's status returns toActive. - The same agent is wounded again at round 4 (an ordinary later game event, no fault of the player).
- The kill-sweep scheduled for the original wound (
roundId == 1) still runs. It checks only the agent's current status — which isWoundedagain because of the round-4 re-wound — and kills the agent anyway. - HARM: the player paid the LOOKS heal fee for nothing — the agent they successfully healed is dead regardless, and they are forced out of the game.
The vulnerable code#
The status-only check (verbatim, Infiltration._killWoundedAgents):
function _killWoundedAgents(
uint256 roundId,
uint256 currentRoundAgentsAlive
) private returns (uint256 deadAgentsCount) {
...
for (uint256 i; i < woundedAgentIdsCount; ) {
uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];
uint256 index = agentIndex(woundedAgentId);
@> if (agents[index].status == AgentStatus.Wounded) { // never checks WHEN wounded
...
}
...
}
emit Killed(roundId, woundedAgentIds);
}
The recommended fix (from the report):
- if (agents[index].status == AgentStatus.Wounded) {
+ if (agents[index].status == AgentStatus.Wounded && agents[index].woundedAt == roundId) {
Root cause#
_killWoundedAgents is supposed to kill agents that have been Wounded for
ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD rounds without being healed. Its check
conflates "is currently Wounded" with "is still wounded from the specific
round this sweep is for" — the two are the same only if an agent can be
wounded at most once in its lifetime. Because agents can be healed and then
wounded again, a wound from round R2 makes an agent look Wounded to the
kill-sweep scheduled for an unrelated, already-healed wound from round R1 <
R2, and the sweep kills it for the wrong reason.
Preconditions#
- An agent must be wounded, then healed, then wounded again before the original wound's kill-sweep round arrives — a fully ordinary sequence of game events, not an attacker action.
- No special permissions or timing manipulation are required; this can happen to any honest player through normal play.
Attack walkthrough#
From output.txt:
- The player's agent is wounded at round 1 (
woundedAt = 1). - At round 3, the player pays
HEAL_COST(100 LOOKS) tohealthe agent — the heal succeeds: status becomesActive. - At round 4, the same agent is wounded again (
woundedAt = 4). - The kill-sweep for the original wound (
roundId = 1) runs and checks onlystatus == AgentStatus.Wounded— true again because of the round-4 wound — so it sets the agent's status toDead. - HARM: the agent is dead despite the player's successful heal, and the
100 LOOKS heal fee was charged for a heal that provided zero benefit. A
control test confirms that if the agent is not wounded again after
healing, the same kill-sweep leaves it alive — isolating the bug to the
missing
woundedAt == roundIdcheck.
Diagrams#
Remediation#
Also require agents[index].woundedAt == roundId in the kill-sweep check, so
a kill-sweep only ever kills the agent for the specific wound it was scheduled
for, never for an unrelated later wound.
How to reproduce#
cd ~/RustroverProjects/audits/evm-hack-registry/27586-h-1-killwoundedagents-sherlock-looksrare-git_exp
forge test -vvv
# Fully local — no fork, no RPC, no anvil_state required.
# Expected: all 3 tests PASS:
# test_exploit (Exploit-driven harm)
# test_healThenRewound_stillDies (standalone rebuild, mirrors finding's round-based PoC)
# test_control_healWithoutRewound_survives (control: no re-wound -> agent survives)
PoC source: test/27586-h-1-killwoundedagents-sherlock-looksrare-git_exp.sol — the verbatim vulnerable status-only check plus a wound/heal/re-wound/kill sequence and a control test.
Note: the exact
ROUNDS_TO_BE_WOUNDED_BEFORE_DEADvalue and VRF-driven wound/kill scheduling are reduced-model details (the real game uses Chainlink VRF each round; out of scope here) — the reduction preserves the verbatim vulnerable check and the round-numbering semantics it depends on (woundedAtvs. the sweep'sroundId), which is the faithful part of the bug.
Reference: finding #27586 (H-1) by cergyk in the Sherlock LooksRare "Infiltration" review (Oct 2023) · curated by AuditVault
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 27586-h-1-killwoundedagents-sherlock-looksrare-git_exp (evm-hack-registry mirror).
- AuditVault finding: 27586-h-1-killwoundedagents-sherlock-looksrare-git.
Alerts & third-party analyses
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.