JayPeggers JAY Reentrancy Drain
Exploit Transactions
0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6Victim Addresses
0xf2919d1d80aff2940274014bef534f7791906ff2EthereumLoss Breakdown
Similar Incidents
SushiBar ERC777 Reentrancy
37%Curve Vyper Lock Reentrancy
36%Conic ETH Oracle Reentrancy
35%PointFarm Reward Reentrancy
35%BatchSwap Counterpart Reentrancy
34%Uwerx Pool Drain
33%Root Cause Analysis
JayPeggers JAY Reentrancy Drain
1. Incident Overview TL;DR
On Ethereum mainnet block 16288200, transaction 0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6 exploited JayPeggers' JAY contract at 0xf2919d1d80aff2940274014bef534f7791906ff2. The attacker EOA 0x0348d20b74ddc0ac9bfc3626e06d30bb6fac213b used a self-deployed helper contract 0xed42cb11b9d03c807ed1ba9c2ed1d3ba5bf37340 plus Balancer Vault flash liquidity to manipulate JAY's mint pricing within a single transaction.
The root cause is a reentrancy bug in buyJay. JAY performs attacker-chosen NFT transfers before minting, and the later mint uses ETHtoJAY(msg.value), which reads address(this).balance.sub(value) after those external calls. Because the attacker-controlled ERC721 callback reentered sell and drained reserve ETH first, the denominator collapsed and the outer buyJay call minted far too much JAY. The attacker then sold those oversized JAY balances back into the reserve, repaid the flash loan, and kept 15.317096411061671907 ETH net profit.
2. Key Background
JayPeggers JAY is an ETH-backed ERC20-like token with pricing derived from the contract's native ETH reserve and total supply. The verified source shows:
function buyJay(...) public payable {
if (total != 0) buyJayWithERC721(erc721TokenAddress, erc721Ids);
...
_mint(msg.sender, ETHtoJAY(msg.value).mul(97).div(100));
}
function sell(uint256 value) public {
uint256 eth = JAYtoETH(value);
_burn(msg.sender, value);
(bool success, ) = msg.sender.call{value: eth.mul(90).div(100)}("");
require(success, "ETH Transfer failed.");
}
function ETHtoJAY(uint256 value) public view returns (uint256) {
return value.mul(totalSupply()).div(address(this).balance.sub(value));
}
This design makes mint pricing sensitive to the contract's live ETH reserve. The pre-state snapshot for block 16288199 records:
totalSupply = 13920555797661954801606address(JAY).balance = 21867269094328377172 wei
Balancer Vault at 0xBA12222222228d8Ba445958a75a0704d566BF2C8 was also publicly offering zero-fee flash loans for WETH in this block, which gave any unprivileged actor enough capital to reproduce the trade path.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability class is reentrancy through attacker-controlled token callbacks combined with mutable-reserve pricing. buyJay accepts arbitrary ERC721 and ERC1155 token addresses from the caller and immediately calls transferFrom or safeTransferFrom on those addresses before minting JAY. There is no allowlist, no reserve snapshot, and no reentrancy guard around these external calls. The attacker exploited that trust boundary by supplying a fake ERC721 contract under its own control. When JAY called transferFrom, the helper contract executed JAY.sell using its existing JAY balance, which burned JAY and removed ETH from the reserve. After control returned, the outer buyJay call still computed ETHtoJAY(msg.value) from the now-depleted reserve, so the mint amount became wildly inflated. That broken sequence violates the core invariant that a mint must be priced from a reserve state that cannot be reduced by the buyer during the same purchase.
4. Detailed Root Cause Analysis
The exploit path is fully visible in the decoded execution trace. The helper contract first received a 72.5 WETH flash loan from Balancer, unwrapped it, and used 22 ETH to perform an ordinary JAY::buyJay([], [], [], [], []), minting 13584899853779845952188 JAY. That seed balance mattered because the callback needed existing JAY to sell.
The next call was the first malicious purchase:
JAY::buyJay{value: 50500000000000000000}([helper], [0], [], [], [])
helper::transferFrom(...)
JAY::balanceOf(helper) -> 13584899853779845952188
JAY::sell(13584899853779845952188)
helper receives 41659433613619462107 wei
emit Transfer(... wad: 4313025058290613910965927)
This shows the precise breakpoint. buyJay delegated control to the attacker-controlled ERC721 address before minting. During transferFrom, the helper queried its current JAY balance, called sell, and withdrew 41.659433613619462107 ETH from JAY. Once the callback returned, the outer buyJay minted 4313025058290613910965927 JAY, which is orders of magnitude larger than the initial seed position and only possible because ETHtoJAY read the reduced reserve.
The contract source explains why that mint inflated:
function buyJayWithERC721(address[] calldata _tokenAddress, uint256[] calldata ids) internal {
for (uint256 id = 0; id < ids.length; id++) {
IERC721(_tokenAddress[id]).transferFrom(msg.sender, address(this), ids[id]);
}
}
function ETHtoJAY(uint256 value) public view returns (uint256) {
return value.mul(totalSupply()).div(address(this).balance.sub(value));
}
Because ETHtoJAY divides by address(this).balance - value, any reserve depletion between function entry and mint evaluation shrinks the denominator and increases the minted amount. JAY never snapshots the reserve before making external calls, so the pricing input remains attacker-reachable.
The trace then shows a second liquidation of that oversized mint, followed by another seed purchase and a second reentrant mint:
JAY::buyJay{value: 8000000000000000000}([helper], [0], [], [], [])
helper::transferFrom(...)
JAY::balanceOf(helper) -> 13221999944300134342235
JAY::sell(13221999944300134342235)
helper receives 6563751031787677615 wei
emit Transfer(... wad: 578675968434609156630781)
The attacker then sold that second oversized tranche, rewrapped 72.5 ETH into WETH, repaid Balancer, and forwarded the residual ETH back to the EOA. The balance diff confirms the economic result: JAY lost 21139316206458876146 wei, the attacker EOA gained 15317096411061671907 wei, and the contract ended with only 727952887869501026 wei of reserve ETH.
5. Adversary Flow Analysis
The adversary flow has four stages.
First, the EOA 0x0348d20b74ddc0ac9bfc3626e06d30bb6fac213b deployed helper contract 0xed42cb11b9d03c807ed1ba9c2ed1d3ba5bf37340 in transaction 0xce5e23f35193c8a2f02243a530c39dbdd60bcf6ea35b291590e3814e0c293cc9. The helper later served both as the Balancer flash-loan receiver and as the fake ERC721 collection.
Second, in the exploit transaction, the helper borrowed 72.5 WETH from Balancer and unwrapped it into ETH. It then executed a normal buyJay with 22 ETH to acquire a seed JAY balance.
Third, the helper called buyJay twice with itself supplied as the ERC721 token address. Each time, JAY called the helper's transferFrom, and each callback reentered sell to dump the helper's current JAY balance before the outer mint executed. Those callback sells reduced the reserve just before ETHtoJAY was evaluated, producing the oversized mints 4313025058290613910965927 and 578675968434609156630781.
Fourth, the helper sold the oversized JAY balances back to JAY for ETH, wrapped enough ETH back into WETH to repay the flash loan, and sent the remaining ETH to the EOA. No privileged keys or private integrations were required; the entire path used public contracts and attacker-deployed code, so the opportunity was ACT.
6. Impact & Losses
The direct victim was the JAY contract at 0xf2919d1d80aff2940274014bef534f7791906ff2. Its ETH reserve fell from 21867269094328377172 wei before the exploit to 727952887869501026 wei after the exploit, for a reserve loss of 21139316206458876146 wei (21.139316206458876146 ETH).
The attacker EOA realized 15317096411061671907 wei (15.317096411061671907 ETH) net profit after gas. The trace and balance diff also show 5814846475397204239 wei flowing to the dev wallet through JAY's fee paths during the same transaction. The exploit therefore did not merely create accounting distortion; it extracted almost the entire reserve and left the protocol economically broken.
7. References
- Exploit transaction:
0xd4fafa1261f6e4f9c8543228a67caf9d02811e4ad3058a2714323964a8db61f6 - Helper deployment transaction:
0xce5e23f35193c8a2f02243a530c39dbdd60bcf6ea35b291590e3814e0c293cc9 - Victim contract: JAY at
0xf2919d1d80aff2940274014bef534f7791906ff2 - Helper contract:
0xed42cb11b9d03c807ed1ba9c2ed1d3ba5bf37340 - Flash-loan source: Balancer Vault at
0xBA12222222228d8Ba445958a75a0704d566BF2C8 - Verified victim source:
JAY.buyJay,JAY.buyJayWithERC721,JAY.sell,JAY.ETHtoJAY - Evidence artifacts used for validation: decoded exploit trace, pre-state snapshot, contract creation record, and seed balance diff