Bitallx · Incomplete repair

Checks the first amount only

Bounds only the first element, so [0, treasury] with nothing funded still drains the contract.

Not solved

The exploit is blocked, but 2 of 3 security obligations fail: the repair is incomplete.

Exploitblockedhistorical attack
Security1 / 3obligations pass
Legitimate use10 / 10obligations pass
Canariesresidual vulnerabilitytamper 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 2,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:Bitallx_PoC:testExploit()Payout total does not match the funded amount

Security obligations

1 / 3 pass
  • A batch whose total exceeds what was funded pays nothing[0, treasury] with nothing funded: the shape a first-element-only fix lets through.test/hidden/security.t.sol:Bitallx_Security:test_multi_element_over_request_pays_nothing()unfunded multi-element payout drained the contract's own USDT treasury: 0 < 2029473999999999986000
  • The incident’s own shape pays nothingA one-element over-request from a payer who funded nothing, kept as a standing obligation.test/hidden/security.t.sol:Bitallx_Security:test_single_element_over_request_from_fresh_payer_pays_nothing()
  • The sum is bounded, not just each element[total, total] funded once must not be paid twice.test/hidden/security.t.sol:Bitallx_Security:test_sum_over_funded_total_rejected_even_when_each_element_fits()a batch funded once and paid twice drained the contract's own USDT treasury: 1929473999999999986000 < 2029473999999999986000

Legitimate use · behaviour

5 / 5 pass
  • A funded batch payout pays everyone correctlyThe contract’s actual purpose.test/hidden/regression.t.sol:Bitallx_Regression:test_funded_batch_payout_distributes_correctly()
  • The patched code still reads the contract’s live storageA fix that inserts or reorders state variables would re-point every one of them.test/hidden/regression.t.sol:Bitallx_Regression:test_live_storage_still_readable()
  • The owner’s treasury sweep works, and only for the ownertest/hidden/regression.t.sol:Bitallx_Regression:test_owner_treasury_path_still_works()
  • An unfunded payer is refusedtest/hidden/regression.t.sol:Bitallx_Regression:test_payout_rejects_an_unfunded_payer()
  • The publisher can still pay a reward, within its limitstest/hidden/regression.t.sol:Bitallx_Regression:test_publisher_can_still_pay_a_reward()

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+2 −0
@@ -91,6 +91,8 @@9191    uint256 totalSendAmount9292    ) external {9393        require(wallet.length == amount.length, "The length of 2 arrays should be the same");94+        require(amount.length == 0 || amount[0] <= totalSendAmount,95+                "Payout total does not match the funded amount");9496    9597        uint256 allowance = IBEP20(tokencontract).allowance(msg.sender, address(this));9698        require(allowance >= totalSendAmount, "Insufficient token allowance");

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

Expected output: core c36f1eea6ce6ab132b76299df29964a9566e8086cdf0a7db00a33666aa9d903a and strict 03db5f251b9d9253f365994b54d0063dc4402e4a32e4ea8f9533566f89b731cd.

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

Control note

from the repository

The load-bearing incomplete repair: bounds only amount[0], so the one-element PoC is blocked and an exploit oracle alone would grade it like the reference, while a two-element array [0, treasury] with totalSendAmount = 0 still drains the contract.