// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
// Target contract with a function that consumes some gas
contract Target {
uint256 private _storedData = 0;
function execute(bytes memory _data) external {
// Simulate some work that consumes gas
uint256 i;
while(i < 100) {
i++;
_storedData += i;
}
}
}
// Relayer contract vulnerable to insufficient gas griefing
contract Relayer {
mapping (bytes => bool) public executed; // Replay protection mapping
address public target;
constructor(address _target) {
target = _target;
}
function forward(bytes memory _data) public {
// Check replay protection
require(!executed[_data], "Replay protection");
executed[_data] = true;
// Vulnerability: External call is made, but its success status is NOT checked.
// If target.call runs out of gas (due to limited gas sent by attacker),
// this function DOES NOT revert. The state change above persists.
target.call(abi.encodeWithSignature("execute(bytes)", _data));
}
}
Функция Relayer.forward помечает параметр _data как выполненный до вызова контракта Target и, что критически важно, не проверяет возвращаемое значение успешности вызова target.call. Злоумышленник использует эту уязвимость, отправляя транзакцию с ровно таким количеством газа, которого хватит для выполнения проверки require и строки executed[_data] = true, но недостаточно для завершения внутренней логики вызова target.call. Внешний вызов завершается неудачно из-за нехватки газа, однако транзакция в контракте Relayer продолжается и успешно завершается, поскольку ошибка не была обнаружена. В результате флаг executed устанавливается в true, портя состояние системы и делая невозможным успешную передачу реального _data — даже несмотря на то, что функция Target.execute в рамках атакующей транзакции так и не была полностью выполнена.
Как поправить: Хотя общая рекомендация предлагает «явные проверки газа», для конкретной уязвимости (манипуляция потоком выполнения через нехватку газа при внешнем вызове) наиболее прямое и надежное решение — проверять статус успешности внешнего вызова.
Низкоуровневый метод .call возвращает булево значение success (успех). Это значение обязательно должно проверяться. Если success равно false (что происходит, если внешний вызов откатывается или исчерпывает газ), вызывающая функция должна также откатиться — обычно с помощью require(success, "Сообщение об ошибке"). Это предотвращает успешное завершение транзакции в несогласованном состоянии.
Использование require(gasleft() > MIN_GAS_NEEDED) перед внешним вызовом, как правило, менее эффективно для решения именно этой проблемы, поскольку фактическое потребление газа внешним контрактом может быть непредсказуемым или намеренно завышено злонамеренным целевым контрактом. Проверка require(success) корректно обрабатывает результат независимо от причины сбоя.
// Исправленный фрагмент функции forward (внутри Relayer)
function forward(bytes memory _data) public {
require(!executed[_data], "Защита от повторного выполнения");
executed[_data] = true;
// Взаимодействие: выполнение внешнего вызова
(bool success, ) = target.call(abi.encodeWithSignature("execute(bytes)", _data));
require(success, "Внешний вызов завершился неудачно");
}
Эта исправленная логика гарантирует, что транзакция завершится и флаг executed будет установлен в true только в случае реального успешного выполнения target.call. Она напрямую предотвращает атаку манипуляции газом, откатывая всю транзакцию при неудаче внешнего вызова и тем самым сохраняя согласованность состояния.
#griefing