Reproduced Exploit
VII Finance — more value extracted by liquidations than expected (`normalizedToFull`)
1. Liquidation / value transfer walks every enabled tokenId and converts a unit-of-account amount into an ERC-6909 amount via normalizedToFull. 2. normalizedToFull multiplies by totalSupply(tokenId) instead of the sender's own ERC-6909 balance of that tokenId.
Chain
Other
Category
logic
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: 61329-more-value-can-be-extracted-by-liquidations-than-expected-du. Standalone Foundry PoC and full write-up: 61329-more-value-can-be-extracted-by-liquidations-than-expected-du_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/logic/liquidation-logic · liquidation-manipulation · integer-bounds
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/61329-more-value-can-be-extracted-by-liquidations-than-expected-du_exp.sol.
AuditVault taxonomy — lang/solidity · platform/cyfrin · has/poc ·
severity/high · sector/dex · sector/lending · sector/nft ·
genome: liquidation-logic · liquidation-manipulation · variant · integer-bounds · liquidation-underwater
Key info#
| Impact | HIGH — value-denominated collateral transfers (used in liquidations) seize more unit-of-account value than requested when the violator owns less than 100% of any enabled ERC-6909 tokenId |
| Protocol | VII Finance — Uniswap V3/V4 position wrappers as EVK collateral |
| Vulnerable code | ERC721WrapperBase.normalizedToFull — multiplies by totalSupply(tokenId) instead of the sender's balance |
| Bug class | Incorrect normalization in liquidation share transfer |
| Finding | Cyfrin 2025-07-15 vii-v2.0 · AuditVault #61329 · reporter Giovanni Di Siena |
| Report | Cyfrin vii-v2.0 |
| Source | AuditVault |
| Status | Fixed in commit b7549f2 |
| Compiler | ^0.8.24 (PoC) |
TL;DR#
- Liquidation / value transfer walks every enabled
tokenIdand converts a unit-of-account amount into an ERC-6909 amount vianormalizedToFull. normalizedToFullmultiplies bytotalSupply(tokenId)instead of the sender's own ERC-6909 balance of thattokenId.- When the violator owns less than 100% of any
tokenId, each leg of the transfer is inflated; the liquidator receives more value than requested. - Fix: multiply by
balanceOf(sender, tokenId).
The vulnerable code#
function normalizedToFull(uint256 tokenId, uint256 amount, uint256 currentBalance) public view returns (uint256) {
// @audit => multiplying by the total ERC-6909 supply of the specified tokenId is incorrect
return Math.mulDiv(amount, totalSupply(tokenId), currentBalance); // @> VULN
}
Recommended fix:
- return Math.mulDiv(amount, totalSupply(tokenId), currentBalance);
+ return Math.mulDiv(amount, balanceOf(_msgSender(), tokenId), currentBalance);
Root cause#
balanceOf(sender) is the sum of unit-of-account values across enabled
tokenIds. For each tokenId the code must convert amount / currentBalance into
a fraction of the sender's claim on that tokenId. Using totalSupply treats
the sender as if they owned 100% of every tokenId, so partial ownership inflates
the ERC-6909 amount transferred on every other leg as well.
Preconditions#
- Sender has enabled multiple tokenIds (or one partially owned tokenId).
- Sender owns less than 100% of at least one enabled tokenId (prior partial transfer / partial liquidation / shared position).
- A value-denominated
transfer(to, amount)is executed (liquidation path).
Attack walkthrough#
- Borrower wraps two positions (tokenId1, tokenId2), full supply 100 each.
- Borrower transfers half of tokenId1 to another account → owns 50 + 100 = 150 value.
- Liquidator requests transfer of 75 (half of remaining value).
- Bug: tokenId1 transfers
75 * 100 / 150 = 50(all remaining); tokenId2 transfers75 * 100 / 150 = 50→ 100 value seized vs 75 requested (+25 surplus). - Correct formula would transfer 25 + 50 = 75.
Diagrams#
Impact#
Liquidated accounts incur larger losses than the liquidation parameters specify. Repeated partial liquidations amplify the surplus extraction.
Sources#
- AuditVault finding #61329
- Cyfrin 2025-07-15 vii-v2.0 report
- Vulnerable source: kankodu/vii-finance-smart-contracts —
ERC721WrapperBase.normalizedToFull(fixed in b7549f2)
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 61329-more-value-can-be-extracted-by-liquidations-than-expected-du_exp (evm-hack-registry mirror).
- AuditVault finding: 61329-more-value-can-be-extracted-by-liquidations-than-expected-du.
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.