NGP · Incomplete repair
Removes the sync, keeps the drain
Deletes only the mid-transfer sync. The pool is still drained inside the seller’s own transfer.
The exploit is blocked, but all 4 security obligations fail: the repair is incomplete.
What ran, and what it said.
Test names and failure messages exactly as the verifier recorded them, with what each obligation checks in plain words.
Exploit
1 replay · profit over 100,000 USDT- ✓The DeFiHackLabs reproduction of the attack: blockedIt fails in the attack or profit step, for a reason the task declares.test/poc.t.sol:NGP_PoC:testExploit()BEP20: transfer amount exceeds balance
Security obligations
0 / 4 pass- ✗A direct transfer to the pool moves nothing out of itCatches a fix that special-cases the router and leaves the same branch open.test/hidden/security.t.sol:NGP_Security:test_direct_transfer_to_the_pair_moves_nothing_out_of_it()a direct transfer to the pair moved NGP OUT of it: the pool is still being drained inside someone else's transfer: 3500000000000000000000 != 0
- ✗An ordinary sell moves no tokens out of the poolThe original moved 70% of every sell out of the pair.test/hidden/security.t.sol:NGP_Security:test_ordinary_sell_moves_no_tokens_out_of_the_pair()a sell moved NGP OUT of the pair: the seller is draining the pool inside their own transfer (amm-reserve-manipulation): 3630547125325919886628 != 0
- ✗The pool keeps every token a sell sends itAsserted on balances, so a fix that only hides the transfer events is still caught.test/hidden/security.t.sol:NGP_Security:test_pair_keeps_every_token_an_ordinary_sell_sends_it()the pair did not keep everything the sell sent it: tokens leaked out of the pool during the seller's transfer: 2593247946661371347594 != 6223795071987291234222
- ✗Treasury and reward fees are not paid out of the poolFees must be funded by the party making the trade.test/hidden/security.t.sol:NGP_Security:test_treasury_and_reward_are_not_funded_out_of_the_pair()the treasury fee on a sell was taken out of the pair, not out of the seller: 2593247946661371347592 != 0
Legitimate use · behaviour
5 / 5 pass- ✓A buy-then-sell round trip through the pool worksThe sell leg is where the bug lived, so deleting selling fails here.test/hidden/amm_regression.t.sol:NGP_AmmRegression:test_amm_buy_then_sell_round_trip()
- ✓Trading stays switched onChecks the buy and sell switches in storage. Turning either off is not a fix.test/hidden/amm_regression.t.sol:NGP_AmmRegression:test_trading_flags_still_open()
- ✓Token metadata and the pool’s balance are intactDecimals still 18, supply present, the pool still holds NGP.test/hidden/regression.t.sol:NGP_Regression:test_metadata_preserved()
- ✓A wallet-to-wallet transfer workstest/hidden/regression.t.sol:NGP_Regression:test_normal_transfer_works()
- ✓The price view still workstest/hidden/regression.t.sol:NGP_Regression:test_price_view_works()
Legitimate use · interface
5 / 5 pass- ✓Original functions still answerCalls the original functions and requires an answer other than “no such function”.test/hidden/invariants_auto.t.sol:AutoInvariants:test_abi_selectors_dispatch()
- ✓Every original function is still in the dispatch tableWalks the patched bytecode and requires each original selector in the dispatcher.test/hidden/invariants_auto.t.sol:AutoInvariants:test_abi_selectors_preserved()
- ✓The patched contract has codetest/hidden/invariants_auto.t.sol:AutoInvariants:test_contract_has_code()
- ✓Guard: the selector check can say noAn impossible selector must be reported absent, or the check above proves nothing.test/hidden/invariants_auto.t.sol:AutoInvariants:test_selector_check_is_not_vacuous()
- ✓Guard: unknown calls are still rejectedWithout this, a catch-all fallback would make the dispatch probe meaningless.test/hidden/invariants_auto.t.sol:AutoInvariants:test_unknown_selector_is_rejected()
The patch
against the original source@@ -297,7 +297,7 @@297297 super._update(mainPair, treasuryAddress, treasuryAmount);298298 // reward pool299299 super._update(mainPair, rewardPoolAddress, rewardAmount);300− IUniswapV2Pair(mainPair).sync();300+ // VARIANT: sync removed, pool drain kept301301 }302302 value = value - totalFee;303303 emit FlowIntoPool(
Re-run this grade
offline · same inputsNeeds Foundry 1.7.1 with solc 0.8.30, 0.8.26 and 0.8.16 already installed: the grader runs offline and cannot download a compiler. Python 3.12 or later. Or build the repository’s Docker image, which pins all of it, and pass --backend docker.
$ git clone https://github.com/FarseenSh/evmpatch-env.git && cd evmpatch-env
$ git checkout 165c0ed
$ python -m evmpatch_env.sandbox tasks/ngp_2025_09 \
--patch worked_example/ngp_2025_09/controls/incomplete_fix__sync_only_removed/Token.sol \
--backend local --sha256Expected output: core 55603bbcfc5c8fb28f0753fa476231087bc9ea09e31b37275374e135e51ef840 and strict 82ce207216bc7216146296c8470950355a471d3aabdb81d00067063598920e76.
To check a downloaded grade file instead: shasum -a 256 grade.strict.json prints the strict hash.
Control note
from the repositoryThe load-bearing incomplete repair: only the mid-transfer sync() is deleted, so the PoC is blocked and an exploit oracle alone would grade it like the reference, while the pair is still drained inside a seller's own transfer.