Reproduced Exploit
Phi — Reentrancy in creating Creds allows an attacker to steal all Ether from the Cred contract
1. createCred writes creds[credIdCounter], then buyShareCred (auto-buy 1 share). 2. buyShareCred refunds excess ETH via an external call before credIdCounter is incremented — and there is no nonReentrant guard. 3. On the refund the attacker buys many more shares on the cheap curve, then
Chain
Other
Category
reentrancy
Date
Aug 2024
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: 43397-h-06-reentrancy-in-creating-creds-allows-an-attacker-to-stea. Standalone Foundry PoC and full write-up: 43397-h-06-reentrancy-in-creating-creds-allows-an-attacker-to-stea_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/reentrancy/single-function · fix/use-reentrancy-guard · novelty/known-pattern
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/43397-h-06-reentrancy-in-creating-creds-allows-an-attacker-to-stea_exp.sol.
AuditVault taxonomy: lang/solidity · sector/dex · sector/launchpad · sector/lending · platform/code4rena · has/github · has/poc · severity/high · genome: single-function · use-reentrancy-guard · known-pattern · direct-drain · flashloan-callback-auth · reentrancy-guard · timestamp-dependence
Key info#
| Impact | HIGH — with two or more whitelisted bonding curves, reentrancy during createCred lets an attacker buy shares cheap, overwrite the cred's curve, sell expensive, and drain nearly all Cred ETH |
| Protocol | Phi — Cred share trading / cred creation |
| Vulnerable code | Cred._createCredInternal / buyShareCred excess-ETH refund (src/Cred.sol) — no reentrancy guard; counter incremented after refund |
| Bug class | Reentrancy + state overwrite before counter increment |
| Finding | Code4rena — Phi, 2024-08 · #43397 (H-06) · reporter rscodes (public report features CAUsr impact) |
| Report | 2024-08-phi |
| Source | AuditVault |
| Status | Audit finding — confirmed by Phi. 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 create-then-buy-then-increment structure and the excess-ETH refund reentrancy surface verbatim in spirit (signature checks omitted — not relevant to the reentrancy).
TL;DR#
createCredwritescreds[credIdCounter], thenbuyShareCred(auto-buy 1 share).buyShareCredrefunds excess ETH via an external call beforecredIdCounteris incremented — and there is nononReentrantguard.- On the refund the attacker buys many more shares on the cheap curve, then
reenters
createCredwith an expensive curve, overwriting the samecreds[credIdCounter]slot (counter still unchanged). - Attacker sells the cheaply-bought shares against the expensive curve and drains protocol ETH.
The vulnerable code#
function _createCredInternal(address creator_, address bondingCurve_) internal returns (uint256) {
creds[credIdCounter].creator = creator_;
creds[credIdCounter].bondingCurve = bondingCurve_;
buyShareCred(credIdCounter, 1); // may refund excess ETH -> reentrancy
credIdCounter += 1; // NOT reached during reentrancy
return credIdCounter - 1;
}
function buyShareCred(uint256 credId_, uint256 amount_) public payable {
// ...
uint256 excessPayment = msg.value - price;
if (excessPayment > 0) {
// @> VULN: external refund before counter increment / without reentrancy guard
(bool ok, ) = msg.sender.call{value: excessPayment}("");
require(ok, "refund failed");
}
}
Root cause#
Effects (cred field writes) and interactions (ETH refund) happen before the
critical counter increment, with no reentrancy guard. A second createCred on
the same open credIdCounter overwrites bondingCurve, so shares bought under
one price function are sold under another.
Preconditions#
- At least two whitelisted bonding curves with materially different prices.
- Cred holds ETH from other share buyers (protocol liquidity).
- Attacker can pass createCred checks (signatures omitted in synthetic; real attack prepares signed create data for both curves).
Attack walkthrough#
From output.txt:
- Seed Cred with 50 ETH of protocol liquidity; whitelist cheap (0.001 ETH/share) and expensive (0.05 ETH/share) curves.
- Attacker
createCred(cheap)with excess ETH → refund reenters. - On reentry: buy 1000 shares cheaply (~1 ETH).
- Nested
createCred(expensive)overwrites the same credId's bonding curve. - Sell ~1000 shares at 0.05 ETH each → ~50 ETH drained from Cred.
- Proceeds swept to the Exploit contract.
Diagrams#
Impact#
As soon as more than one bonding curve is whitelisted (an expected product direction), nearly the entire Cred balance can be drained, repeatedly. Confirmed by Phi; selected for the public report for its severe multi-curve impact.
Sources#
- AuditVault finding #43397
- Code4rena report 2024-08-phi
- Reduced source: code-423n4/2024-08-phi
@ 8c0985fsrc/Cred.sol(_createCredInternal,buyShareCred)
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 43397-h-06-reentrancy-in-creating-creds-allows-an-attacker-to-stea_exp (evm-hack-registry mirror).
- AuditVault finding: 43397-h-06-reentrancy-in-creating-creds-allows-an-attacker-to-stea.
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.