Reproduced Exploit
VII Finance — liquidations can be made to revert by an attacker
1. A borrower can enableTokenIdAsCollateral for a tokenId they do not hold. 2. Liquidation seizes value via transfer, which walks all enabled tokenIds. 3. normalizedToFull multiplies by totalSupply(tokenId) even when the sender's balance is 0 → tries to transfer a non-zero ERC-6909 amount → reverts.
Chain
Other
Category
untagged
Date
Jul 2025
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: 61327-liquidations-can-be-made-to-revert-by-an-attacker-through-va. Standalone Foundry PoC and full write-up: 61327-liquidations-can-be-made-to-revert-by-an-attacker-through-va_exp in the
evm-hack-registrymirror.
Vulnerability classes: liquidation-logic · fake-account-substitution · dos-resistance · access-roles
Reproduction: a self-contained Foundry PoC that compiles & runs in an isolated project with only
forge-std— no fork, no RPC. Full trace: output.txt. PoC: test/61327-liquidations-can-be-made-to-revert-by-an-attacker-through-va_exp.sol.
AuditVault taxonomy — lang/solidity · platform/cyfrin · has/poc ·
severity/high · sector/lending · sector/oracle ·
genome: fake-account-substitution · liquidation-logic · dos-resistance · access-roles · liquidation-underwater
Key info#
| Impact | HIGH — an attacker can force liquidation of their underwater account to revert, so debt cannot be cleared and bad debt accrues in the vault |
| Protocol | VII Finance — Uniswap position wrappers as EVK collateral |
| Vulnerable code | enableTokenIdAsCollateral (no balance check) + normalizedToFull using totalSupply (see #61329); fee theft (#61328) is an alternate path |
| Bug class | Liquidation DoS via unowned collateral enable + transfer inflation |
| Finding | Cyfrin 2025-07-15 vii-v2.0 · AuditVault #61327 · reporter Giovanni Di Siena |
| Report | Cyfrin vii-v2.0 |
| Source | AuditVault |
| Status | Fixed via #61328/#61329 mitigations; enable-without-balance alone becomes a zero transfer after the fix |
| Compiler | ^0.8.24 (PoC) |
TL;DR#
- A borrower can
enableTokenIdAsCollateralfor atokenIdthey do not hold. - Liquidation seizes value via
transfer, which walks all enabled tokenIds. normalizedToFullmultiplies bytotalSupply(tokenId)even when the sender's balance is 0 → tries to transfer a non-zero ERC-6909 amount → reverts.- Liquidation is permanently blocked; the underwater debt can become bad debt.
The report also documents fee-theft + transfer-inflation combinations; this PoC
reproduces the minimal enable-unowned-collateral path from
test_blockLiquidationsPoC_enableCollateral.
The vulnerable code#
function normalizedToFull(uint256 tokenId, uint256 amount, uint256 currentBalance) public view returns (uint256) {
return Math.mulDiv(amount, totalSupply(tokenId), currentBalance); // @> VULN
}
function enableTokenIdAsCollateral(uint256 tokenId) external {
// missing: require(balanceOf(msg.sender, tokenId) > 0)
_enabledTokenIds[msg.sender].push(tokenId);
}
Recommended mitigations (from the report):
- Decrement
tokensOwedon partial unwrap (#61328). - Use sender's tokenId balance in
normalizedToFull(#61329). - Consider blocking enable when the sender holds zero ERC-6909 of that tokenId.
Root cause#
Liquidation assumes every enabled tokenId is a real claim the violator can transfer. Enabling unowned ids plus totalSupply-based normalization invents a transfer amount the account cannot fund, turning a soft zero-value id into a hard revert that aborts the entire liquidation.
Preconditions#
- Attacker can wrap at least one real position and enable an unowned
tokenId. - Attacker borrows against the wrapper collateral and becomes liquidatable.
- Liquidation path calls value-denominated
wrapper.transfer.
Attack walkthrough#
- Honest user wraps tokenId1; attacker wraps tokenId2.
- Attacker enables both tokenId1 (balance 0) and tokenId2 as collateral.
- Attacker borrows; position later becomes liquidatable.
- Liquidator calls
liquidate→transfer→ for tokenId1,normalizedToFull = yield * totalSupply(1) / balis non-zero while balance is 0. - Transfer reverts with
ERC6909InsufficientBalance; debt remains.
Diagrams#
Impact#
Vault cannot liquidate the engineered position. As collateral value falls further, bad debt socializes losses onto lenders. Fee-theft variants can also strand liquidators after a "successful" partial seize (collateral unrecoverable).
Sources#
- AuditVault finding #61327
- Cyfrin 2025-07-15 vii-v2.0 report
- Related fixes: 8c6b6cc, b7549f2
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 61327-liquidations-can-be-made-to-revert-by-an-attacker-through-va_exp (evm-hack-registry mirror).
- AuditVault finding: 61327-liquidations-can-be-made-to-revert-by-an-attacker-through-va.
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.