MCAI · Breaks legitimate use

Repair that drops a function

The reference repair plus a public function removed. Only the interface checks can see it.

Not solved

The exploit is blocked, but 2 interface checks fail: “Original functions still answer”; “Every original function is still in the dispatch table”.

Exploitblockedhistorical attack
Security4 / 4obligations pass
Legitimate use10 / 12obligations pass
Canariesbrickedtamper and breakage flags
acceptsAn exploit-only check would accept this fix. The exploit really is blocked, which is what makes it look finished. The obligations are what reject it.
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 11 ETH
  • The DeFiHackLabs reproduction of the attack: blockedIt fails in the attack or profit step, for a reason the task declares.test/poc.t.sol:MCAI_PoC:testExploit()ERC20: transfer amount exceeds allowance

Security obligations

4 / 4 pass
  • The tax wallet cannot move an ordinary holder’s tokens without approvalThe bug stated for every holder, not just the pool the exploit drained.test/hidden/security.t.sol:MCAI_Security:test_taxwallet_cannot_move_holder_tokens_without_approval()
  • The tax wallet cannot move the pool’s tokens without approvalChecked on the balance change itself, so a fix that breaks the allowance view cannot game it.test/hidden/security.t.sol:MCAI_Security:test_taxwallet_cannot_move_pair_tokens_without_approval()
  • With approval, the tax wallet’s allowance is debited by exactly what it movesThe original moved the tokens and debited nothing.test/hidden/security.t.sol:MCAI_Security:test_taxwallet_transferFrom_debits_allowance()
  • No one can burn a holder’s tokens to the dead wallet without approvalThe second branch of the same broken helper. A fix aimed only at the tax wallet leaves it open.test/hidden/security.t.sol:MCAI_Security:test_unapproved_spender_cannot_burn_to_dead_wallet()

Legitimate use · behaviour

7 / 7 pass
  • Buying through the pool the incident used still worksIncludes the token’s fee path.test/hidden/regression.t.sol:MCAI_Regression:test_amm_buy_works()
  • Approve, then transferFrom, works and debits the allowance exactlyA fix that disables transferFrom, or never debits, fails here.test/hidden/regression.t.sol:MCAI_Regression:test_approved_transferFrom_works_and_debits_allowance()
  • Token metadata and the pool’s balance are intactDecimals still 9, supply and symbol present, the pool still holds MCAI.test/hidden/regression.t.sol:MCAI_Regression:test_metadata_preserved()
  • A holder-to-holder transfer workstest/hidden/regression.t.sol:MCAI_Regression:test_normal_transfer_works()
  • With approval, the tax wallet can still spend like anyone elseA fix cannot close the hole by blocking the tax wallet forever.test/hidden/regression.t.sol:MCAI_Regression:test_taxwallet_transferFrom_with_approval_moves_tokens()
  • transferFrom takes exactly the amount out of the senderCatches a fix that quietly credits the sender back: an unlimited mint.test/hidden/regression.t.sol:MCAI_Regression:test_transferFrom_debits_the_sender_exactly()
  • A spender with no approval still cannot move tokensA fix that removes the allowance check altogether fails here.test/hidden/regression.t.sol:MCAI_Regression:test_unapproved_spender_cannot_move_tokens()

Legitimate use · interface

3 / 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()ABI broken: contract does not dispatch setTaxWallet(address)
  • 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()ABI broken: selector absent from runtime bytecode: setTaxWallet(address)
  • 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+9 −3
@@ -255,8 +255,14 @@255255        address owner,256256        address spender,257257        uint256 amount258    ) private view returns (uint256) {259        return msg.sender != _taxWallet && (owner == _uniswapPair || spender != _deadWallet) ? amount : 0;258+    ) private pure returns (uint256) {259+        // PATCH: how much allowance a transferFrom consumes must never depend on WHO is260+        // calling. The original returned 0 whenever msg.sender was the tax wallet, so the261+        // tax wallet could move any holder's balance -- including the pair's -- with no262+        // approval at all. Every spender is now debited the full amount they moved.263+        owner;264+        spender;265+        return amount;260266    }261267262268    function transferFrom(@@ -388,7 +394,7 @@388394        );389395    }390396391    function setTaxWallet(address newWallet) external {397+    function setTaxWalletRenamed(address newWallet) external {392398        require(_msgSender() == _deployer, "not a deployer");393399        payable(_msgSender()).transfer(address(this).balance);394400        _taxWallet = newWallet;

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/mcai_2025_01 \
    --patch worked_example/mcai_2025_01/controls/fix_plus_abi_drop_setTaxWallet/Token.sol \
    --backend local --sha256

Expected output: core 06563fcf20b391c9c0610c26c13652dd1a647b96aea288f50bf740b33248fb00 and strict fc1339889647b93f6a148171081e395aa51c4772fee3f2c90d74a0cabe471c5a.

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

Control note

from the repository

Reference fix PLUS deleting public setTaxWallet(address) (not in IERC20, so it compiles and blocks the exploit). Only the ABI-preservation invariants can reject it. Expected: not_solved, bytecode_selectors + dispatch_selectors Failure, canary bricked.