GoldReserve · Complete repair
Alternative repair: settle first
The same settlement placed before the balance update, with a guard instead of an early return.
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@@ -3937,6 +3937,30 @@39373937 require(success, "Falha ao enviar fundos");39383938 }393939393940+ // ALTERNATIVE COMPLETE REPAIR: settle the debt, then call super.3941+ function _update(3942+ address from,3943+ address to,3944+ uint256[] memory ids,3945+ uint256[] memory values3946+ ) internal virtual override {3947+ uint256 moved = 0;3948+ for (uint256 i = 0; i < values.length; i++) {3949+ moved += values[i];3950+ }3951+ if (moved > 0) {3952+ uint256 debt = (moved * accumulatedProfitPerNFT) / 1e18;3953+ if (to != address(0)) {3954+ claimedProfitPerAddress[to] += debt;3955+ }3956+ if (from != address(0)) {3957+ uint256 owed = claimedProfitPerAddress[from];3958+ claimedProfitPerAddress[from] = owed > debt ? owed - debt : 0;3959+ }3960+ }3961+ super._update(from, to, ids, values);3962+ }3963+39403964 function _balanceOfAllNFTs(address account) internal view returns (uint256) {39413965 uint256 totalUserBalance = 0;39423966 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/alt_settle_before_super/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 with a different implementation: the same settlement placed BEFORE super._update(), with a guard instead of an early return. A correct repair that is not the reference must still be credited.