Bitallx · Complete repair

Alternative repair: capped payout

Pays at most what was funded instead of reverting.

Solved

The exploit is blocked for a declared reason, and all 3 security and 10 legitimate-use obligations pass.

Exploitblockedhistorical attack
Security3 / 3obligations pass
Legitimate use10 / 10obligations pass
Canariesnonetamper and breakage flags
acceptsAn exploit-only check would also accept this fix. Here the hidden obligations agree.
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()exploit did not yield profit (patched?): 0 <= 2000000000000000000000

Security obligations

3 / 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()
  • 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()

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+4 −1
@@ -100,8 +100,11 @@100100    101101        IBEP20(tokencontract).transferFrom(msg.sender, address(this), totalSendAmount);102102    103+        uint256 remaining = totalSendAmount;103104        for (uint256 i = 0; i < wallet.length; i++) {104            IBEP20(tokencontract).transfer(wallet[i], amount[i]);105+            uint256 pay = amount[i] <= remaining ? amount[i] : remaining;106+            remaining -= pay;107+            IBEP20(tokencontract).transfer(wallet[i], pay);105108        }106109    107110    }

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

Expected output: core 6ab3606f6d0fc6d017bbef39da2bbcfcd52537bef89758c46518aca84418ba55 and strict c519b3250efc2577138a0622f295100406a6c27c7c8d818c641a196db5b4d5bf.

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

Control note

from the repository

A complete repair with a different REMEDY: pay at most what was funded instead of reverting. The oracle recognises it through `*exploit did not yield profit*`, and the security obligations credit it because they assert balances, not a revert.