Reproduced Exploit
Frankencoin — Position owners can deny liquidations (unbounded price overflow)
1. A position owner can set the liquidation price arbitrarily high via adjustPrice (or the opening _liqPrice) — there is no upper bound. 2. Every challenge-resolution path multiplies price by a collateral amount: price * _collateralAmount in tryAvertChallenge, and _mulD18(price, _size)
Chain
Other
Category
arithmetic
Date
Apr 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: 20020-h-05-position-owners-can-deny-liquidations-code4rena-franken. Standalone Foundry PoC and full write-up: 20020-h-05-position-owners-can-deny-liquidations-code4rena-franken_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/arithmetic/overflow-dos · vuln/access-control/unbounded-parameter · vuln/loss-of-funds/frozen-funds
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/20020-h-05-position-owners-can-deny-liquidations-code4rena-franken.sol.
Key info#
| Impact | HIGH — a position owner sets the liquidation price to type(uint256).max, overflowing every challenge-resolution multiplication. No bid can avert the challenge and MintingHub.end can never settle it, so the challenger's escrowed collateral is locked in the hub forever and no reward is paid |
| Protocol | Frankencoin — a collateralized, decentralized stablecoin (ZCHF) with an auction-based liquidation mechanism |
| Vulnerable code | Position.adjustPrice (unbounded price) → price * _collateralAmount in tryAvertChallenge and _mulD18(price, _size) in notifyChallengeSucceeded both overflow |
| Bug class | Unbounded owner-set parameter → checked-arithmetic overflow → permanent denial of liquidation (funds frozen) |
| Finding | Code4rena — Frankencoin, 2023-04 · #20020 · reporter JGcarv |
| Report | code4rena.com/reports/2023-04-frankencoin |
| Source | AuditVault |
| Status | Audit finding — caught in review (not exploited on-chain). Confirmed by the Frankencoin team ("Ouch, this is a good one.") and judged HIGH. Reproduced here as a standalone local PoC. |
| Compiler | ^0.8.24 (PoC) |
This is an audit finding, not a historical on-chain incident. The PoC keeps the
three blamed lines verbatim: the unbounded price = newPrice in adjustPrice, and
the two overflowing multiplications in tryAvertChallenge and
notifyChallengeSucceeded.
TL;DR#
- A position owner can set the liquidation
pricearbitrarily high viaadjustPrice(or the opening_liqPrice) — there is no upper bound. - Every challenge-resolution path multiplies
priceby a collateral amount:price * _collateralAmountintryAvertChallenge, and_mulD18(price, _size)(i.e.price * _size / 1e18) innotifyChallengeSucceeded. - With
price = type(uint256).maxthose products overflow, and Solidity 0.8 checked arithmetic reverts. - So no bid can ever avert the challenge, and
MintingHub.endcan never settle it. The collateral a challenger escrowed when launching the challenge is locked in the hub forever, and the challenger gets no reward. - This denies liquidation entirely: the owner keeps their minted ZCHF and simply abandons the (now worthless) collateral, pushing the loss onto challengers.
The vulnerable code#
The unbounded price (verbatim, Position.adjustPrice):
function adjustPrice(uint256 newPrice) public onlyOwner noChallenge {
if (newPrice > price) {
restrictMinting(3 days);
} else {
checkCollateral(collateralBalance(), newPrice);
}
price = newPrice; // @> no upper bound: can be type(uint256).max
emitUpdate();
}
The averting-path overflow (verbatim, Position.tryAvertChallenge):
} else if (_bidAmountZCHF * ONE_DEC18 >= price * _collateralAmount){ // @> price * amount overflows
The settlement-path overflow (verbatim, Position.notifyChallengeSucceeded):
uint256 volumeZCHF = _mulD18(price, _size); // @> _mulD18 == price * _size / 1e18 -> price * _size overflows
Root cause#
The liquidation price is an owner-controlled value with no maximum, yet it is used
as a multiplicand in checked arithmetic across every path that resolves a
challenge. Setting it to type(uint256).max turns each of those multiplications
into a guaranteed revert, so the challenge mechanism — the protocol's only tool for
liquidating an under-collateralized position — becomes permanently inoperative for
that position.
Preconditions#
- Anyone can open a position and raise its price via
adjustPrice(owner-only, but the owner is the attacker) — permissionless. - The position is not expired (so
tryAvertChallengereaches the overflowing comparison rather than the "expired" early return). - A challenger has escrowed collateral against the position (the value that becomes locked).
Attack walkthrough#
From output.txt (two positions are used only because the browser PoC
cannot advance time — one with a zero challenge period so end is callable
immediately, one with an open window so a bid can be attempted):
- The owner reprices both positions to
type(uint256).maxviaadjustPrice. - A challenger escrows
1e18collateral against each (launchChallengepulls it into the hub). endis called on the zero-period challenge. It first tries to return the escrow, then callsnotifyChallengeSucceeded, where_mulD18(price, _size)overflows — reverting the wholeendcall and rolling back the return.- A
bidis attempted on the open challenge;tryAvertChallengeoverflows atprice * _collateralAmount, so no bid can ever avert it. - HARM: both resolution paths revert permanently, and the challenger's full
2e18escrow stays trapped in the hub with no way out. A control assertion shows that with a sane price the sameendflow settles and returns the collateral — isolating the unbounded price as the root cause.
Diagrams#
Remediation#
Enforce an upper bound on the liquidation price (and/or the per-adjustment change),
and restrict when a repriced position can be challenged so that a challenge can only
target a position that was once validly priced. This prevents an owner from moving
price into the range where the challenge-resolution arithmetic overflows.
How to reproduce#
cd ~/RustroverProjects/audits/evm-hack-registry/20020-h-05-position-owners-can-deny-liquidations-code4rena-franken_exp
forge test -vvv
# Fully local — no fork, no RPC, no anvil_state required.
# Expected: both tests PASS:
# test_maxPrice_locks_challenger_collateral (attack: end + bid both revert, escrow locked)
# test_sanePrice_end_settles_and_returns_collateral (control: sane price -> end settles)
PoC source: test/20020-h-05-position-owners-can-deny-liquidations-code4rena-franken.sol
— the verbatim adjustPrice, tryAvertChallenge, and notifyChallengeSucceeded
lines plus the deny-liquidation attack and a control test.
Note: two positions and specific challenge sizes are reduced-model scaffolding chosen so the harm is observable without advancing time in-browser. The three blamed lines and the overflow-on-max-price behavior are faithful; the exact
2e18locked magnitude validates the model. The lock (challenger collateral frozen, liquidation denied) is the impact accepted by the judge.
Reference: finding #20020 by JGcarv in the Code4rena Frankencoin audit (Apr 2023) · curated by AuditVault
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 20020-h-05-position-owners-can-deny-liquidations-code4rena-franken_exp (evm-hack-registry mirror).
- AuditVault finding: 20020-h-05-position-owners-can-deny-liquidations-code4rena-franken.
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.