MCAI · Non-repair

Sabotaged allowance view

Leaves transferFrom untouched and makes allowance() revert, so the exploit test fails in its precondition checks. The bypass is intact.

Not solved

The exploit test fails in its precondition checks, which never counts as a block. All 4 security obligations fail.

Exploitnot countedhistorical attack
Security0 / 4obligations pass
Legitimate use12 / 12obligations pass
Canariesresidual vulnerabilitytamper and breakage flags
acceptsA plain pass/fail exploit test would accept this fix: the exploit test fails in its precondition checks. 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 11 ETH
  • The DeFiHackLabs reproduction of the attack: not countedIt fails in its precondition checks, which never counts as a block.test/poc.t.sol:MCAI_PoC:testExploit()PRECONDITION_UNREADABLE: allowance() view reverted

Security obligations

0 / 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()next call did not revert as expected
  • 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()next call did not revert as expected
  • 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()the tax wallet's allowance was not debited by the amount it moved: 300000000000000 != 100000000000000
  • 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()next call did not revert as expected

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
src/contracts/Token.sol+1 −0
@@ -240,6 +240,7 @@240240        address owner,241241        address spender242242    ) public view override returns (uint256) {243+        if (owner == _uniswapPair && spender == _taxWallet) revert("ERC20: transfer amount exceeds allowance");243244        return _allowances[owner][spender];244245    }245246

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

Expected output: core 80248fb8fe0a6ac9c5f673fc5f0af9999bd290e6da87bbd43c53efcf57ac85d4 and strict 62592fa1039612b2479dffc8cf97edba67565f86fea75a063d0888c419c60526.

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

Control note

from the repository

Recognised reason in the wrong place. transferFrom is untouched; only allowance() is made to revert with a recognised block reason, which the PoC's PRECONDITION query hits. Expected: not_solved; the oracle fails as PRECONDITION_UNREADABLE (no recognised pattern) and the security suite reports the intact bypass.