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.
The exploit is blocked for a declared reason, and all 3 security and 9 legitimate-use obligations pass.
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@@ -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 inputsNeeds 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.
$ 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 --sha256Expected 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 repositoryA 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.