Зависит ли логика контракта от порядка транзакций?
Описание: Майнеры контролируют порядок транзакций и могут использовать это для атак типа «фронт-раннинг», «бэк-раннинг» или «сэндвич».
Устранение: Реализуйте защиту, позволяя пользователям указывать приемлемые результаты, которые отменяют транзакции в случае нарушения.
Майнеры определяют порядок включения транзакций в блок. Хотя майнеры обычно отдают приоритет транзакциям с более высокими ценами на газ, они не обязаны этого делать. Это позволяет злонамеренным майнерам (или сложным ботам) стратегически упорядочивать транзакции в своих интересах. Такое использование может привести к фронтран, бекран или сэндвич атакам. Эти атаки используют манипуляции с порядком транзакций для извлечения выгоды.
Представьте себе децентрализованную биржу (DEX). Майнер видит в мемпуле крупный ордер на покупку определенного токена. Он может вставить свой собственный ордер на покупку перед крупным ордером (фронт-раннинг), что приведет к росту цены. Затем он может вставить свой ордер на продажу после крупного ордера (бэк-раннинг или сэндвич-атака), извлекая прибыль из роста цены, вызванного первоначальной сделкой.
Вот упрощенная уязвимая DEX:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// Simple ERC20 token for testing
contract TestToken is ERC20 {
constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
}
}
// Simplified DEX that's vulnerable to front-running
contract VulnerableDEX {
TestToken public tokenA;
TestToken public tokenB;
uint public reserveA;
uint public reserveB;
constructor(address _tokenA, address _tokenB) {
tokenA = TestToken(_tokenA);
tokenB = TestToken(_tokenB);
}
// Initialize liquidity
function addLiquidity(uint amountA, uint amountB) external {
tokenA.transferFrom(msg.sender, address(this), amountA);
tokenB.transferFrom(msg.sender, address(this), amountB);
reserveA += amountA;
reserveB += amountB;
}
// Calculate output amount for a given input
function _calculateSwapOutput(address tokenIn, uint amountIn) internal view returns (uint amountOut) {
require(tokenIn == address(tokenA) || tokenIn == address(tokenB), "Invalid token");
bool isTokenA = tokenIn == address(tokenA);
if (isTokenA) {
amountOut = (reserveB * amountIn) / (reserveA + amountIn);
require(amountOut < reserveB, "Insufficient liquidity");
} else {
amountOut = (reserveA * amountIn) / (reserveB + amountIn);
require(amountOut < reserveA, "Insufficient liquidity");
}
return amountOut;
}
// Execute the swap with pre-calculated output
function _executeSwap(address tokenIn, uint amountIn, uint amountOut, address sender) internal {
bool isTokenA = tokenIn == address(tokenA);
if (isTokenA) {
tokenA.transferFrom(sender, address(this), amountIn);
// Update reserves
reserveA += amountIn;
reserveB -= amountOut;
// Transfer output tokens to the user
tokenB.transfer(sender, amountOut);
} else {
tokenB.transferFrom(sender, address(this), amountIn);
reserveB += amountIn;
reserveA -= amountOut;
tokenA.transfer(sender, amountOut);
}
}
// Vulnerable swap function (no minimum output)
function swap(address tokenIn, uint amountIn) external returns (uint amountOut) {
// Calculate the expected output
amountOut = _calculateSwapOutput(tokenIn, amountIn);
// Execute the swap
_executeSwap(tokenIn, amountIn, amountOut, msg.sender);
return amountOut;
}
}