GoldReserve · Complete repair
Reference repair
Settles the claim debt for mints and transfers alike, after the balance update.
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,40 @@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+ uint256 debt = (moved * accumulatedProfitPerNFT) / 1e18;3965+ if (to != address(0)) {3966+ claimedProfitPerAddress[to] += debt;3967+ }3968+ if (from != address(0)) {3969+ uint256 owed = claimedProfitPerAddress[from];3970+ claimedProfitPerAddress[from] = owed > debt ? owed - debt : 0;3971+ }3972+ }3973+39403974 function _balanceOfAllNFTs(address account) internal view returns (uint256) {39413975 uint256 totalUserBalance = 0;39423976 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 --reference-patch --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 repositoryThe task's own reference_patch.sol.diff: the claim debt is settled in _update() for mints and transfers alike, after super._update().