GoldReserve · Non-repair
Settles on burns only
Shaped like the fix, but touches neither half of the bug. The exploit still lands.
The historical exploit still succeeds against the frozen state.
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: landsIt still clears the case’s profit threshold (12 BNB).test/poc.t.sol:GoldReserve_PoC:testExploit()
Security obligations
0 / 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()the same accrual was paid a second time after the NFTs moved to a fresh address: 512820512820512820 != 0
- ✗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@@ -3937,6 +3937,31 @@39373937 require(success, "Falha ao enviar fundos");39383938 }393939393940+ // NON-REPAIR: settlement on burns only.3941+ function _update(3942+ address from,3943+ address to,3944+ uint256[] memory ids,3945+ uint256[] memory values3946+ ) internal virtual override {3947+ super._update(from, to, ids, values);3948+3949+ if (to != address(0)) {3950+ return; // VARIANT: only burns settle, so nothing real is settled3951+ }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 (from != address(0)) {3959+ uint256 owed = claimedProfitPerAddress[from];3960+ claimedProfitPerAddress[from] = owed > debt ? owed - debt : 0;3961+ }3962+ }3963+ }3964+39403965 function _balanceOfAllNFTs(address account) internal view returns (uint256) {39413966 uint256 totalUserBalance = 0;39423967 for (uint256 i = 0; i < TOTAL_VARIATIONS; i++) {
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/non_repair__settle_on_burn_only/Token.sol \
--backend local --sha256Expected output: core b599647c054680d996736958a0f3a4fd5ca9d4dbb89adbdfdab8f57df2cb9c0c and strict 6e199dd3fc03ba592656ab24e6767c7cba5faf93d4b373929913156008f5a7f4.
To check a downloaded grade file instead: shasum -a 256 grade.strict.json prints the strict hash.
Control note
from the repositoryA patch shaped like the fix that settles the debt only on burns. Neither half of the vulnerability is touched, so the PoC itself still catches it.