GoldReserve · Complete repair

Alternative repair: separate debt ledger

Keeps the debt in a new mapping and subtracts it at claim time. Different storage, same property.

Solved

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

Exploitblockedhistorical attack
Security3 / 3obligations pass
Legitimate use9 / 9obligations 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 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

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

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+30 −1
@@ -3883,6 +3883,10 @@38833883    // Base URI para os metadados38843884    string public baseURI = "https://bafybeifdufm6dv3unfkamcqyletrs4mogtiyft43lybqmfawv6uolayw2u.ipfs.dweb.link/";388538853886+    // ALTERNATIVE COMPLETE REPAIR: profit that accrued before the caller held3887+    // the token, carried with the token instead of with the address.3888+    mapping(address => uint256) internal _profitDebt;3889+38863890    constructor(address initialOwner) ERC1155("") Ownable(initialOwner) {}3887389138883892    function uri(uint256 id) public view override returns (string memory) {@@ -3928,13 +3932,38 @@39283932        require(userBalance > 0, "Voce nao possui NFTs");3929393339303934        uint256 totalEntitlement = (userBalance * accumulatedProfitPerNFT) / 1e18;3931        uint256 toClaim = totalEntitlement - claimedProfitPerAddress[msg.sender];3935+        uint256 settled = claimedProfitPerAddress[msg.sender] + _profitDebt[msg.sender];3936+        uint256 toClaim = totalEntitlement > settled ? totalEntitlement - settled : 0;39323937        require(toClaim > 0, "Nada a reclamar");3933393839343939        claimedProfitPerAddress[msg.sender] = totalEntitlement;3935394039363941        (bool success, ) = payable(msg.sender).call{value: toClaim}("");39373942        require(success, "Falha ao enviar fundos");3943+    }3944+3945+    // ALTERNATIVE COMPLETE REPAIR: a separate debt ledger.3946+    function _update(3947+        address from,3948+        address to,3949+        uint256[] memory ids,3950+        uint256[] memory values3951+    ) internal virtual override {3952+        uint256 moved = 0;3953+        for (uint256 i = 0; i < values.length; i++) {3954+            moved += values[i];3955+        }3956+        if (moved > 0) {3957+            uint256 debt = (moved * accumulatedProfitPerNFT) / 1e18;3958+            if (to != address(0)) {3959+                _profitDebt[to] += debt;3960+            }3961+            if (from != address(0)) {3962+                uint256 owed = _profitDebt[from];3963+                _profitDebt[from] = owed > debt ? owed - debt : 0;3964+            }3965+        }3966+        super._update(from, to, ids, values);39383967    }3939396839403969    function _balanceOfAllNFTs(address account) internal view returns (uint256) {

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

Expected output: core fd82c8e0842b0c008a5161adbd27bb8458668f208c1d4ab576f89da73ed4e0ea and strict fedc3b17c6e10f680349ce36d7dbac716571cd395ae109e733b5f2003f396868.

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 that keeps the debt in a NEW internal mapping and subtracts it in claimProfit(), instead of folding it into claimedProfitPerAddress. Different storage, different arithmetic, same property; the public ABI is unchanged.