Процесс может выглядеть следующим образом:
- Проверить, существует ли пул.
- Если пул существует, приступить к взаимодействию (например, добавить ликвидность).
- Если пул не существует, создать его с желаемыми параметрами, а затем приступить к взаимодействию.
И вот тут-то и начинаются проблемы! При наивной реализации в рамках одной транзакции паттерн «get-or-create» может быть уязвим для фронт-раннинга. Злоумышленник может отслеживать мемпул на предмет транзакций, пытающихся создать определенный актив (например, наш VulnerablePool, связанный с адресом _poolCreator).
Увидев параметры, которые планирует использовать жертва, злоумышленник может быстро отправить свою собственную транзакцию, вызывая ту же функцию (getOrCreatePool), но с другими, вредоносными параметрами (например, с подправленной _initialPrice). Заплатив более высокую комиссию за газ, злоумышленник гарантирует, что его транзакция будет выполнена первой. Это создает актив под идентификатором жертвы, но с параметрами злоумышленника, перехватывая этап создания.
Когда исходная транзакция жертвы наконец выполняется, проверка (address(pools[_poolCreator]) == address(0)) обнаруживает, что ресурс уже существует. Этап создания пропускается, и функция возвращает созданный злоумышленником ресурс с измененными параметрами. Не подозревающая об этом жертва затем взаимодействует с этим скомпрометированным ресурсом.
Это похоже на то, как если бы вы зарезервировали определенную комнату для встреч, а кто-то проник туда раньше вас и заменил проектор на неисправный. В итоге вы используете комнату, но с подделанным оборудованием.
Вот этот паттерн в коде:
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract VulnerablePool {
address public poolCreator;
uint256 public initialPrice;
constructor(address _poolCreator, uint256 _initialPrice) {
poolCreator = _poolCreator;
initialPrice = _initialPrice;
}
}
contract ExploitableContract {
mapping(address => VulnerablePool) public pools;
function getOrCreatePool(address _poolCreator, uint256 _initialPrice)
public returns (VulnerablePool)
{
if (address(pools[_poolCreator]) == address(0)) {
// An attacker can front-run this transaction with different initialPrice
pools[_poolCreator] = new VulnerablePool(_poolCreator, _initialPrice);
}
return pools[_poolCreator];
}
function viewPoolInitialPrice(address _poolCreator) public view returns (uint256) {
if(address(pools[_poolCreator]) != address(0)) {
return pools[_poolCreator].initialPrice();
} else {
return 0;
}
}
}
Ниже приведен тестовый случай, демонстрирующий эту уязвимость. Тест имитирует атаку «фронт-раннинг» путем создания пула с начальной ценой 50, в то время как жертва намеревалась создать его с начальной ценой 100. Злоумышленник успешно осуществляет «фронт-раннинг» транзакции жертвы, и в результате жертва взаимодействует с пулом злоумышленника, а не со своим собственным.