Reproduced Exploit
Polynomial Protocol — missing totalFunds update in LiquidityPool.openShort shortchanges LP holders
1. openShort charges the trader a fee by paying out only totalCost = tradeCost - fees, so the pool keeps the fee. 2. Of that fee, externalFee goes to the dev/hedge and the remainder (feesCollected - externalFee) is the pool's earned revenue for LPs.
Chain
Other
Category
accounting
Date
Mar 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, 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: 20230-h-07-missing-totalfunds-update-in-liquiditypools-openshort-c. Standalone Foundry PoC and full write-up: 20230-h-07-missing-totalfunds-update-in-liquiditypools-openshort-c_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/accounting/fee-accounting · vuln/logic/missing-state-update · 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/20230-h-07-missing-totalfunds-update-in-liquiditypools-openshort-c_exp.sol.
Key info#
| Impact | HIGH — openShort earns the pool a net trading fee it keeps as sUSD but never credits to totalFunds, so LP token price is under-stated and LP holders never receive the fee revenue they earned |
| Protocol | Polynomial Protocol — LiquidityPool |
| Vulnerable code | LiquidityPool.openShort — retains the net fee but is missing totalFunds += feesCollected - externalFee; |
| Bug class | Missing state update: earned revenue not reflected in fund accounting |
| Finding | Code4rena — Polynomial Protocol, 2023-03 · #20230 · reporter auditor0517 |
| Report | code4rena.com/reports/2023-03-polynomial |
| Source | AuditVault |
| Status | Audit finding — disputed then confirmed by the sponsor, judged HIGH (loss of funds for users). Reproduced here as a standalone local PoC. |
| Compiler | ^0.8.24 (PoC) |
This is an audit finding, not a historical on-chain incident. The upstream
Code4rena source repo has been taken down, so the reduced PoC preserves the
vulnerable openShort fee handling verbatim and models the LiquidityPool's
fund/price accounting minimally (the short's principal is value-neutral at
open) so the "LP token value lost" harm becomes a checkable shortfall.
TL;DR#
openShortcharges the trader a fee by paying out onlytotalCost = tradeCost - fees, so the pool keeps the fee.- Of that fee,
externalFeegoes to the dev/hedge and the remainder (feesCollected - externalFee) is the pool's earned revenue for LPs. openShortis missingtotalFunds += feesCollected - externalFee;, sototalFunds— which drivesgetTokenPrice— never grows with the fee.- The pool's real sUSD balance rises by the net fee while its accounting does
not.
availableFundsunderstates the balance and the LP token price is depressed. LP holders never receive the fee they earned. - In the PoC an LP deposits
50e18; a short earns the pool a1.8e18net fee; the LP redeems and gets back only its50e18principal — the1.8e18fee is stuck, uncredited, in the pool.
The vulnerable code#
LiquidityPool.openShort — the trader pays the fee via a deduction of tradeCost
(verbatim), and the net fee is kept but never booked:
totalCost = tradeCost - fees;
SUSD.safeTransfer(user, totalCost); // @> pool retains (feesCollected - externalFee) here
// MISSING: totalFunds += feesCollected - externalFee;
Root cause#
openShort performs the cash side of collecting a fee (it keeps sUSD) but omits
the accounting side (totalFunds += net fee). totalFunds is the input to
getTokenPrice, so the LP token price ignores the earned fee. The revenue sits
in the contract's balance, invisible to and unclaimable by the LP holders whose
capital backed the trade.
Preconditions#
- Any short is opened (a normal, permissionless trader action). Each open leaves another slice of net fee uncredited.
Attack walkthrough#
From output.txt:
- An LP deposits
50e18;totalFunds = 50e18, price1e18, pool holds50e18sUSD. - A trader opens a
100e18short. The pool charges2e18fees, forwards0.2e18to the dev, and keeps the1.8e18net fee — buttotalFundsstays50e18(the missing update). - HARM (accounting): the pool now holds
51.8e18sUSD whileavailableFunds = totalFunds - usedFunds = 50e18; the accounting understates the balance by exactly the1.8e18net fee.getTokenPricestays at1e18instead of rising to(50+1.8)/50. - HARM (loss): the LP redeems and receives only its
50e18principal — not the1.8e18fee it earned. The net fee remains locked in the pool with no LP tokens left to claim it.
Diagrams#
Impact#
Every short opened credits the pool with a net fee that is never distributed to LP holders: their token price is depressed and, on redemption, they receive less than the fee-inclusive value their capital earned. Over many trades this is a persistent, compounding loss of LP token value. The judge kept it HIGH as a loss of funds for users.
Remediation#
Book the earned fee:
totalCost = tradeCost - fees;
SUSD.safeTransfer(user, totalCost);
+ totalFunds += feesCollected - externalFee;
How to reproduce#
cd ~/RustroverProjects/audits/evm-hack-registry/20230-h-07-missing-totalfunds-update-in-liquiditypools-openshort-c_exp
forge test -vvv
# Fully local — no fork, no RPC, no anvil_state required.
# Expected: test_missingTotalFundsUpdate PASSES (LP redeems only its 50e18
# principal; the 1.8e18 net fee is stuck in the pool).
PoC source: test/20230-h-07-missing-totalfunds-update-in-liquiditypools-openshort-c_exp.sol
— drives the verbatim vulnerable openShort fee handling and re-asserts the LP
shortfall.
Note: the short principal is modeled as value-neutral at open (collateral offsets the obligation) and getTokenPrice is a reduced form of the pool's price function; both isolate the fee-accounting bug. The vulnerable
openShortfee handling and the missingtotalFunds += feesCollected - externalFeeupdate are faithful, as is the "LP never receives the earned fee" harm.
Sources#
- AuditVault finding: 20230-h-07-missing-totalfunds-update-in-liquiditypools-openshort-c.md
- Contest report: Code4rena — Polynomial Protocol (2023-03)
- Reduced-source provenance: vulnerable
LiquidityPool.openShortfee handling (totalCost = tradeCost - fees; SUSD.safeTransfer(user, totalCost);) and the recommendedtotalFunds += feesCollected - externalFee;fix reconstructed verbatim from the AuditVault finding and the merged contest reportcode-423n4/2023-03-polynomial-findings@main. The original contest source repocode-423n4/2023-03-polynomialhas been taken down.
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 20230-h-07-missing-totalfunds-update-in-liquiditypools-openshort-c_exp (evm-hack-registry mirror).
- AuditVault finding: 20230-h-07-missing-totalfunds-update-in-liquiditypools-openshort-c.
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.