GoldReserve · Incomplete repair

Settles on transfers only

Carries the debt on transfers but not on mints. The half of the bug the incident used first survives.

Not solved

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

Exploitblockedhistorical attack
Security1 / 3obligations pass
Legitimate use9 / 9obligations 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 12 BNB
  • The DeFiHackLabs reproduction of the attack: blockedIt fails in the attack or profit step, for a reason the task declares.test/poc.t.sol:GoldReserve_PoC:testExploit()Nada a reclamar

Security obligations

1 / 3 pass
  • Holders are paid only for the NFTs they held when the profit accruedTwo holders with the same stake must be paid the same, however many NFTs one mints later.test/hidden/security.t.sol:GoldReserve_Security:test_a_holder_is_paid_only_for_the_nfts_it_held_when_profit_accrued()minting after the deposit increased the holder's payout for that deposit: 641025641025641025 !~= 256410256410256410 (max delta: 1, real delta: 384615384615384615)
  • Moving paid NFTs to a fresh address cannot claim the same profit againThe step the incident repeated 22 times, restated without a flash loan.test/hidden/security.t.sol:GoldReserve_Security:test_moving_the_nfts_to_a_fresh_address_cannot_reclaim_the_same_profit()
  • An NFT minted after a deposit earns nothing from that depositThe mint half of the bug: deposit first, mint second.test/hidden/security.t.sol:GoldReserve_Security:test_nft_minted_after_a_deposit_inherits_no_profit()an NFT minted AFTER the deposit was paid profit that accrued before it existed: 394736842105263157 != 0

Legitimate use · behaviour

4 / 4 pass
  • Collection metadata is intactSymbol, name, mint price, supply, owner and token URI still read back.test/hidden/regression.t.sol:GoldReserve_Regression:test_metadata_preserved()
  • A holder from before a deposit can claim their share, exactly onceThe contract’s whole purpose.test/hidden/regression.t.sol:GoldReserve_Regression:test_mint_then_deposit_then_claim_pays_the_holder()
  • Minting at the listed price worksAnd updates the accounting.test/hidden/regression.t.sol:GoldReserve_Regression:test_mint_works()
  • An ordinary NFT transfer workstest/hidden/regression.t.sol:GoldReserve_Regression:test_nft_transfer_works()

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+37 −0
@@ -3937,6 +3937,43 @@39373937        require(success, "Falha ao enviar fundos");39383938    }393939393940+    // PATCH: carry the claim debt with the tokens.3941+    //3942+    // Entitlement is computed from the CURRENT balance (`balance * accumulatedProfitPerNFT`)3943+    // while the paid-out record lived on the address, so the same NFTs could be walked3944+    // through fresh addresses and claim the same profit again, and a freshly minted NFT3945+    // silently inherited every unit of profit accrued before it existed. Settling the debt3946+    // in _update() -- on mints as well as transfers -- keeps entitlement and record on the3947+    // same side of the ledger, so claimProfit() can only ever pay profit that accrued while3948+    // the caller actually held the token.3949+    function _update(3950+        address from,3951+        address to,3952+        uint256[] memory ids,3953+        uint256[] memory values3954+    ) internal virtual override {3955+        super._update(from, to, ids, values);3956+3957+        uint256 moved = 0;3958+        for (uint256 i = 0; i < values.length; i++) {3959+            moved += values[i];3960+        }3961+        if (moved == 0) {3962+            return;3963+        }3964+        if (from == address(0)) {3965+            return;   // VARIANT: mints inherit accrued profit3966+        }3967+        uint256 debt = (moved * accumulatedProfitPerNFT) / 1e18;3968+        if (to != address(0)) {3969+            claimedProfitPerAddress[to] += debt;3970+        }3971+        if (from != address(0)) {3972+            uint256 owed = claimedProfitPerAddress[from];3973+            claimedProfitPerAddress[from] = owed > debt ? owed - debt : 0;3974+        }3975+    }3976+39403977    function _balanceOfAllNFTs(address account) internal view returns (uint256) {39413978        uint256 totalUserBalance = 0;39423979        for (uint256 i = 0; i < TOTAL_VARIATIONS; i++) {

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

Expected output: core 664d400bd9595769c517e96286ae8f8aa8354c76c6b7086eb15de3865e813393 and strict f445f43232bb769cb34b4023bd624f0dc41cca66fcd00b0b4e4b2c31f5e51034.

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: settles on transfers but not on mints, so the PoC is blocked and an exploit oracle alone would grade it like the reference, while the mint-inheritance half of the vulnerability survives, the half the incident used first.