
Date: September 21, 2026
Chain: Polygon
Loss: about $35.5K in USDC from marketplace escrow
Class: a settlement path that paid out an offer without clearing it
On September 21, 2026, in Polygon block 94170781, a single transaction placed an offer of 35,486.935717 USDC on DoinGud's NFT marketplace, accepted it, and then accepted it again with byte-identical calldata. Both acceptances paid out the full price, and the second one came out of USDC that other users had left sitting in the marketplace's escrow.
The attacker was both the bidder and the seller, no NFT changed hands, and when the transaction ended the escrow was short by exactly one offer's worth of USDC. It worked because one branch of the function that settles offers paid the offer's price to the caller and left the offer on the books, ready to be accepted again.
DoinGud's marketplace runs on EIP-2535, the Diamond standard. A Diamond is a single proxy address that forwards each incoming call to one of several implementation contracts, called facets, and each facet owns a slice of the protocol's logic. The proxy holds the state and the funds, and the facets hold the code. DoinGud's marketplace proxy sits at 0xE3A161EdD679fC5ce2dB2316a4B6f7ab33a8eD6A, and the facet that handles offers is 0x123aAFC8D0a07CE1A146E53aA899e77f21A2DDe1.
That facet's source was never verified on Polygonscan, so nobody could read its logic there. The researcher who wrote the DeFiHackLabs proof of concept confirmed the behavior against live fork state rather than source code, and DeFiHackLabs merged it on September 26. Security researcher teddyctf flagged the exploit publicly on X early on September 22 UTC, and wrote in the same thread that they'd found the bug in the DoinGud Diamond contract about a week before the hack.
An offer on a marketplace is a promise held in escrow: a buyer locks funds, and when a seller accepts, the funds move and the offer is consumed. The function that does this is acceptOffer(offerer, tokenId, price, amount). When amount is greater than zero, the normal path for a self-offer reverts, but when amount is zero, acceptOffer transfers the offer's price out of the marketplace escrow to the caller and doesn't delete or zero the offer record afterward.
So a settled offer stays exactly as it was in storage. Anyone holding the right calldata can accept it again, and each time it pays out the same price. It's like a cashier who hands over the money for a voucher and gives the voucher back. The function also had no check that the caller wasn't the offerer, so the attacker could fund an offer and then accept it themselves, without owning or delivering anything.
Every path out of a settlement function has to leave the record settled, including the branches that look like edge cases. A zero amount looks like a no-op, and in this contract it was the one input that paid out without consuming anything.
The attacker's EOA, 0xb8C717239BCACE558c3a8Dc471c16E07bF57a1Eb, called a helper contract it had deployed beforehand, 0xe588834AA3161a0720E8F6bf223748D6098a4B76. The helper opened a Uniswap V2 flash swap from the USDC pair at 0x160532D2536175d65C03B97b0630A9802c274daD to borrow the capital, approved USDC, and called makeOffer to create offer 1 and fund the escrow with 35,486.935717 USDC.
It then called acceptOffer with an amount of zero and collected 35,486.935717 USDC, and called acceptOffer a second time with byte-identical calldata and collected the same amount again. The two payouts came to about 70,973.87 USDC. After repaying the flash swap with 35,593.716868 USDC, the attacker kept 35,380.154566 USDC, which matches the escrow's loss of one offer price minus the swap fee.
The attack never needed anyone's cooperation or any special timing. The Diamond answered calls from anyone, the escrow held other users' USDC, and the flash swap supplied the only capital required, so the full cost of the exploit was a fee of about 107 USDC. An escrow keeps its balance whether or not anyone is watching it, and every branch of the code that guards it stays callable for as long as the contract is deployed.
Olympix reads a contract the way an attacker does, as states, transitions and entry points, and it works from the code a team actually ships, so an unverified facet on a block explorer doesn't slow it down. In DoinGud's marketplace, the states that matter are the offer records and the escrow balance behind them, and makeOffer and acceptOffer are the transitions between them. The engine maps every path through those two functions, including each branch inside acceptOffer, and uses signals, patterns in the code that mark a path as dangerous, to decide which ones to attack. Olympix's static analysis surfaces the signal here: a branch of a settlement function that sends escrowed funds to the caller and returns without writing to the record it paid from.
With that branch flagged, BugPoCer goes to work on the escrow as a whole. Counterexample-Guided Invariant Refutation starts from what the marketplace has to guarantee, and for an offer book that's a simple accounting rule: each offer pays out once at most, so the escrow never releases more for an offer than was deposited for it. BugPoCer executes sequences of makeOffer and acceptOffer calls against that rule, varying the amount and the caller, until a sequence breaks it, and the amount-zero branch breaks it on the second acceptance.
The output is a counterexample: create an offer, accept it with an amount of zero, accept it again with the same calldata, and watch the escrow pay twice, along with the minimal conditions that trigger it and a generated Forge test that replays it. That test is the September 21 transaction, produced in development instead of on Polygon.
Because the engine runs automatically in development, at every merge and before release, a team building with facets keeps a standing view of every settlement path across them, including the branches nobody expected to be called.
DoinGud's acceptOffer handled the cases it was built for and paid out on one it wasn't. Settlement code is only as safe as its least-used branch, and an escrow holding other people's funds keeps every one of those branches worth trying. To see Olympix walk every settlement path in your own contracts and hand back a runnable counterexample for any that pays twice, book a technical walkthrough.
Protocol: DoinGud marketplace (EIP-2535 Diamond)
Chain: Polygon
Date: September 21, 2026
Loss: 35,486.935717 USDC from escrow (attacker net 35,380.154566 USDC)
Attacker EOA: 0xb8C7...a1Eb
Attacker helper contract: 0xe588...4B76
Marketplace Diamond proxy: 0xE3A1...eD6A
Vulnerable marketplace facet (source unverified): 0x123a...2DDe
Flash swap pair (Uniswap V2, USDC): 0x1605...4daD
USDC: 0x2791...4174
Exploit tx: 0x5681...16a8
Block: 94170781
DeFiHackLabs PR #1273: DoinGud acceptOffer replay via missing offer cleanup