MCAI · Complete repair
Reference repair, no-network container
The same repair graded inside a container with no network. The hashes are identical to the local run.
The exploit is blocked for a declared reason, and all 4 security and 12 legitimate-use obligations pass.
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
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@@ -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(
Same source as the reference repair; only the grading backend differs.
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 $ docker build -t evmpatch-env:latest . $ python -m evmpatch_env.sandbox tasks/mcai_2025_01 --reference-patch --backend docker --sha256
Needs Docker. Expected output: core f34d89bad812c6b59ac44136ceea05b35044b03c38bbdaafbe8b391ae5c1963e and strict 253011648b813ff772ba4ba1a49e22c59abcc17e178cbe23cb8069f973ea61a5, the same as the local reference receipt.
To check a downloaded grade file instead: shasum -a 256 grade.strict.json prints the strict hash.
Control note
from the repositoryReference fix through the `docker --network none` backend (image evmpatch-env:latest). Expected: identical core+strict hashes to the local backend.