Die pure Ökonomie von MEV läuft auf einen knallharten Wettlauf um Blockspace hinaus, bei dem eine Millisekunde Netzwerk-Latenz den gesamten potenziellen Profit zunichtemacht. Einen eigenen Bot auf Ethereum oder Layer-2-EVM-Netzwerken zu starten, ist schon lange kein Easy Money mehr. Der Markt ist unter spezialisierte Searcher aufgeteilt, die sich Server in Rechenzentren in Frankfurt und Tokio direkt neben den Validator-Nodes mieten.
Echte Zahlen und Rentabilität
Ein Solo-Bot auf einem Heimserver oder einem Standard-Cloud-VPS arbeitet wegen Gas-Auktionen zu 95 % im Minus. Die Profitabilität hängt komplett von der Strategie ab:
- Atomare Arbitrage (Atomic Arbitrage): Die Margen sind auf Bruchteile eines Prozents geschrumpft. Der Netto-Tagesgewinn eines erfolgreichen Bots liegt in volatilen Phasen zwischen 100 $ und 3.000 $. Das Kapitalverlustrisiko ist gleich null, da die Transaktion in einen einzigen atomaren Smart Contract verpackt ist: Wenn die Bedingungen nicht stimmen, rollt sich die gesamte Kette zurück.
- Sandwich-Angriffe (Sandwich Attacks): Die theoretischen Erträge sind höher und liegen bei 500 $ bis 10.000 $ pro Tag auf liquiditätsschwachen Paaren auf Uniswap v2 oder ähnlichen AMMs. Die Realität ist brutaler. Konkurrierende Bots schleusen ihre Aufträge über private Transaktionspools ein und überbieten sich gegenseitig mit Validator-Bribes.
- Liquidationen (Liquidations): Der Spot-Markt von Kreditprotokollen wie Aave bringt dicke Fische, erfordert aber Millionen an Liquidität, um Schulden abzusichern und Positionen sofort zu begleichen. Für Solojäger ist hier nichts zu holen.
Gas-Kostenstruktur und die Arbeit mit Buildern
Der größte Kostenfaktor beim Betrieb eines MEV-Bots sind nicht erfolgreiche Transaktionen, sondern Fehlschläge und Gas Priority Auctions (GPAs).
- Öffentlicher Mempool: Transaktionen ohne private Relays abzusetzen, garantiert Verluste. Ein einfacher Bot sieht die Opfer-Tx, schickt seine eigene mit höherem Gas hinterher, aber rivalisierende Bots fangen sie über private RPCs vorher ab. Am Ende wird deine Transaktion zum Worst-Case-Preis ausgeführt oder reverted komplett, während echtes Gas auf der Blockchain verbrannt wird.
- Block Builder und Flashbots: Moderne Arbitrage läuft über eth_sendBundle. Du schnürst ein Bundle aus mehreren Transaktionen und schickst es direkt an Block Builder (Beaverbuild, Titan, Flashbots).
- Fehlschlaggebühr: Der Vorteil privater Bundles ist, dass die Transaktion bei fehlgeschlagener Arbitrage gar nicht erst in den Block gelangt. Du zahlst keine Netzwerkgebühren für Schüsse ins Leere. Der Haken: Der Validator streicht den Löwenanteil des Profits als direkten Tip (coinbase.transfer) ein und lässt dem Entwickler gerade mal 5 bis 15 % des Bruttogewinns.
Tabelle der MEV-Strategiemetriken
| Strategie | Startkapital | Ø Netto-Tagesgewinn | Erfolgsquote | Hauptgrund für Verluste |
|---|---|---|---|---|
| Atomare Arbitrage | ab 5.000 $ | 50 $ - 500 $ | 15-25 % | Netzwerklatenz-Rückstand gegenüber Konkurrenten |
| Sandwich-Angriffe | ab 20.000 $ | 200 $ - 3.000 $ | 5-10 % | Überteuerte Gebühren in Validator-Auktionen |
| Liquidationen | ab 100.000 $+ | 1.000 $ - 10.000 $+ | < 2 % | Kapitalmangel zur Begleichung großer Kredite |
Beispiel für den Versand eines privaten Bundles in Python
Unten siehst du ein funktionierendes Python-Skriptgerüst mit der web3.py-Bibliothek, um ein Bundle über einen Flashbots-kompatiblen Provider zu senden. Der Code wurde von unnötigen Abstraktionen befreit.
import os
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3
from flashbots import flashbot
# Schlüssel zum Signieren von Relay-Anfragen (Flashbots-Reputation).
# Verwende KEINEN Schlüssel, auf dem echtes Guthaben liegt.
signature_account: LocalAccount = Account.from_key(os.environ["FLASHBOTS_SIGNER_KEY"])
# Private Key des Transaktionsabsenders
sender: LocalAccount = Account.from_key(os.environ["SENDER_PRIVATE_KEY"])
w3 = Web3(Web3.HTTPProvider(os.environ["ETH_NODE_URL"]))
flashbot(w3, signature_account, "https://relay-sepolia.flashbots.net")
def build_signed_tx(nonce: int) -> bytes:
base_fee = w3.eth.get_block("latest")["baseFeePerGas"]
tx = {
"to": sender.address, # Beispiel: Eigenüberweisung
"value": 0,
"gas": 21_000,
"maxFeePerGas": base_fee * 2 + Web3.to_wei(2, "gwei"),
"maxPriorityFeePerGas": Web3.to_wei(2, "gwei"),
"nonce": nonce,
"chainId": w3.eth.chain_id,
"type": 2,
}
return sender.sign_transaction(tx).rawTransaction # in neueren Versionen: .raw_transaction
def send_private_bundle(target_block: int):
nonce = w3.eth.get_transaction_count(sender.address)
bundle = [{"signed_transaction": build_signed_tx(nonce)}]
# Simulation vor dem Absenden
w3.flashbots.simulate(bundle, target_block)
result = w3.flashbots.send_bundle(bundle, target_block_number=target_block)
result.wait() # wartet, bis der target_block verarbeitet wird
try:
return result.receipts()
except Exception:
return None # Bundle nicht im Block enthalten: kann im nächsten Block wiederholt werden
if __name__ == "__main__":
target = w3.eth.block_number + 1
receipts = send_private_bundle(target)
print("Im Block enthalten" if receipts else "Bundle nicht enthalten, im nächsten Block erneut versuchen")Ein Bundle gilt nur für einen einzigen Zielblock. In der Praxis muss das Senden daher in einer Schleife über mehrere aufeinanderfolgende Blöcke hinweg erfolgen. Nicht inkludiert zu werden ist kein Fehler: Der Wettkampf entscheidet sich über die Höhe des Builder-Tips.
Die Rentabilität eines eigenen Bots steht und fällt mit den Infrastrukturkosten. Einen dedizierten Rack-Server direkt neben einem Validator im Rechenzentrum anzumieten, kostet ab 500 $ pro Monat. Ohne diesen Hardware-Vorteil schafft es dein Bot physisch nicht, den Pool-Status schneller als die Konkurrenz zu berechnen. Wer das kürzeste Kabel zum Validator und den schlanksten Spread-Search-Algorithmus hat, gewinnt.
Flash Loans ermöglichen großangelegte Arbitrage-Operationen ohne eigenes Startkapital, indem sie unbesicherte Kredite innerhalb einer einzigen Transaktion mit garantiertem Rollback bei Verlusten ermöglichen.
Flash Loans und Smart-Contract-Architektur
Ein Eigenkapital von mehreren Hunderttausend Dollar ist für Arbitrage längst keine Pflicht mehr. Integrationsverträge mit Aave-v3- oder Balancer-Pools erlauben unbesicherte Flash Loans im Rahmen einer einzigen Transaktion. Der Bot leiht sich das Asset, jagt es durch DEX-Paare, zahlt die Schulden inklusive einer Gebühr von 0,09 % zurück und streicht den Restbetrag auf dem Konto des Erstellers ein. Geht die Rechnung am Ende nicht auf, wird die gesamte Transaktion per Revert zurückgerollt und der Initiator verliert lediglich das Gas für den Funktionsaufruf.
Das Schreiben eines Arbitrage-Vertrags in Solidity erfordert eine knallharte Optimierung auf den EVM-Bytecode. Die Nutzung standardmäßiger ERC-20-Schnittstellen über SafeERC20 bringt redundante Checks mit sich, die beim Prüfen von Transaktionslimits wertvolle Gaseinheiten auffressen. Erfahrene Entwickler schreiben direkte Low-Level-Aufrufe via call und minimieren so Lese- und Schreiboperationen im persistenten Speicher (SLOAD/SSTORE).
Solidity-Template-Beispiel
Unten ist ein Aave-V3-Flash-Loan-Template für Uniswap-V2-Pools ohne Drittanbieter-Bibliotheken abgebildet:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
}
/// Aave V3: Flash Loan eines einzelnen Assets
interface IPool {
function flashLoanSimple(
address receiverAddress,
address asset,
uint256 amount,
bytes calldata params,
uint16 referralCode
) external;
}
interface IUniswapV2Pair {
function token0() external view returns (address);
function getReserves() external view returns (uint112, uint112, uint32);
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
}
/// Arbitrage über Aave V3 Flash Loan: asset -> tokenB (pair1) -> asset (pair2).
/// Beispiel für Uniswap-V2-Pools. Nicht auditiert.
contract FlashArbitrage {
error Unauthorized();
error InvalidCaller();
error InvalidInitiator();
error TransferFailed();
error ApproveFailed();
error InsufficientProfit(uint256 balanceAfter, uint256 required);
address private immutable OWNER;
IPool private immutable POOL;
constructor(address aavePool) {
OWNER = msg.sender;
POOL = IPool(aavePool); // Pool-Adresse aus dem PoolAddressesProvider des jeweiligen Netzwerks beziehen
}
modifier onlyOwner() {
if (msg.sender != OWNER) revert Unauthorized();
_;
}
/// Einstiegspunkt. Leiht `amount` des Tokens `asset` und startet die Schleife in executeOperation.
function executeArbitrage(
address asset,
uint256 amount,
address tokenB,
address pair1,
address pair2,
uint256 minProfit
) external onlyOwner {
bytes memory params = abi.encode(tokenB, pair1, pair2, minProfit);
POOL.flashLoanSimple(address(this), asset, amount, params, 0);
}
/// Aave V3 Callback. Wird vom Pool nach Kreditvergabe aufgerufen.
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external returns (bool) {
// 1. Callback darf nur vom Aave-Pool aufgerufen werden...
if (msg.sender != address(POOL)) revert InvalidCaller();
// 2. ...und nur auf Anfrage dieses Contracts selbst
if (initiator != address(this)) revert InvalidInitiator();
(address tokenB, address pair1, address pair2, uint256 minProfit) =
abi.decode(params, (address, address, address, uint256));
// Der Kontostand enthält bereits die geliehenen Mittel
uint256 balanceBefore = IERC20(asset).balanceOf(address(this));
uint256 amountB = _swap(pair1, asset, amount);
_swap(pair2, tokenB, amountB);
// Gewinn = balanceAfter - balanceBefore - premium (Kreditgebühr)
uint256 balanceAfter = IERC20(asset).balanceOf(address(this));
uint256 required = balanceBefore + premium + minProfit;
if (balanceAfter < required) revert InsufficientProfit(balanceAfter, required);
// Aave zieht die Schuld via transferFrom selbst ein, daher ist ein approve erforderlich
_safeApprove(asset, address(POOL), amount + premium);
return true;
}
function withdraw(address token, uint256 amount) external onlyOwner {
_safeTransfer(token, OWNER, amount);
}
function _swap(address pair, address tokenIn, uint256 amountIn) private returns (uint256 amountOut) {
(uint256 r0, uint256 r1, ) = IUniswapV2Pair(pair).getReserves();
bool zeroForOne = tokenIn == IUniswapV2Pair(pair).token0();
(uint256 reserveIn, uint256 reserveOut) = zeroForOne ? (r0, r1) : (r1, r0);
// Uniswap V2 Formel (0,3 % Gebühr)
uint256 amountInWithFee = amountIn * 997;
amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
_safeTransfer(tokenIn, pair, amountIn);
IUniswapV2Pair(pair).swap(
zeroForOne ? 0 : amountOut,
zeroForOne ? amountOut : 0,
address(this),
"" // leeres data: normaler Swap, kein Flash Swap
);
}
function _safeTransfer(address token, address to, uint256 amount) private {
if (token.code.length == 0) revert TransferFailed();
(bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.transfer, (to, amount)));
if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed();
}
function _safeApprove(address token, address spender, uint256 amount) private {
if (token.code.length == 0) revert ApproveFailed();
(bool ok, bytes memory data) = token.call(abi.encodeCall(IERC20.approve, (spender, amount)));
if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert ApproveFailed();
}
}
Die AWS-Infrastruktur-Sackgasse
Das Betreiben eines Such-Bots auf virtuellen Servern von Amazon Web Services oder Google Cloud verbrennt garantiert das Budget. Virtualisierung fügt dem Netzwerkstack Latenz (Jitter) hinzu, und gemeinsam genutzte Backbones neigen bei Spitzenlasten im Mempool zu Paketverlusten.
Ein wettbewerbsfähiges Setup verlangt nach einem physischen Dedicated Server (Bare-Metal) mit hoher Single-Core-Taktfrequenz, schnellem NVMe-Storage für einen lokalen Geth- oder Reth-Node und einer direkten Gigabit-Anbindung an Rechenzentren in Frankfurt. Das Ausführen eines eigenen Lightweight Nodes ist Pflicht, da öffentliche RPC-Provider wie Infura oder Alchemy die Auslieferungsgeschwindigkeit neuer Blöcke künstlich drosseln und Benachrichtigungen über Pending Transactions um kritische 100 bis 300 Millisekunden verzögern.
Den Gewinn im MEV streicht nicht ein, wer das komplexere mathematische Modell hat, sondern wer physisch am nächsten am Validator sitzt und Gaskosten bis hin zur kleinsten Assembler-Anweisung im Bytecode optimieren kann.