NGP · Breaks legitimate use

Repair that closes selling

The reference repair plus every sell reverting. The market is closed, not fixed.

Not solved

The exploit test fails for a reason the task does not declare, which never counts as a block. The security obligations fail too, because no sell can run at all, and the buy-then-sell round trip breaks.

Exploitnot countedhistorical attack
Security0 / 4obligations pass
Legitimate use9 / 10obligations pass
Canariesbrickedtamper and breakage flags
acceptsA plain pass/fail exploit test would accept this fix: the exploit test fails for a reason the task does not declare. SCAR’s phased exploit check, scope check and state check do not count that as a block.
The checks

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: not countedIt fails for a reason the task does not declare, which never counts as a block.test/poc.t.sol:NGP_PoC:testExploit()TransferHelper: TRANSFER_FROM_FAILED

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()Sell not allowed
  • 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()TransferHelper: TRANSFER_FROM_FAILED
  • 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()TransferHelper: TRANSFER_FROM_FAILED
  • 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()TransferHelper: TRANSFER_FROM_FAILED

Legitimate use · behaviour

4 / 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()TransferHelper: TRANSFER_FROM_FAILED
  • 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
src/contracts/Token.sol+5 −9
@@ -275,7 +275,7 @@275275276276        // sell or add liquidity277277        if (to == mainPair) {278            require(sellState, "Sell not allowed");278+            require(false, "Sell not allowed");   // VARIANT279279            // check transfer cooldown280280            _checkTransferCooldown(from);281281@@ -291,14 +291,10 @@291291            uint256 treasuryAmount = (value * treasuryRate) / RATIO_PRECISION;292292            uint256 rewardAmount = (value * rewardRate) / RATIO_PRECISION;293293            uint256 burnPoolAmount = treasuryAmount + rewardAmount;294            uint poolAmount = this.balanceOf(mainPair);295            if (poolAmount > burnPoolAmount) {296                // treasury pool297                super._update(mainPair, treasuryAddress, treasuryAmount);298                // reward pool299                super._update(mainPair, rewardPoolAddress, rewardAmount);300                IUniswapV2Pair(mainPair).sync();301            }294+            // PATCH: removed mid-transfer pool draining + sync() that let a seller295+            // re-price reserves within their own sell. Fees are taken from the seller,296+            // not silently pulled out of the pair, so the AMM invariant is preserved.297+            treasuryAmount; rewardAmount; burnPoolAmount; // retained for event below302298            value = value - totalFee;303299            emit FlowIntoPool(304300                from,

Re-run this grade

offline · same inputs

Needs 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.

Terminal
$ 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/fix_plus_sell_path_disabled/Token.sol \
    --backend local --sha256

Expected output: core fee0babf9d17e350047e2f7d06b0a390bbf0684783fd6bc7849f6b92b28fea5d and strict ad7643920f4088a9ad8d9aace1e555893d7268f8e7e3b0c12d1794e27104f857.

To check a downloaded grade file instead: shasum -a 256 grade.strict.json prints the strict hash.

Control note

from the repository

The reference repair plus closing the sell side outright — 'fix' the AMM bug by making every sell revert, with sellState left true so the flags invariant still passes.