Вместо push, позвольте пользователям самим забирать свои токены. Они инициируют перевод из контракта. Это как дать им ключ от хранилища. Выглядит это так:
Теперь, если вывод средств пользователя не удается, это затрагивает только его самого. Контракт продолжает функционировать для всех остальных. Это и есть правильная децентрализация! Оператор require проверяет, был ли перевод успешным. В случае неудачи транзакция отменяется только для запрашивающего пользователя, что оказывает минимальное влияние на текущие операции.
В примере, приведенном в пункте чеклиста, подробно описан сценарий, в котором комиссии переводятся владельцу до вывода средств пользователем. Если адрес владельца случайно установлен на нулевой адрес или если владелец является, э-э-э, злонамеренным контрактом, который ревертит перевод токенов, вывод средств пользователями не удастся. Это неприятная ошибка для отладки! Минимальный пример и PoC, написанные в Foundry, доступны здесь.
Далее разберем еще два случая атаки.
#dos
// Pull Pattern: Much Safer!
mapping(address => uint) public withdrawableBalances;
// Step 1: Admin marks funds as withdrawable without actually sending them
function startBatchWithdrawal() public {
address[] memory users = getUsers(); // imaginary function to get users
for (uint i = 0; i < users.length; i++) {
uint amount = balances[users[i]];
if (amount > 0) {
balances[users[i]] = 0;
withdrawableBalances[users[i]] += amount;
}
}
}
// Step 2: Each user withdraws their own funds individually
function withdraw() public {
uint amount = withdrawableBalances[msg.sender];
require(amount > 0, "No funds to withdraw");
withdrawableBalances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
Теперь, если вывод средств пользователя не удается, это затрагивает только его самого. Контракт продолжает функционировать для всех остальных. Это и есть правильная децентрализация! Оператор require проверяет, был ли перевод успешным. В случае неудачи транзакция отменяется только для запрашивающего пользователя, что оказывает минимальное влияние на текущие операции.
В примере, приведенном в пункте чеклиста, подробно описан сценарий, в котором комиссии переводятся владельцу до вывода средств пользователем. Если адрес владельца случайно установлен на нулевой адрес или если владелец является, э-э-э, злонамеренным контрактом, который ревертит перевод токенов, вывод средств пользователями не удастся. Это неприятная ошибка для отладки! Минимальный пример и PoC, написанные в Foundry, доступны здесь.
Далее разберем еще два случая атаки.
#dos
👍2❤1
Атака Denial-of-Service. Часть 2
Существует ли минимальная сумма транзакции?
Намек: Вы предотвращаете «пылевые» транзакции (крошечные, незначительные суммы), которые забивают ваш контракт и делают все неиспользуемым?
Здесь речь идет о предотвращении спама, просто и понятно. Если ваш контракт позволяет пользователям взаимодействовать с ним на любую сумму (даже на крошечные, незначительные доли токенов), злоумышленники могут затопить его бесчисленными транзакциями с нулевой или почти нулевой стоимостью. Все эти транзакции потребляют газ и делают законные операции гораздо более дорогостоящими. Это увеличение затрат может потенциально достичь предела лимита газа в блоке. Если это произойдет, нормальные пользователи не смогут взаимодействовать с вашим протоколом. Вот как выглядит уязвимый код:
Решение: ввести минимальное требование!
Простой оператор require может значительно улучшить ситуацию. Введите пороговое значение с помощью оператора require и действуйте как охранник, проверяющий удостоверение личности на входе в клуб. Нам также необходимо обеспечить возможность обработки запросов на снятие средств пакетами. Вот как это сделать:
В примере чеклиста показан контракт, в котором пользователи могут запрашивать вывод буквально любой суммы (даже нулевой), независимо от ее размера. Злоумышленник может воспользоваться этим, чтобы создать огромную очередь запросов на вывод с нулевой стоимостью. Это делает обработку законных запросов на вывод чрезмерно дорогой, что обходится пользователям в огромные расходы на газ. Минимальный пример и PoC, написанные в Foundry, доступны здесь.
#foundry #dos
Существует ли минимальная сумма транзакции?
Намек: Вы предотвращаете «пылевые» транзакции (крошечные, незначительные суммы), которые забивают ваш контракт и делают все неиспользуемым?
Здесь речь идет о предотвращении спама, просто и понятно. Если ваш контракт позволяет пользователям взаимодействовать с ним на любую сумму (даже на крошечные, незначительные доли токенов), злоумышленники могут затопить его бесчисленными транзакциями с нулевой или почти нулевой стоимостью. Все эти транзакции потребляют газ и делают законные операции гораздо более дорогостоящими. Это увеличение затрат может потенциально достичь предела лимита газа в блоке. Если это произойдет, нормальные пользователи не смогут взаимодействовать с вашим протоколом. Вот как выглядит уязвимый код:
// Vulnerable to dust attacks
struct WithdrawalRequest {
address user;
uint amount;
}
WithdrawalRequest[] public withdrawalRequests;
// Anyone can submit withdrawal requests for ANY amount (even 1 wei!)
function requestWithdrawal(uint amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
// Add to the global withdrawal queue - no minimum amount check!
withdrawalRequests.push(WithdrawalRequest(msg.sender, amount));
}
function processWithdrawals() external onlyOwner {
for (uint256 i = 0; i < withdrawalRequests.length; i++) { // This amplifies the attack even more because it tries to handle all the requests at once
WithdrawalRequest memory request = withdrawalRequests[i];
request.user.transfer(request.amount);
}
withdrawalRequests = new WithdrawalRequest[](0);
}
Решение: ввести минимальное требование!
Простой оператор require может значительно улучшить ситуацию. Введите пороговое значение с помощью оператора require и действуйте как охранник, проверяющий удостоверение личности на входе в клуб. Нам также необходимо обеспечить возможность обработки запросов на снятие средств пакетами. Вот как это сделать:
uint public minimumWithdrawal = 0.1 ether;
function requestWithdrawal(uint amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
require(amount >= minimumWithdrawal, "Amount below minimum withdrawal threshold"); // Bouncer!
balances[msg.sender] -= amount;
withdrawalRequests.push(WithdrawalRequest(msg.sender, amount));
}
function processWithdrawals(uint count) external onlyOwner { // We can process in batches now
for (uint256 i = 0; i < count; i++) {
WithdrawalRequest memory request = withdrawalRequests[i];
request.user.transfer(request.amount);
}
for (uint i = 0; i < withdrawalRequests.length - count; i++) {
withdrawalRequests[i] = withdrawalRequests[i + count];
}
withdrawalRequests.length = withdrawalRequests.length - count;
}
В примере чеклиста показан контракт, в котором пользователи могут запрашивать вывод буквально любой суммы (даже нулевой), независимо от ее размера. Злоумышленник может воспользоваться этим, чтобы создать огромную очередь запросов на вывод с нулевой стоимостью. Это делает обработку законных запросов на вывод чрезмерно дорогой, что обходится пользователям в огромные расходы на газ. Минимальный пример и PoC, написанные в Foundry, доступны здесь.
#foundry #dos
👍8❤1
Атака Denial-of-Service. Часть 3
Как протокол обрабатывает токены с функцией черного списка?
Уточнение: Вы учитываете последствия использования токенов (например, USDC и подобных), которые могут вносить пользовательские адреса в черный список?
Это гораздо более сложный вопрос, который имеет огромное значение в современном мире регулируемых стейблкоинов. Некоторые токены имеют функцию черного списка. Это означает, что центральный орган владеющий токеном может заморозить или полностью заблокировать использование токена для определенных адресов.
Почему это потенциальная бомба замедленного действия?
Представьте себе контракт сообщества, в котором друзья, члены семьи или инвестиционные партнеры объединяют свои токены в «группу стейкинга». Звучит кооперативно и эффективно, не так ли? Каждому члену назначается процент вознаграждения в зависимости от его вклада.
Но вот, где дело становится опасно интересным: что произойдет, если только один член вашей стейкинг-группы попадет в черный список токена? Возможно, ваш двоюродный брат оказался в каком-то списке регулирующих органов, или адрес вашего друга был помечен из-за какой-то совершенно не связанной с этим транзакции. Для остальных членов группы это не имеет большого значения, верно? Неверно!
Когда ваша группа пытается вывести свои вознаграждения, вся транзакция терпит крах! Почему? Потому что контракт пытается распределить вознаграждения всем участникам в рамках одной транзакции. Если один перевод не удается, вся операция отменяется! Совокупные токены вашей группы на сумму 100 ETH? Полностью заблокированы! Ваши запланированные выводы? Невозможны! И все из-за того, что один из участников попал в черный список, что может не иметь никакого отношения к вашей стейкинговой деятельности!
Знаете, что еще хуже? Обычно нет возможности удалить члена, занесенного в черный список, или перераспределить доли. Средства группы фактически замораживаются до тех пор, пока черный список токенов не будет обновлен — а это может никогда не произойти, если занесение в черный список было сделано по регуляторным причинам.
Это не теоретическая проблема — это происходит в реальных контрактах сегодня! Единственная точка отказа, затрагивающая сразу нескольких пользователей. Механизм коллективного наказания, о котором никто не просил! Страшно, не правда ли?
Вот как это выглядит в коде:
Как протокол обрабатывает токены с функцией черного списка?
Уточнение: Вы учитываете последствия использования токенов (например, USDC и подобных), которые могут вносить пользовательские адреса в черный список?
Это гораздо более сложный вопрос, который имеет огромное значение в современном мире регулируемых стейблкоинов. Некоторые токены имеют функцию черного списка. Это означает, что центральный орган владеющий токеном может заморозить или полностью заблокировать использование токена для определенных адресов.
Почему это потенциальная бомба замедленного действия?
Представьте себе контракт сообщества, в котором друзья, члены семьи или инвестиционные партнеры объединяют свои токены в «группу стейкинга». Звучит кооперативно и эффективно, не так ли? Каждому члену назначается процент вознаграждения в зависимости от его вклада.
Но вот, где дело становится опасно интересным: что произойдет, если только один член вашей стейкинг-группы попадет в черный список токена? Возможно, ваш двоюродный брат оказался в каком-то списке регулирующих органов, или адрес вашего друга был помечен из-за какой-то совершенно не связанной с этим транзакции. Для остальных членов группы это не имеет большого значения, верно? Неверно!
Когда ваша группа пытается вывести свои вознаграждения, вся транзакция терпит крах! Почему? Потому что контракт пытается распределить вознаграждения всем участникам в рамках одной транзакции. Если один перевод не удается, вся операция отменяется! Совокупные токены вашей группы на сумму 100 ETH? Полностью заблокированы! Ваши запланированные выводы? Невозможны! И все из-за того, что один из участников попал в черный список, что может не иметь никакого отношения к вашей стейкинговой деятельности!
Знаете, что еще хуже? Обычно нет возможности удалить члена, занесенного в черный список, или перераспределить доли. Средства группы фактически замораживаются до тех пор, пока черный список токенов не будет обновлен — а это может никогда не произойти, если занесение в черный список было сделано по регуляторным причинам.
Это не теоретическая проблема — это происходит в реальных контрактах сегодня! Единственная точка отказа, затрагивающая сразу нескольких пользователей. Механизм коллективного наказания, о котором никто не просил! Страшно, не правда ли?
Вот как это выглядит в коде:
🐳1
contract GroupStaking {
IERC20 public token;
struct StakingGroup {
uint256 id;
uint256 totalAmount;
address[] members;
uint256[] weights;
bool exists;
}
// Mapping from group ID to group data
mapping(uint256 => StakingGroup) public stakingGroups;
// Current group ID counter
uint256 public nextGroupId = 1;
constructor(IERC20 _token) {
token = _token;
}
// Create a new staking group
function createStakingGroup(address[] calldata _members, uint256[] calldata _weights) external returns (uint256) {
require(_members.length > 0, "Empty members list");
require(_members.length == _weights.length, "Members and weights length mismatch");
// Validate weights sum to 100%
uint256 totalWeight = 0;
for (uint256 i = 0; i < _weights.length; i++) {
totalWeight += _weights[i];
}
require(totalWeight == 100, "Weights must sum to 100");
uint256 groupId = nextGroupId;
stakingGroups[groupId] = StakingGroup({
id: groupId,
totalAmount: 0,
members: _members,
weights: _weights,
exists: true
});
nextGroupId++;
return groupId;
}
// Stake tokens to a group
function stakeToGroup(uint256 _groupId, uint256 _amount) external {
require(stakingGroups[_groupId].exists, "Group does not exist");
require(token.transferFrom(msg.sender, address(this), _amount), "Transfer failed");
stakingGroups[_groupId].totalAmount += _amount;
}
// Withdraw tokens from a group with rewards distributed according to weights
function withdrawFromGroup(uint256 _groupId, uint256 _amount) external {
StakingGroup storage group = stakingGroups[_groupId];
require(group.exists, "Group does not exist");
require(group.totalAmount >= _amount, "Insufficient group balance");
// Only a group member can initiate a withdrawal
bool isMember = false;
for (uint256 i = 0; i < group.members.length; i++) {
if (group.members[i] == msg.sender) {
isMember = true;
break;
}
}
require(isMember, "Not a group member");
// Update the group's total amount
group.totalAmount -= _amount;
// Distribute the withdrawn amount to all members according to their weights
// VULNERABLE: If any member is blacklisted, the entire distribution fails
for (uint256 i = 0; i < group.members.length; i++) {
uint256 memberShare = (_amount * group.weights[i]) / 100;
if (memberShare > 0) {
token.transfer(group.members[i], memberShare);
}
}
}
// Get group info
function getGroupInfo(uint256 _groupId) external view returns (
uint256 id,
uint256 totalAmount,
address[] memory members,
uint256[] memory weights
) {
StakingGroup storage group = stakingGroups[_groupId];
require(group.exists, "Group does not exist");
return (
group.id,
group.totalAmount,
group.members,
group.weights
);
}
}Решение: учитывайте возможность попадания в черный список.
К сожалению, здесь нет единственно «правильного» ответа. Каждая ситуация уникальна. Все зависит от структуры вашего протокола, терпимости к рискам и даже от ваших юридических соглашений с пользователями. По крайней мере, имейте в виду такую возможность и разработайте резервный механизм, обеспечивающий соблюдение нормативных требований.
Этот пункт чеклиста заставляет вас критически подумать о внешних зависимостях и их потенциальном влиянии на основные функции вашего протокола. Речь идет о том, чтобы предвидеть худшие сценарии и иметь хотя бы какой-то запасной план.
Минимальный пример и PoC, написанные в Foundry, доступны здесь.
#dos
👍5❤2🐳2
Атака Denial-of-Service. Часть 4
Может ли злоумышленник заблокировать или предотвратить обработку очереди транзакций, чтобы вызвать сбой в работе сервиса?
Проблема: если ваш смарт-контракт использует очереди для обработки задач, злоумышленник может манипулировать определенным статусом в очереди, чтобы предотвратить правильную обработку.
Исправление: Ваш механизм обработки очередей требует надежной обработки ошибок и механизмов резервного копирования, чтобы обеспечить продолжение процесса даже при возникновении проблем.
Рассмотрим пример очереди вывода средств:
1. Пользователи запрашивают вывод средств, и некоторые флаги указывают, что запрос активен.
2. Злоумышленники могут использовать resetUserStatus после того, как их вывод средств поставлен в очередь, и помешать другим пользователям сделать то же самое.
Для того, чтобы воспользоваться этой уязвимостью, злоумышленник может выполнить следующие действия:
- Инициировать запрос на снятие средств.
- Вызвать функцию resetUserStatus.
- Функция processNextWithdrawal вернется в исходное состояние, что приведет к непрерывной DoS-атаке.
Устранение уязвимости:
- Ограничить изменение статуса withdrawalRequested администратором.
- Обеспечить проверку валидности, чтобы избежать транзакций с нулевым значением.
- Реализовать резервную функцию для обработки непредвиденных ошибок.
#dos #queue
Может ли злоумышленник заблокировать или предотвратить обработку очереди транзакций, чтобы вызвать сбой в работе сервиса?
Проблема: если ваш смарт-контракт использует очереди для обработки задач, злоумышленник может манипулировать определенным статусом в очереди, чтобы предотвратить правильную обработку.
Исправление: Ваш механизм обработки очередей требует надежной обработки ошибок и механизмов резервного копирования, чтобы обеспечить продолжение процесса даже при возникновении проблем.
Рассмотрим пример очереди вывода средств:
1. Пользователи запрашивают вывод средств, и некоторые флаги указывают, что запрос активен.
2. Злоумышленники могут использовать resetUserStatus после того, как их вывод средств поставлен в очередь, и помешать другим пользователям сделать то же самое.
// VULNERABLE FUNCTION: Can be exploited
function resetUserStatus() external {
// Anyone can reset their status while remaining in the queue
withdrawalRequested[msg.sender] = false;
// Note: User is not removed from the queue!
}
// Process the next withdrawal in the queue
function processNextWithdrawal() external {
require(withdrawalQueue.length > currentIndex, "No withdrawals to process");
// Get the next withdrawal
Withdrawal memory withdrawal = withdrawalQueue[currentIndex];
// VULNERABLE: This check can be exploited by an attacker by resetting the status
require(withdrawalRequested[withdrawal.user], "Withdrawal no longer requested");
// Process the withdrawal
uint256 amount = withdrawal.amount;
require(balances[withdrawal.user] >= amount, "Insufficient balance");
// Update balance
balances[withdrawal.user] -= amount;
// Reset withdrawal request
withdrawalRequested[withdrawal.user] = false;
// Send funds
(bool success, ) = payable(withdrawal.user).call{value:amount}("");
require(success, "Failed to send funds");
// Move to next in queue
currentIndex++;
}
Для того, чтобы воспользоваться этой уязвимостью, злоумышленник может выполнить следующие действия:
- Инициировать запрос на снятие средств.
- Вызвать функцию resetUserStatus.
- Функция processNextWithdrawal вернется в исходное состояние, что приведет к непрерывной DoS-атаке.
Устранение уязвимости:
- Ограничить изменение статуса withdrawalRequested администратором.
- Обеспечить проверку валидности, чтобы избежать транзакций с нулевым значением.
- Реализовать резервную функцию для обработки непредвиденных ошибок.
#dos #queue
🔥3❤1👍1
Атака Denial-of-Service. Часть 5
Могут ли токены с низким количеством десятичных знаков (decimal) вызвать DoS?
Проблема: токены с низким decimals могут привести к проблемам с целочисленным делением, в результате чего происходит округление в меньшую сторону до нуля.
Представьте себе контракт потоковой передачи токенов (streaming contract), который распределяет токены в течение определенного периода времени. Если tokensPerSecond округляется до нуля из-за целочисленного деления с токенами с низким количеством десятичных знаков, функция распределения будет заблокирована.
Как предотвратить: реализуйте логику для обработки низких десятичных знаков, которая предотвращает сбой процесса транзакции из-за ошибок округления.
Пример:
Рассмотрим контракт TokenStream, который передает определенное количество токенов пользователям, где:
- total_tokens необходимо перевести в контракт;
- token_per_second может быть округлено до нуля, поскольку мы используем токен с 1 десятичным знаком;
- функция distributeTokens будет отменена.
Тест testDOSWithLowDecimalTokens в TokenStreamTest в этом случае вернется к исходному состоянию.
Исправление: убедитесь, что контракт правильно обрабатывает токены с небольшими decimals, масштабируя математические формулы для смягчения округления целых чисел во время вычислений.
#dos #decimals
Могут ли токены с низким количеством десятичных знаков (decimal) вызвать DoS?
Проблема: токены с низким decimals могут привести к проблемам с целочисленным делением, в результате чего происходит округление в меньшую сторону до нуля.
Представьте себе контракт потоковой передачи токенов (streaming contract), который распределяет токены в течение определенного периода времени. Если tokensPerSecond округляется до нуля из-за целочисленного деления с токенами с низким количеством десятичных знаков, функция распределения будет заблокирована.
Как предотвратить: реализуйте логику для обработки низких десятичных знаков, которая предотвращает сбой процесса транзакции из-за ошибок округления.
Пример:
Рассмотрим контракт TokenStream, который передает определенное количество токенов пользователям, где:
- total_tokens необходимо перевести в контракт;
- token_per_second может быть округлено до нуля, поскольку мы используем токен с 1 десятичным знаком;
- функция distributeTokens будет отменена.
contract TokenStream {
IERC20 public token;
uint256 public streamDuration;
uint256 public tokensPerSecond;
constructor(IERC20 _token, uint256 _streamDuration, uint256 _tokensPerSecond) {
token = _token;
streamDuration = _streamDuration;
tokensPerSecond = _tokensPerSecond;
}
function distributeTokens(address recipient) external {
uint256 balance = token.balanceOf(address(this));
uint256 amount = tokensPerSecond * streamDuration;
uint256 tokensToSend = amount > balance ? balance : amount;
require(tokensToSend > 0, "Insufficient tokens to stream");
token.transfer(recipient, tokensToSend);
}
}
contract LowDecimalToken is ERC20 {
constructor() ERC20("LowDecimalToken", "LDT") {
_mint(msg.sender, 100000 * (10 ** decimals()));
}
function decimals() public view virtual override returns (uint8) {
return 1; // Simulate a low decimal token
}
}Тест testDOSWithLowDecimalTokens в TokenStreamTest в этом случае вернется к исходному состоянию.
Исправление: убедитесь, что контракт правильно обрабатывает токены с небольшими decimals, масштабируя математические формулы для смягчения округления целых чисел во время вычислений.
#dos #decimals
👍2🤔2
Атака Denial-of-Service. Часть 6
Безопасно ли протокол обрабатывает взаимодействия с внешними контрактами?
Проблема: многие смарт-контракты полагаются на взаимодействия с внешними контрактами. Неожиданное поведение внешних контрактов может привести к сбою всей системы. Неспособность обработать эти внешние ошибки приводит к уязвимости DoS.
Исправление: обеспечьте надежную обработку ошибок при взаимодействии с внешними контрактами, чтобы защитить целостность протокола независимо от их производительности.
Пример:
Рассмотрим контракт, который взаимодействует с внешним фидом цен Chainlink. Без надлежащей обработки ошибок с помощью try/catch любой откат транзакции из внешнего фида будет каскадироваться вверх, и весь вызов будет ревертиться.
Как исправить: оберните вызовы внешних контрактов в блоки try/catch для обработки возвращенных ошибок и реализуйте резервный вариант или кэшированное значение.
Примечание: существует особый случай, когда внешний контракт намеренно тратит газ, что может привести к сбою блока catch! Мы обсудим это позже.
#dos #external
Безопасно ли протокол обрабатывает взаимодействия с внешними контрактами?
Проблема: многие смарт-контракты полагаются на взаимодействия с внешними контрактами. Неожиданное поведение внешних контрактов может привести к сбою всей системы. Неспособность обработать эти внешние ошибки приводит к уязвимости DoS.
Исправление: обеспечьте надежную обработку ошибок при взаимодействии с внешними контрактами, чтобы защитить целостность протокола независимо от их производительности.
Пример:
Рассмотрим контракт, который взаимодействует с внешним фидом цен Chainlink. Без надлежащей обработки ошибок с помощью try/catch любой откат транзакции из внешнего фида будет каскадироваться вверх, и весь вызов будет ревертиться.
contract PriceDependentContract {
AggregatorV3Interface public priceFeed;
constructor(address _priceFeed) {
priceFeed = AggregatorV3Interface(_priceFeed);
}
// Vulnerable function that retrieves the price without handling potential Chainlink reverts
function getPrice() public view returns (uint256) {
(, int256 price, , , ) = priceFeed.latestRoundData(); // Vulnerable line: No error handling
require(price > 0, "Price must be positive");
return uint256(price);
}
function calculateSomethingImportant() public view returns (uint256) {
uint256 price = getPrice();
// ... some important calculation using the price
return price * 2;
}Как исправить: оберните вызовы внешних контрактов в блоки try/catch для обработки возвращенных ошибок и реализуйте резервный вариант или кэшированное значение.
Примечание: существует особый случай, когда внешний контракт намеренно тратит газ, что может привести к сбою блока catch! Мы обсудим это позже.
#dos #external
👍4
Всех с Днем Знаний!
Знаете, я тут подумал о Solidity и о том, как всё изменилось. Раньше это было как на американских горках: каждый месяц выходило какое-то обновление, добавлялись новые фичи, разрабатывались программы и плагины и все пытались изобрести свою, самую хитрую архитектуру смарт контракта. Сейчас же всё как-то устаканилось. Язык стал взрослым и надежным, как старый добрый швейцарский армейский нож — в нем есть всё нужное, и он просто работает без сюрпризов (ну, при должном подходе).
Из-за этого все сейчас как будто пришли к негласному соглашению: «давайте делать всё по проверенным шаблонам». Зачем рисковать и выдумывать велосипед, если есть готовые, протестированные всеми практики реализации стандартов вроде тех же ERC-20 для токенов? Это как собирать мебель из IKEA — есть инструкция, и если ей следовать, то всё будет стоять крепко и не развалится. Так гораздо безопаснее и спокойнее для всех.
И знаете, что самое крутое? Сейчас учиться — одно удовольствие! Раньше уроки устаревали, не успев выйти. А сейчас можно открыть любой годный курс или гайд и быть уверенным, что основы уже не поменяются через полгода. Не нужно бежать за поездом, который постоянно ускоряется. Можно спокойно разбираться в нюансах, учиться правильно тестировать код и понимать, как избегать классических ловушек.
В общем, мне кажется, мы сейчас живем в золотое время для разработчика. Экосистема повзрослела, перестала бросаться из крайности в крайность и стала надежным фундаментом. Можно наконец-то не отвлекаться на постоянные новшества, а просто сосредоточиться и строить что-то классное и долговечное. По-моему, это офигенный прогресс!
Solidity прекрасен! Вы прекрасны! С Днем Знаний!
#solidity
Знаете, я тут подумал о Solidity и о том, как всё изменилось. Раньше это было как на американских горках: каждый месяц выходило какое-то обновление, добавлялись новые фичи, разрабатывались программы и плагины и все пытались изобрести свою, самую хитрую архитектуру смарт контракта. Сейчас же всё как-то устаканилось. Язык стал взрослым и надежным, как старый добрый швейцарский армейский нож — в нем есть всё нужное, и он просто работает без сюрпризов (ну, при должном подходе).
Из-за этого все сейчас как будто пришли к негласному соглашению: «давайте делать всё по проверенным шаблонам». Зачем рисковать и выдумывать велосипед, если есть готовые, протестированные всеми практики реализации стандартов вроде тех же ERC-20 для токенов? Это как собирать мебель из IKEA — есть инструкция, и если ей следовать, то всё будет стоять крепко и не развалится. Так гораздо безопаснее и спокойнее для всех.
И знаете, что самое крутое? Сейчас учиться — одно удовольствие! Раньше уроки устаревали, не успев выйти. А сейчас можно открыть любой годный курс или гайд и быть уверенным, что основы уже не поменяются через полгода. Не нужно бежать за поездом, который постоянно ускоряется. Можно спокойно разбираться в нюансах, учиться правильно тестировать код и понимать, как избегать классических ловушек.
В общем, мне кажется, мы сейчас живем в золотое время для разработчика. Экосистема повзрослела, перестала бросаться из крайности в крайность и стала надежным фундаментом. Можно наконец-то не отвлекаться на постоянные новшества, а просто сосредоточиться и строить что-то классное и долговечное. По-моему, это офигенный прогресс!
Solidity прекрасен! Вы прекрасны! С Днем Знаний!
#solidity
👏21👍5❤1
Аудит смарт контрактов изменился, нужно меняться и вам
Аудит смарт-контрактов за последние годы изменился до неузнаваемости. Раньше всё было проще: смотришь код, ловишь очевидные ошибки — переполнения, недостаточные проверки доступа, утечки средств. Баги были грубые, но и последствия от них — большие. Потом, по мере того как проекты становились сложнее, начали всплывать уязвимости не в синтаксисе, а в самой логике. Что-то работает технически правильно, но при этом позволяет вытянуть все деньги из пула, если чуть-чуть подтолкнуть систему в нужную сторону. И тут уже нужно было не просто читать код, а понимать, как он живёт в сети, как взаимодействует с другими контрактами, как реагирует на разные сценарии.
На помощь пришли инструменты. Появились штуки вроде Ceritora — формальная верификация, где ты математически доказываешь, что контракт делает то, что должен. Потом halmos, medusa — фреймворки, которые могут перебирать миллионы путей выполнения, чтобы найти, где что сломается. И, конечно, статические анализаторы вроде Slither или 4nalyer — они как радар: быстро сканируют код и подсвечивают подозрительные места. Удобно, полезно, теперь это база.
Но настоящий виток — это, конечно, нейросети. Сейчас почти все с ними в той или иной форме работают. Кто-то просто кидает контракт в chatGPT и верит, что модель сама найдёт уязвимости. Ну, знаешь, типа: «вот, посмотри, тут всё норм?» — и доверяется ответу. Другие используют нейросети, чтобы красиво оформить отчёт — сформулировать мысли чётко, без воды, чтобы клиент или судьи поняли суть.
А есть те, кто подошёл иначе. Для них нейросеть — не ментор и не помощник, который говорит: «смотри сюда». Это инструмент, как мощный микроскоп или осциллограф. Ты сам решаешь, что проверять, куда копать, а модель помогает ускорить процесс. Вместо того чтобы тратить три часа на то, чтобы разобраться, зачем нужна та или иная функция, как она связана с другими, что может пойти не так — ты можешь за пару минут получить гипотезу, проверить её, перепроверить, построить сценарии атаки.
Но это, честно говоря, сложнее. Потому что теперь ты отвечаешь не только за код, но и за то, что говорит модель. Нужно уметь задавать правильные вопросы, понимать, когда она ошибается, когда просто «догадывается», а когда реально анализирует. Нужно держать контроль.
И вот в этом и есть разница. Кто-то позволяет нейросети вести себя за руку, а кто-то сам идёт вперёд, а модель просто делает путь быстрее и светлее. Как ты с этим справишься — покажет, насколько ты действительно сильный аудитор. Не потому что ты знаешь все инструменты, а потому что умеешь ими пользоваться, не теряя головы.
#audit
Аудит смарт-контрактов за последние годы изменился до неузнаваемости. Раньше всё было проще: смотришь код, ловишь очевидные ошибки — переполнения, недостаточные проверки доступа, утечки средств. Баги были грубые, но и последствия от них — большие. Потом, по мере того как проекты становились сложнее, начали всплывать уязвимости не в синтаксисе, а в самой логике. Что-то работает технически правильно, но при этом позволяет вытянуть все деньги из пула, если чуть-чуть подтолкнуть систему в нужную сторону. И тут уже нужно было не просто читать код, а понимать, как он живёт в сети, как взаимодействует с другими контрактами, как реагирует на разные сценарии.
На помощь пришли инструменты. Появились штуки вроде Ceritora — формальная верификация, где ты математически доказываешь, что контракт делает то, что должен. Потом halmos, medusa — фреймворки, которые могут перебирать миллионы путей выполнения, чтобы найти, где что сломается. И, конечно, статические анализаторы вроде Slither или 4nalyer — они как радар: быстро сканируют код и подсвечивают подозрительные места. Удобно, полезно, теперь это база.
Но настоящий виток — это, конечно, нейросети. Сейчас почти все с ними в той или иной форме работают. Кто-то просто кидает контракт в chatGPT и верит, что модель сама найдёт уязвимости. Ну, знаешь, типа: «вот, посмотри, тут всё норм?» — и доверяется ответу. Другие используют нейросети, чтобы красиво оформить отчёт — сформулировать мысли чётко, без воды, чтобы клиент или судьи поняли суть.
А есть те, кто подошёл иначе. Для них нейросеть — не ментор и не помощник, который говорит: «смотри сюда». Это инструмент, как мощный микроскоп или осциллограф. Ты сам решаешь, что проверять, куда копать, а модель помогает ускорить процесс. Вместо того чтобы тратить три часа на то, чтобы разобраться, зачем нужна та или иная функция, как она связана с другими, что может пойти не так — ты можешь за пару минут получить гипотезу, проверить её, перепроверить, построить сценарии атаки.
Но это, честно говоря, сложнее. Потому что теперь ты отвечаешь не только за код, но и за то, что говорит модель. Нужно уметь задавать правильные вопросы, понимать, когда она ошибается, когда просто «догадывается», а когда реально анализирует. Нужно держать контроль.
И вот в этом и есть разница. Кто-то позволяет нейросети вести себя за руку, а кто-то сам идёт вперёд, а модель просто делает путь быстрее и светлее. Как ты с этим справишься — покажет, насколько ты действительно сильный аудитор. Не потому что ты знаешь все инструменты, а потому что умеешь ими пользоваться, не теряя головы.
#audit
1👍12🔥4👏1🤔1
Немного про мой процесс самообучения
Когда я начал погружаться в machine learning и deep learning — совершенно новые для меня области, — меня снова охватило то самое ощущение неуверенности, с которым я уже сталкивался при изучении Solidity. Это состояние, когда чувствуешь, что не хватает базы, не понимаешь, с чего начать, куда двигаться, и возникает ощущение, будто ты отстаёшь, даже если только стартовал.
Уже около месяца я систематически изучаю ML/DL. За это время освоил Python, разобрался с NumPy и Pandas, изучил базовые принципы нейронных сетей и ряд других тем. Однако математическая часть — линейная алгебра, градиентный спуск, операции с векторами и матрицами — до сих пор даётся с трудом. Уже несколько недель я возвращаюсь к одним и тем же концепциям, пытаясь не просто запомнить, а именно понять.
И здесь я пришёл к важному выводу: обучение — это не про количество прочитанных уроков или просмотренных видео. Это про глубину понимания. Можно пройти десять курсов подряд, но если ты не можешь объяснить тему своими словами, не можешь воспроизвести логику, не понимаешь, почему формула выглядит именно так — значит, знания не усвоены. Они временно залегли в памяти, но не стали частью мышления.
Я давно для себя определил: настоящий прогресс начинается тогда, когда ты перестаёшь просто потреблять информацию и начинаешь с ней работать. Если что-то непонятно — нельзя просто пропустить, надеясь, что «всё встанет на свои места позже». Нужно остановиться, разобраться, докопаться. В случае с математикой ML я не ограничиваюсь учебником. Каждый непонятный символ, каждый логический переход, каждая формула, которую не могу воспроизвести самостоятельно, становится поводом для глубокого погружения. Я ищу дополнительные материалы, смотрю лекции, задаю вопросы нейросетям, прося расписать выводы по шагам, объяснить интуицию за формулами, показать аналогии.
Например, сейчас я читаю книгу Дайзенрота «Математика в машинном обучении». Но чтение — это только отправная точка. На каждой странице у меня десятки пометок: что непонятно, где теряется логика, какие шаги пропущены. И каждая такая пометка требует отдельной работы. Иногда на одну страницу уходит несколько часов: диалоги с ИИ, поиск объяснений, конспектирование, переписывание формул с комментариями. И даже после всего этого чувствуется, что где-то что-то ускользает — и это нормально. Главное, что я не просто читаю, а работаю с материалом.
Вот в чём разница между поверхностным изучением и настоящим пониманием: первое заканчивается там, где заканчивается урок, второе — продолжается за его пределами. Нужно быть готовым тратить время, задавать вопросы, искать разные объяснения, пока не щёлкнет. Потому что только тогда знания становятся твоими.
Особенно это важно, когда ты начинаешь с нуля. Без фундамента сложно оценить, что сейчас критически важно, а что — деталь, которую можно отложить. Но если не разобраться в основах, дальше будет только сложнее. Поэтому лучше потратить больше времени сейчас, чем позже натолкнуться на непреодолимый барьер из непонятных концепций.
Я убеждён: ключ к освоению сложных тем — в настойчивом стремлении к пониманию. Не в том, сколько вы прочитали, а в том, насколько глубоко вы проработали материал. Даже если сегодня что-то не до конца ясно — главное, что вы не прошли мимо, а попытались разобраться. Со временем эти усилия складываются, и однажды вы замечаете, что всё, что раньше казалось запутанным, теперь выстраивается в единую, логичную картину.
Поэтому, если вы учитесь чему-то новому, не ограничивайтесь пассивным потреблением. Останавливайтесь на каждом «не понял», возвращайтесь, ищите объяснения, пересказывайте своими словами. Понимание — не приходит само, его нужно выстраивать.
#edu
Когда я начал погружаться в machine learning и deep learning — совершенно новые для меня области, — меня снова охватило то самое ощущение неуверенности, с которым я уже сталкивался при изучении Solidity. Это состояние, когда чувствуешь, что не хватает базы, не понимаешь, с чего начать, куда двигаться, и возникает ощущение, будто ты отстаёшь, даже если только стартовал.
Уже около месяца я систематически изучаю ML/DL. За это время освоил Python, разобрался с NumPy и Pandas, изучил базовые принципы нейронных сетей и ряд других тем. Однако математическая часть — линейная алгебра, градиентный спуск, операции с векторами и матрицами — до сих пор даётся с трудом. Уже несколько недель я возвращаюсь к одним и тем же концепциям, пытаясь не просто запомнить, а именно понять.
И здесь я пришёл к важному выводу: обучение — это не про количество прочитанных уроков или просмотренных видео. Это про глубину понимания. Можно пройти десять курсов подряд, но если ты не можешь объяснить тему своими словами, не можешь воспроизвести логику, не понимаешь, почему формула выглядит именно так — значит, знания не усвоены. Они временно залегли в памяти, но не стали частью мышления.
Я давно для себя определил: настоящий прогресс начинается тогда, когда ты перестаёшь просто потреблять информацию и начинаешь с ней работать. Если что-то непонятно — нельзя просто пропустить, надеясь, что «всё встанет на свои места позже». Нужно остановиться, разобраться, докопаться. В случае с математикой ML я не ограничиваюсь учебником. Каждый непонятный символ, каждый логический переход, каждая формула, которую не могу воспроизвести самостоятельно, становится поводом для глубокого погружения. Я ищу дополнительные материалы, смотрю лекции, задаю вопросы нейросетям, прося расписать выводы по шагам, объяснить интуицию за формулами, показать аналогии.
Например, сейчас я читаю книгу Дайзенрота «Математика в машинном обучении». Но чтение — это только отправная точка. На каждой странице у меня десятки пометок: что непонятно, где теряется логика, какие шаги пропущены. И каждая такая пометка требует отдельной работы. Иногда на одну страницу уходит несколько часов: диалоги с ИИ, поиск объяснений, конспектирование, переписывание формул с комментариями. И даже после всего этого чувствуется, что где-то что-то ускользает — и это нормально. Главное, что я не просто читаю, а работаю с материалом.
Вот в чём разница между поверхностным изучением и настоящим пониманием: первое заканчивается там, где заканчивается урок, второе — продолжается за его пределами. Нужно быть готовым тратить время, задавать вопросы, искать разные объяснения, пока не щёлкнет. Потому что только тогда знания становятся твоими.
Особенно это важно, когда ты начинаешь с нуля. Без фундамента сложно оценить, что сейчас критически важно, а что — деталь, которую можно отложить. Но если не разобраться в основах, дальше будет только сложнее. Поэтому лучше потратить больше времени сейчас, чем позже натолкнуться на непреодолимый барьер из непонятных концепций.
Я убеждён: ключ к освоению сложных тем — в настойчивом стремлении к пониманию. Не в том, сколько вы прочитали, а в том, насколько глубоко вы проработали материал. Даже если сегодня что-то не до конца ясно — главное, что вы не прошли мимо, а попытались разобраться. Со временем эти усилия складываются, и однажды вы замечаете, что всё, что раньше казалось запутанным, теперь выстраивается в единую, логичную картину.
Поэтому, если вы учитесь чему-то новому, не ограничивайтесь пассивным потреблением. Останавливайтесь на каждом «не понял», возвращайтесь, ищите объяснения, пересказывайте своими словами. Понимание — не приходит само, его нужно выстраивать.
#edu
🔥17❤5💯1
Front-running атаки. Часть 1
Сегодня мы посмотрим на другую, не менее коварную уязвимость: атаки с опережением или Front-Running атаки.
Для иллюстрации представьте, что вы находитесь на оживленном фермерском рынке, где цены меняются в зависимости от спроса. Вы замечаете выгодную сделку по редким трюфелям за 50 долларов, которые, как вы знаете, в других местах стоят 100 долларов. Когда вы подходите, чтобы купить их, инсайдер рынка, который может видеть все поступающие заказы клиентов, замечает ваше намерение. Он быстро проскакивает вперед в очереди, покупает трюфели за 50 долларов, а затем сразу же предлагает продать их вам за 90 долларов. Вы все равно получаете трюфели, но теперь этот посредник заработал 40 долларов, которые могли бы быть вашими.
Это и есть фронт-раннинг в двух словах — увидеть чужую ожидающую транзакцию в публичной системе, а затем вставить свою собственную транзакцию перед ней, чтобы извлечь выгоду из изменения цены, о котором вы знаете.
В блокчейне это происходит, когда злоумышленники используют прозрачность мемпула, чтобы увидеть предстоящие транзакции. Затем они создают свои собственные транзакции с более высокими комиссиями за газ, гарантируя, что их транзакции будут выполнены первыми. Это может привести к ужасным последствиям, от манипулирования ценами на децентрализованных биржах (DEX) до кражи токенов NFT.
Понимание front-running атак
Прежде чем перейти к самому чек-листу, давайте начнем с некоторых важных определений.
1. Атака с опережением: злоумышленник наблюдает за ожидающей транзакцией в мемпуле. Чтобы получить выгоду из этой транзакции, он быстро выполняет свою собственную транзакцию, опережая ее. Обычно для этого он предлагает более высокую цену газа, чтобы его транзакция была обработана первой.
2. Паттерн «Get-or-create»: паттерн проектирования смарт контракта, который предполагает либо извлечение существующих активов (например, существующей торговой пары на DEX), либо создание новых, если они не существуют. Этот паттерн может быть уязвим для фронт-раннинга, если он не защищен должным образом.
3. Фронт-раннинг с двумя транзакциями: эта уязвимость возникает, когда критическое действие в смарт-контракте разделяется на две или более отдельных транзакций. Злоумышленник может использовать временной разрыв между этими транзакциями, чтобы вставить свою собственную операцию, потенциально нарушив запланированный поток или похитив активы.
4. Атака «пыль»: специфическая форма фронт-раннинга, при которой злоумышленник отправляет транзакцию с незначительной суммой («пыль») перед законной транзакцией. Это приводит к сбою (реверсии) законной транзакции из-за измененного состояния или невыполненных предварительных условий.
5. Схема «Commit-reveal»: криптографический протокол, разработанный для обеспечения справедливости и секретности в ситуациях, когда участники должны предоставить информацию, не раскрывая ее сразу. Он состоит из двух отдельных этапов: этапа «commit», на котором участники предоставляют криптографическое обязательство (обычно хэш их предполагаемого действия в сочетании со случайным секретом или nonce), и этапа «reveal», на котором они раскрывают исходное действие и секрет по истечении определенного периода времени. Этот процесс гарантирует, что ни один участник не может изменить свою заявку или реагировать на действия других, пока все обязательства не будут окончательно оформлены. Некоторые версии протокола гарантируют, что только лицо, которое взяло на себя обязательство, может раскрыть его позже. Это предотвращает вмешательство других лиц и обеспечивает ответственность участников.
SOL-AM-FrA-1: Защищены ли шаблоны «get-or-create» от атак front-running?
Сегодня мы посмотрим на другую, не менее коварную уязвимость: атаки с опережением или Front-Running атаки.
Для иллюстрации представьте, что вы находитесь на оживленном фермерском рынке, где цены меняются в зависимости от спроса. Вы замечаете выгодную сделку по редким трюфелям за 50 долларов, которые, как вы знаете, в других местах стоят 100 долларов. Когда вы подходите, чтобы купить их, инсайдер рынка, который может видеть все поступающие заказы клиентов, замечает ваше намерение. Он быстро проскакивает вперед в очереди, покупает трюфели за 50 долларов, а затем сразу же предлагает продать их вам за 90 долларов. Вы все равно получаете трюфели, но теперь этот посредник заработал 40 долларов, которые могли бы быть вашими.
Это и есть фронт-раннинг в двух словах — увидеть чужую ожидающую транзакцию в публичной системе, а затем вставить свою собственную транзакцию перед ней, чтобы извлечь выгоду из изменения цены, о котором вы знаете.
В блокчейне это происходит, когда злоумышленники используют прозрачность мемпула, чтобы увидеть предстоящие транзакции. Затем они создают свои собственные транзакции с более высокими комиссиями за газ, гарантируя, что их транзакции будут выполнены первыми. Это может привести к ужасным последствиям, от манипулирования ценами на децентрализованных биржах (DEX) до кражи токенов NFT.
Понимание front-running атак
Прежде чем перейти к самому чек-листу, давайте начнем с некоторых важных определений.
1. Атака с опережением: злоумышленник наблюдает за ожидающей транзакцией в мемпуле. Чтобы получить выгоду из этой транзакции, он быстро выполняет свою собственную транзакцию, опережая ее. Обычно для этого он предлагает более высокую цену газа, чтобы его транзакция была обработана первой.
2. Паттерн «Get-or-create»: паттерн проектирования смарт контракта, который предполагает либо извлечение существующих активов (например, существующей торговой пары на DEX), либо создание новых, если они не существуют. Этот паттерн может быть уязвим для фронт-раннинга, если он не защищен должным образом.
3. Фронт-раннинг с двумя транзакциями: эта уязвимость возникает, когда критическое действие в смарт-контракте разделяется на две или более отдельных транзакций. Злоумышленник может использовать временной разрыв между этими транзакциями, чтобы вставить свою собственную операцию, потенциально нарушив запланированный поток или похитив активы.
4. Атака «пыль»: специфическая форма фронт-раннинга, при которой злоумышленник отправляет транзакцию с незначительной суммой («пыль») перед законной транзакцией. Это приводит к сбою (реверсии) законной транзакции из-за измененного состояния или невыполненных предварительных условий.
5. Схема «Commit-reveal»: криптографический протокол, разработанный для обеспечения справедливости и секретности в ситуациях, когда участники должны предоставить информацию, не раскрывая ее сразу. Он состоит из двух отдельных этапов: этапа «commit», на котором участники предоставляют криптографическое обязательство (обычно хэш их предполагаемого действия в сочетании со случайным секретом или nonce), и этапа «reveal», на котором они раскрывают исходное действие и секрет по истечении определенного периода времени. Этот процесс гарантирует, что ни один участник не может изменить свою заявку или реагировать на действия других, пока все обязательства не будут окончательно оформлены. Некоторые версии протокола гарантируют, что только лицо, которое взяло на себя обязательство, может раскрыть его позже. Это предотвращает вмешательство других лиц и обеспечивает ответственность участников.
SOL-AM-FrA-1: Защищены ли шаблоны «get-or-create» от атак front-running?
❤3🔥1
Рассмотрим сценарий с фабричным контрактом (ExploitableContract), предназначенным для управления экземплярами VulnerablePool. Предполагается, что пользователь (идентифицируемый как _poolCreator) вызывает getOrCreatePool, указывая _initialPrice. Если пул для этого создателя не существует, контракт создает его. В противном случае он возвращает существующий пул. Этот паттерн «get-or-create» является распространенным выбором при проектировании смарт контрактов. Представьте себе сценарий, в котором необходимо создать пул для обмена, и кто-то хочет добавить ликвидность.
Процесс может выглядеть следующим образом:
- Проверить, существует ли пул.
- Если пул существует, приступить к взаимодействию (например, добавить ликвидность).
- Если пул не существует, создать его с желаемыми параметрами, а затем приступить к взаимодействию.
И вот тут-то и начинаются проблемы! При наивной реализации в рамках одной транзакции паттерн «get-or-create» может быть уязвим для фронт-раннинга. Злоумышленник может отслеживать мемпул на предмет транзакций, пытающихся создать определенный актив (например, наш VulnerablePool, связанный с адресом _poolCreator).
Увидев параметры, которые планирует использовать жертва, злоумышленник может быстро отправить свою собственную транзакцию, вызывая ту же функцию (getOrCreatePool), но с другими, вредоносными параметрами (например, с подправленной _initialPrice). Заплатив более высокую комиссию за газ, злоумышленник гарантирует, что его транзакция будет выполнена первой. Это создает актив под идентификатором жертвы, но с параметрами злоумышленника, перехватывая этап создания.
Когда исходная транзакция жертвы наконец выполняется, проверка (address(pools[_poolCreator]) == address(0)) обнаруживает, что ресурс уже существует. Этап создания пропускается, и функция возвращает созданный злоумышленником ресурс с измененными параметрами. Не подозревающая об этом жертва затем взаимодействует с этим скомпрометированным ресурсом.
Это похоже на то, как если бы вы зарезервировали определенную комнату для встреч, а кто-то проник туда раньше вас и заменил проектор на неисправный. В итоге вы используете комнату, но с подделанным оборудованием.
Вот этот паттерн в коде:
Ниже приведен тестовый случай, демонстрирующий эту уязвимость. Тест имитирует атаку «фронт-раннинг» путем создания пула с начальной ценой 50, в то время как жертва намеревалась создать его с начальной ценой 100. Злоумышленник успешно осуществляет «фронт-раннинг» транзакции жертвы, и в результате жертва взаимодействует с пулом злоумышленника, а не со своим собственным.
Процесс может выглядеть следующим образом:
- Проверить, существует ли пул.
- Если пул существует, приступить к взаимодействию (например, добавить ликвидность).
- Если пул не существует, создать его с желаемыми параметрами, а затем приступить к взаимодействию.
И вот тут-то и начинаются проблемы! При наивной реализации в рамках одной транзакции паттерн «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. Злоумышленник успешно осуществляет «фронт-раннинг» транзакции жертвы, и в результате жертва взаимодействует с пулом злоумышленника, а не со своим собственным.
👍4
function testFrontRunning() public {
uint256 intendedPrice = 100;
uint256 attackPrice = 50;
// 1. Victim intends to create a pool with initialPrice = intendedPrice
vm.startPrank(victim);
// exploitableContract.getOrCreatePool(victim, intendedPrice); //Simulate that the victim is about to call this.
// 2. Attacker observes this transaction and front-runs it with attackPrice
vm.stopPrank();
vm.startPrank(attacker);
exploitableContract.getOrCreatePool(victim, attackPrice);
vm.stopPrank();
// 3. Victim's transaction now executes
vm.startPrank(victim);
exploitableContract.getOrCreatePool(victim, intendedPrice); // This call should not change the price because the pool already exists
// 4. Assert that the pool's initialPrice is now the attacker's price, NOT the victim's intended price
assertEq(exploitableContract.viewPoolInitialPrice(victim), attackPrice, "The pool's initial price should be the attacker's price.");
vm.stopPrank();
}Как избежать этой уязвимости?
1. Разделите создание актива и взаимодействие с ним на две отдельные транзакции.
2. Прроверьте параметры целевого ресурса и вернитесь назад, если они неверны.
#frontrun
👍4❤2
Front-running атаки. Часть 2
Во многих смарт контрактах для выполнения определенных действий требуется несколько транзакций. Например, пользователь может сначала одобрить контракт на расходование своих токенов, а затем вызвать функцию для выполнения транзакции. Этот двухэтапный процесс может быть уязвим для атак типа фронт-раннинг, если он не разработан тщательно.
Контракт NFTRefinanceMarket в приведенном ниже примере предназначен для работы в качестве торговой площадки или депозитария, где владельцы NFT могут депонировать свои NFT, возможно, в качестве залога для получения кредита или участия в другой децентрализованной финансовой деятельности (DeFi). Предполагаемый рабочий процесс использует стандартный механизм утверждения ERC-721 и требует от владельца NFT двух отдельных транзакций:
1. Транзакция approve: владелец NFT сначала вызывает функцию approve() в самом контракте NFT, предоставляя адресу контракта NFTRefinanceMarket разрешение на управление конкретным tokenId.
2. Транзакция рефинансирования: затем владелец NFT вызывает функцию refinance() в контракте NFTRefinanceMarket, указав тот же tokenId. Эта функция предназначена для:
- Проверки наличия необходимого аппрува из шага 1.
- Передачу NFT от владельца в хранение контракту NFTRefinanceMarket.
- Запись адреса, который вызвал refinance() (предположительно, первоначальный владелец), в отображении tokenCreditors, связывая его с депонированным NFT.
По сути, это безопасный двухэтапный процесс, позволяющий владельцу зафиксировать свой NFT в маркет контракте и отслеживать свои права собственности/кредиторские права с помощью него. Вот как это выглядит:
Однако злоумышленник может опередить транзакцию, фактически похитив NFT, как показано ниже:
Во многих смарт контрактах для выполнения определенных действий требуется несколько транзакций. Например, пользователь может сначала одобрить контракт на расходование своих токенов, а затем вызвать функцию для выполнения транзакции. Этот двухэтапный процесс может быть уязвим для атак типа фронт-раннинг, если он не разработан тщательно.
Контракт NFTRefinanceMarket в приведенном ниже примере предназначен для работы в качестве торговой площадки или депозитария, где владельцы NFT могут депонировать свои NFT, возможно, в качестве залога для получения кредита или участия в другой децентрализованной финансовой деятельности (DeFi). Предполагаемый рабочий процесс использует стандартный механизм утверждения ERC-721 и требует от владельца NFT двух отдельных транзакций:
1. Транзакция approve: владелец NFT сначала вызывает функцию approve() в самом контракте NFT, предоставляя адресу контракта NFTRefinanceMarket разрешение на управление конкретным tokenId.
2. Транзакция рефинансирования: затем владелец NFT вызывает функцию refinance() в контракте NFTRefinanceMarket, указав тот же tokenId. Эта функция предназначена для:
- Проверки наличия необходимого аппрува из шага 1.
- Передачу NFT от владельца в хранение контракту NFTRefinanceMarket.
- Запись адреса, который вызвал refinance() (предположительно, первоначальный владелец), в отображении tokenCreditors, связывая его с депонированным NFT.
По сути, это безопасный двухэтапный процесс, позволяющий владельцу зафиксировать свой NFT в маркет контракте и отслеживать свои права собственности/кредиторские права с помощью него. Вот как это выглядит:
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
interface INFT is IERC721 {
function mint(address to, uint256 tokenId) external;
}
contract NFTRefinanceMarket is Ownable {
INFT public nft;
mapping(uint256 => address) public tokenCreditors;
constructor(INFT _nft) Ownable(msg.sender) {
nft = _nft;
}
function refinance(uint256 _tokenId) external {
// Check if this contract has approval to transfer the NFT
require(nft.getApproved(_tokenId) == address(this), "Not approved");
address originalOwner = nft.ownerOf(_tokenId);
// Pull the NFT into this contract as collateral
nft.transferFrom(originalOwner, address(this), _tokenId);
// Record the caller as the creditor for this NFT
tokenCreditors[_tokenId] = msg.sender;
}
}
Однако злоумышленник может опередить транзакцию, фактически похитив NFT, как показано ниже:
function testFrontRunRefinance() public {
// 1. User approves the contract to refinance their NFT
assertEq(nft.ownerOf(tokenId), user);
vm.startPrank(user);
nft.approve(address(nftRefinanceMarket), tokenId);
assertEq(nft.getApproved(tokenId), address(nftRefinanceMarket));
vm.stopPrank();
// 2. Attacker monitors the mempool, and before the user calls refinance, the attacker calls refinance
vm.startPrank(attacker);
// Simulate the attacker front-running the transaction
// In a real scenario, the attacker would increase the gas price to get their transaction included first
nftRefinanceMarket.refinance(tokenId);
// Verify that the attacker is now marked as the creditor for this NFT
assertEq(nftRefinanceMarket.tokenCreditors(tokenId), attacker);
// Verify that the NFT now belongs to the contract
assertEq(nft.ownerOf(tokenId), address(nftRefinanceMarket));
vm.stopPrank();
// 3. User tries to refinance the NFT, but it has already been refinanced by the attacker
vm.startPrank(user);
vm.expectRevert("Not approved");
nftRefinanceMarket.refinance(tokenId);
vm.stopPrank();
}❤1
Этот тип атаки происходит из-за разрыва между двумя транзакциями, когда злоумышленник может вмешаться между этими вызовами.
Как же этого избежать?
Внедрите проверки, чтобы убедиться, что первая транзакция принадлежит тому же пользователю, который вызывает вторую транзакцию. Например, требуйте, чтобы msg.sender функции refinance был тем же адресом, который изначально утвердил контракт.
#frontrun
Как же этого избежать?
Внедрите проверки, чтобы убедиться, что первая транзакция принадлежит тому же пользователю, который вызывает вторую транзакцию. Например, требуйте, чтобы msg.sender функции refinance был тем же адресом, который изначально утвердил контракт.
#frontrun
👍10
Front-running атаки. Часть 3
Могут ли пользователи злонамеренно вызвать отмену транзакций других пользователей, опередив их с помощью «пыли»?
Атаки с использованием «пыли» предполагают, что злоумышленники опережают законные транзакции с помощью крошечных сумм токенов, что может привести к сбою исходной транзакции. Изменяя состояние цепочки, транзакция злоумышленника делает исполнение транзакции жертвы недействительными, что в конечном итоге приводит к ее сбою. Это похоже на то, как слегка сдвинуть кусочек пазла, чтобы помешать кому-то собрать картинку.
Вот упрощенный сценарий:
Уязвимость в этом контракте аукциона заключается в том, что злоумышленник может помешать кому-либо начать аукцион с помощью атаки «пыль».
Если Алиса захочет начать аукцион и отправит транзакцию с вызовом startAuction(), злоумышленник Боб может опередить ее транзакцию, отправив минимальное количество токенов (пыль) на адрес контракта. Когда транзакция Алисы будет обработана, проверка require(token.balanceOf(address(this)) == 0, «Balance must be zero») завершится сбоем, поскольку баланс контракта больше не будет равен нулю, что приведет к отмене ее транзакции.
Для защиты от пылевых атак мы можем реализовать пороги допустимых отклонений вместо точных проверок баланса и использовать средства контроля доступа, чтобы ограничить круг лиц, которые могут запускать конфиденциальные функции.
#frontrun
Могут ли пользователи злонамеренно вызвать отмену транзакций других пользователей, опередив их с помощью «пыли»?
Атаки с использованием «пыли» предполагают, что злоумышленники опережают законные транзакции с помощью крошечных сумм токенов, что может привести к сбою исходной транзакции. Изменяя состояние цепочки, транзакция злоумышленника делает исполнение транзакции жертвы недействительными, что в конечном итоге приводит к ее сбою. Это похоже на то, как слегка сдвинуть кусочек пазла, чтобы помешать кому-то собрать картинку.
Вот упрощенный сценарий:
// Contract with a function that requires zero balance
contract Auction {
IERC20 public token;
event AuctionStarted(uint256 id);
constructor(address _token) {
token = IERC20(_token);
}
// Function that requires zero balance to execute
function startAuction() external returns (uint256) {
// Vulnerable check that can be exploited
require(token.balanceOf(address(this)) == 0, "Balance must be zero");
// Start auction logic would go here
uint256 id = 1;
emit AuctionStarted(id);
return id;
}
}
Уязвимость в этом контракте аукциона заключается в том, что злоумышленник может помешать кому-либо начать аукцион с помощью атаки «пыль».
Если Алиса захочет начать аукцион и отправит транзакцию с вызовом startAuction(), злоумышленник Боб может опередить ее транзакцию, отправив минимальное количество токенов (пыль) на адрес контракта. Когда транзакция Алисы будет обработана, проверка require(token.balanceOf(address(this)) == 0, «Balance must be zero») завершится сбоем, поскольку баланс контракта больше не будет равен нулю, что приведет к отмене ее транзакции.
Для защиты от пылевых атак мы можем реализовать пороги допустимых отклонений вместо точных проверок баланса и использовать средства контроля доступа, чтобы ограничить круг лиц, которые могут запускать конфиденциальные функции.
#frontrun
👍8
Front-running атаки. Часть 4
Использует ли протокол правильно привязанную к пользователю схему "commit-reveal"?
Схемы "commit-reveal" (фиксация-раскрытие) предназначены для защиты конфиденциальных действий в блокчейне, таких как ставки на аукционе, от утечки информации и упреждающих сделок (фронт-раннинга) в процессе подтверждения транзакции. В типичной схеме пользователи сначала отправляют в контракт commitment (криптографический хэш от планируемого действия и секретной "соли"). После окончания установленного периода фиксации начинается период раскрытия, в течение которого пользователи отправляют свое исходное действие и соль. Контракт проверяет, что хэш от переданных действия и соли совпадает с ранее сохраненным commitment'ом. Это похоже на то, как вы запечатываете свою ставку в конверте, подаете его и вскрываете только после того, как все ставки поданы.
Однако схема может быть уязвима, если сам commitment не привязан уникальным образом к пользователю, который его отправил. Плохо спроектированная схема может позволить злоумышленнику, который наблюдает за транзакцией commitment'а пользователя (например, в мемпуле), скопировать этот commitment или вмешаться в процесс раскрытия. Данная конкретная уязвимость фокусируется на том, как отсутствие идентификатора пользователя внутри хэша commitment'а позволяет атакующему сорвать аукцион. Это как если бы кто-то идеально скопировал содержимое вашего запечатанного конверта и подал его точную копию от своего имени.
Давайте рассмотрим пример:
Суть уязвимости, продемонстрированной кодом:
- Хэш commitment'а (keccak256(abi.encode(bid, salt))) вычисляется без включения адреса участника (msg.sender).
- Поскольку адрес участника не является частью хэша, любой, кто узнает ставку (bid) и соль (salt), использованные честным участником, может вычислить точно такой же хэш commitment'а.
- Атакующий наблюдает за транзакцией честного участника и вызывает commit() с идентичным хэшем. Теперь и адрес честного участника, и адрес атакующего связаны с одним и тем же значением bytes32 в маппинге commitments.
Использует ли протокол правильно привязанную к пользователю схему "commit-reveal"?
Схемы "commit-reveal" (фиксация-раскрытие) предназначены для защиты конфиденциальных действий в блокчейне, таких как ставки на аукционе, от утечки информации и упреждающих сделок (фронт-раннинга) в процессе подтверждения транзакции. В типичной схеме пользователи сначала отправляют в контракт commitment (криптографический хэш от планируемого действия и секретной "соли"). После окончания установленного периода фиксации начинается период раскрытия, в течение которого пользователи отправляют свое исходное действие и соль. Контракт проверяет, что хэш от переданных действия и соли совпадает с ранее сохраненным commitment'ом. Это похоже на то, как вы запечатываете свою ставку в конверте, подаете его и вскрываете только после того, как все ставки поданы.
Однако схема может быть уязвима, если сам commitment не привязан уникальным образом к пользователю, который его отправил. Плохо спроектированная схема может позволить злоумышленнику, который наблюдает за транзакцией commitment'а пользователя (например, в мемпуле), скопировать этот commitment или вмешаться в процесс раскрытия. Данная конкретная уязвимость фокусируется на том, как отсутствие идентификатора пользователя внутри хэша commitment'а позволяет атакующему сорвать аукцион. Это как если бы кто-то идеально скопировал содержимое вашего запечатанного конверта и подал его точную копию от своего имени.
Давайте рассмотрим пример:
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
/**
* Overview:
* Checklist Item ID: SOL-AM-FrA-4
*
* This test demonstrates a front-running vulnerability in a commit-reveal auction contract.
* The vulnerability is that the protocol doesn't include the committer's address in the commitment,
* allowing anyone to reveal another user's commitment and claim the reward.
*/
contract Auction {
mapping(address => bytes32) public commitments;
address public winner;
uint256 public winningBid;
bool public revealed;
uint256 public endTime;
constructor(uint256 _duration) {
endTime = block.timestamp + _duration;
}
// Users commit their bids with a salt
function commit(bytes32 commitment) public {
require(block.timestamp < endTime, "Auction ended");
commitments[msg.sender] = commitment;
}
// Vulnerable reveal function - doesn't include committer address in the commitment
function reveal(uint256 bid, bytes32 salt) public {
require(block.timestamp > endTime, "Reveal time not reached");
require(!revealed, "Already revealed");
// Vulnerability: commitment doesn't include the committer's address
// This allows anyone to create the same commitment with the same bid and salt
bytes32 expectedCommitment = keccak256(abi.encode(bid, salt));
// The attacker can commit the same value and then reveal it
require(commitments[msg.sender] == expectedCommitment, "Invalid commitment");
// The revealer becomes the winner
if (bid > winningBid) {
winningBid = bid;
winner = msg.sender;
}
revealed = true;
}
function claimReward() public view returns (address) {
require(block.timestamp > endTime && revealed, "Auction not ended or not revealed");
return winner;
}
}
Суть уязвимости, продемонстрированной кодом:
- Хэш commitment'а (keccak256(abi.encode(bid, salt))) вычисляется без включения адреса участника (msg.sender).
- Поскольку адрес участника не является частью хэша, любой, кто узнает ставку (bid) и соль (salt), использованные честным участником, может вычислить точно такой же хэш commitment'а.
- Атакующий наблюдает за транзакцией честного участника и вызывает commit() с идентичным хэшем. Теперь и адрес честного участника, и адрес атакующего связаны с одним и тем же значением bytes32 в маппинге commitments.
❤1🔥1
- Функция reveal() проверяет commitments[msg.sender] == expectedCommitment. Это означает, что она проверяет, зафиксировал ли ранее вызывающий reveal (то есть msg.sender в этом вызове) хэш, соответствующий предоставленным bid и salt.
Поскольку атакующий действительно зафиксировал идентичный хэш, он может успешно вызвать reveal(bid, salt). Если он сделает это раньше честного участника (например, заплатив более высокую комиссию за газ, чтобы его транзакция была обработана первой — front-run), он пройдет проверку commitments[атакующий] == expectedCommitment. Адрес атакующего (msg.sender внутри этого вызова reveal) затем будет установлен как победитель.
Как обеспечить уникальную привязку commitment'а к участнику?
- Включите адрес участника (committer) в данные, которые хэшируются при создании commitment'а (обычно это делается пользователем off-chain). Commitment должен вычисляться так: commitment = keccak256(abi.encode(msg.sender, bid, salt)) (могут быть и другие возможные дополнительные поля, например, chainId, nonce и т.д., которые можно добавить в зависимости от случая использования).
- Функция reveal затем должна воссоздать хэш, используя ту же структуру, включая msg.sender (тот, кто вызывает reveal): bytes32 expectedCommitment = keccak256(abi.encode(msg.sender, bid, salt));
#frontrun
contract AuctionTest is Test {
Auction public auction;
address public bidder1;
address public bidder2;
uint256 public auctionDuration = 1 days;
function setUp() public {
bidder1 = address(1);
bidder2 = address(2);
auction = new Auction(auctionDuration);
vm.warp(block.timestamp + 1 minutes);
}
function testFrontRunningReveal() public {
uint256 bid1 = 1 ether;
bytes32 salt1 = keccak256("secret1");
// Both bidders create the same commitment with the same bid and salt
// This is possible because the commitment doesn't include the committer's address
bytes32 commitment = keccak256(abi.encode(bid1, salt1));
// Bidder1 commits first
vm.prank(bidder1);
auction.commit(commitment);
// Attacker sees the bid and salt values (e.g., from mempool or other side channel)
// and commits the same values
vm.prank(bidder2);
auction.commit(commitment);
vm.warp(block.timestamp + auctionDuration);
// Attacker front-runs by revealing first
vm.prank(bidder2);
auction.reveal(bid1, salt1);
// Attacker becomes the winner by front-running
assertEq(auction.winner(), bidder2);
assertEq(auction.winningBid(), bid1);
}
}Поскольку атакующий действительно зафиксировал идентичный хэш, он может успешно вызвать reveal(bid, salt). Если он сделает это раньше честного участника (например, заплатив более высокую комиссию за газ, чтобы его транзакция была обработана первой — front-run), он пройдет проверку commitments[атакующий] == expectedCommitment. Адрес атакующего (msg.sender внутри этого вызова reveal) затем будет установлен как победитель.
Как обеспечить уникальную привязку commitment'а к участнику?
- Включите адрес участника (committer) в данные, которые хэшируются при создании commitment'а (обычно это делается пользователем off-chain). Commitment должен вычисляться так: commitment = keccak256(abi.encode(msg.sender, bid, salt)) (могут быть и другие возможные дополнительные поля, например, chainId, nonce и т.д., которые можно добавить в зависимости от случая использования).
- Функция reveal затем должна воссоздать хэш, используя ту же структуру, включая msg.sender (тот, кто вызывает reveal): bytes32 expectedCommitment = keccak256(abi.encode(msg.sender, bid, salt));
#frontrun
👍7
Griefing атаки. Часть 1
Давайте разбираться с очередным видом атак.
При грифинг атаке злоумышленник намерен нарушить работу или помешать законным пользователям выполнять желаемые действия в контракте. Злоумышленник часто несет расходы (например, комиссию за газ), не получая прямой финансовой выгоды от своих действий.
Термин «грифинг» (griefing), вероятно, происходит из сообществ онлайн-игр, где он описывает игроков, которые намеренно раздражают и донимают других, часто нарушая предполагаемый ход игры ради собственного развлечения, а не стратегического преимущества. Точно так же в контексте смарт контрактов грифинг атаки ставят в основу создание неудобств, а не извлечение прибыли.
Стоит отметить, что в сфере безопасности web3 термины «грифинг» и «отказ в обслуживании (DoS)» иногда используются как взаимозаменяемые, что может вызывать путаницу. Однако понимание их различий может прояснить цель каждой из атак.
Атака типа «отказ в обслуживании» (DoS) — это термин, укоренившийся в общей безопасности сетей и компьютеров. Ее цель — сделать сервис или сеть недоступными для всех пользователей на время или на неопределенный срок. Это может включать в себя перегрузку системы трафиком или эксплуатацию уязвимостей, которые предотвращают законный доступ для всех. Целью, как правило, является широкомасштабный срыв работы всего сервиса.
Масштаб и намерение являются основными различиями между этими двумя типами атак. Однако грань иногда может быть размытой, а это означает, что некоторые уязвимости могут относиться к любой из категорий. Крупная грифинг атака потенциально может привести к сценарию DoS для части пользователей или функций.
В конечном счете, точная категоризация менее важна, чем выявление уязвимости, понимание ее воздействия и внедрение эффективных мер по ее устранению.
Существует ли внешняя функция, которая зависит от состояний, которые могут быть изменены другими?
Тут злоумышленники могут предотвратить выполнение транзакций обычными пользователями, внося незначительные изменения в состояния в блокчейне.
Меры по устранению: Убедитесь, что обычные действия пользователей, особенно важные, такие как withdrawal (вывод средств) и repayment (погашение), не могут быть нарушены другими участниками.
Эта проверка направлена на предотвращение ситуаций, в которых злоумышленник может заблокировать важные действия жертвы, изменив общие переменные состояния, которые использует функция жертвы.
Если контракт позволяет любому изменять переменную состояния, от которой зависит выполнение критической операции другого пользователя (например, условие вывода, флаг или временная метка), злоумышленник может злонамеренно изменить это состояние, чтобы заблокировать жертву. Злоумышленник специально нацеливается на способность жертвы исполнять последующие транзакции, часто тратя на это собственный газ. то особенно практично в блокчейнах с низкой комиссией за транзакции.
Рассмотрим контракт VulnerableVault с отсроченным выводом.
Функция deposit(address _for) является слабым местом. Она позволяет вызывающему (msg.sender) указать любой адрес _for. При вызове этой функции обновляется lastDeposit[_for]. Злоумышленник может вызвать эту функцию, указав адрес своей жертвы в качестве _for, и отправить минимальную сумму (например, 1 вей). Это действие сбрасывает временную метку lastDeposit жертвы.
Давайте разбираться с очередным видом атак.
При грифинг атаке злоумышленник намерен нарушить работу или помешать законным пользователям выполнять желаемые действия в контракте. Злоумышленник часто несет расходы (например, комиссию за газ), не получая прямой финансовой выгоды от своих действий.
Термин «грифинг» (griefing), вероятно, происходит из сообществ онлайн-игр, где он описывает игроков, которые намеренно раздражают и донимают других, часто нарушая предполагаемый ход игры ради собственного развлечения, а не стратегического преимущества. Точно так же в контексте смарт контрактов грифинг атаки ставят в основу создание неудобств, а не извлечение прибыли.
Стоит отметить, что в сфере безопасности web3 термины «грифинг» и «отказ в обслуживании (DoS)» иногда используются как взаимозаменяемые, что может вызывать путаницу. Однако понимание их различий может прояснить цель каждой из атак.
Атака типа «отказ в обслуживании» (DoS) — это термин, укоренившийся в общей безопасности сетей и компьютеров. Ее цель — сделать сервис или сеть недоступными для всех пользователей на время или на неопределенный срок. Это может включать в себя перегрузку системы трафиком или эксплуатацию уязвимостей, которые предотвращают законный доступ для всех. Целью, как правило, является широкомасштабный срыв работы всего сервиса.
Масштаб и намерение являются основными различиями между этими двумя типами атак. Однако грань иногда может быть размытой, а это означает, что некоторые уязвимости могут относиться к любой из категорий. Крупная грифинг атака потенциально может привести к сценарию DoS для части пользователей или функций.
В конечном счете, точная категоризация менее важна, чем выявление уязвимости, понимание ее воздействия и внедрение эффективных мер по ее устранению.
Существует ли внешняя функция, которая зависит от состояний, которые могут быть изменены другими?
Тут злоумышленники могут предотвратить выполнение транзакций обычными пользователями, внося незначительные изменения в состояния в блокчейне.
Меры по устранению: Убедитесь, что обычные действия пользователей, особенно важные, такие как withdrawal (вывод средств) и repayment (погашение), не могут быть нарушены другими участниками.
Эта проверка направлена на предотвращение ситуаций, в которых злоумышленник может заблокировать важные действия жертвы, изменив общие переменные состояния, которые использует функция жертвы.
Если контракт позволяет любому изменять переменную состояния, от которой зависит выполнение критической операции другого пользователя (например, условие вывода, флаг или временная метка), злоумышленник может злонамеренно изменить это состояние, чтобы заблокировать жертву. Злоумышленник специально нацеливается на способность жертвы исполнять последующие транзакции, часто тратя на это собственный газ. то особенно практично в блокчейнах с низкой комиссией за транзакции.
Рассмотрим контракт VulnerableVault с отсроченным выводом.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableVault {
uint256 public delay = 60 minutes;
mapping(address => uint256) public lastDeposit;
mapping(address => uint256) public balances;
function deposit(address _for) public payable {
lastDeposit[_for] = block.timestamp;
balances[_for] += msg.value;
}
function withdraw(uint256 _amount) public {
require(block.timestamp >= lastDeposit[msg.sender] + delay,
"Wait period not over");
require(balances[msg.sender] >= _amount, "Insufficient funds");
balances[msg.sender] -= _amount;
(bool success,) = payable(msg.sender).call{value: _amount}("");
require(success, "Transfer failed");
}
}
Функция deposit(address _for) является слабым местом. Она позволяет вызывающему (msg.sender) указать любой адрес _for. При вызове этой функции обновляется lastDeposit[_for]. Злоумышленник может вызвать эту функцию, указав адрес своей жертвы в качестве _for, и отправить минимальную сумму (например, 1 вей). Это действие сбрасывает временную метку lastDeposit жертвы.
❤3
Поскольку функция withdraw проверяет lastDeposit[msg.sender], действие злоумышленника напрямую мешает жертве (когда она является msg.sender в вызове withdraw) удовлетворить условию по времени. Злоумышленник усугубляет положение жертвы, сбрасывая ее таймер вывода средств.
Исправление: запретить пользователям изменять критические переменные состояния, принадлежащие другим пользователям. Самым простым способом исправления здесь является ограничение функции deposit только обновлением состояния вызывающего (msg.sender).
Теперь только Алиса может обновлять lastDeposit[alice].
#griefing
Исправление: запретить пользователям изменять критические переменные состояния, принадлежащие другим пользователям. Самым простым способом исправления здесь является ограничение функции deposit только обновлением состояния вызывающего (msg.sender).
// Corrected deposit function (inside VulnerableVault)
function deposit() public payable {
// Only affects the caller's state
lastDeposit[msg.sender] = block.timestamp;
balances[msg.sender] += msg.value;
}
Теперь только Алиса может обновлять lastDeposit[alice].
#griefing
👍6
Griefing атаки. Часть 2
Могут ли операции контракта быть манипулированы с помощью точного указания лимита газа?
Злоумышленники могут указывать тщательно рассчитанные объемы газа, чтобы принудительно направить выполнение контракта по определенному пути, тем самым манипулируя его поведением неожиданным образом.
Предоставляя ровно столько газа, сколько необходимо для одних шагов, но недостаточно для других, злоумышленник может обойти проверки, оставить контракт в несогласованном состоянии или выборочно вызвать сбои операций. Это приводит к отказу в обслуживании или другим непредвиденным последствиям.
Ярким примером такой манипуляции является "груминг" (намеренное причинение вреда) из-за недостаточного газа при внешних вызовах. Хотя в рекомендациях по устранению упоминаются «явные проверки газа», которые часто означают require(gasleft() > MIN_GAS_NEEDED), данная конкретная атака, связанная с внешними вызовами, наиболее эффективно предотвращается иным способом.
В этом сценарии злоумышленник использует контракт, который вызывает внешний контракт, но не проверяет, успешно ли завершился этот внешний вызов. Злоумышленник формирует транзакцию, указывая ровно столько газа, чтобы выполнить логику вызывающего контракта до момента внешнего вызова и, возможно, даже обновить внутреннее состояние преждевременно, но недостаточно газа для успешного завершения внешнего вызова в целевом контракте. Если вызывающий контракт не проверяет статус успеха, он может завершить свое выполнение, полагая, что все прошло успешно, оставляя систему в несогласованном состоянии, где внутренние записи не соответствуют реальному результату неудачного внешнего взаимодействия.
Продемонстрируем это на примере контракта Relayer:
1. Функция forward контракта Relayer предназначена для выполнения действия через внешний вызов контракта Target.
2. Ключевой момент: функция forward обновляет внутреннее состояние (помечает запрос _data как выполненный для защиты от повторного выполнения), прежде чем совершается внешний вызов или подтверждается его успешность.
3. Функция не проверяет статус успешности, возвращаемый вызовом target.call(...).
4. Злоумышленник вызывает forward, указывая тщательно рассчитанный лимит газа: достаточно для прохождения проверки require и обновления executed[_data] = true, но недостаточно для успешного завершения последующего вызова target.call(...) внутри контракта Target.
5. Вызов target.call исчерпывает выделенный газ и молча завершается с ошибкой (с точки зрения контракта Relayer, поскольку его успешность не проверяется). Однако функция forward завершается «успешно».
6. В результате возникает несогласованное состояние: отображение executed в контракте Relayer показывает, что действие выполнено, но необходимая внешняя операция в контракте Target фактически не произошла. Конкретная транзакция легитимного пользователя (_data) теперь навсегда заблокирована из-за механизма защиты от повторного выполнения, который по сути подвергся цензуре со стороны злоумышленника, воспользовавшегося механикой газа и отсутствием проверки ошибок.
Могут ли операции контракта быть манипулированы с помощью точного указания лимита газа?
Злоумышленники могут указывать тщательно рассчитанные объемы газа, чтобы принудительно направить выполнение контракта по определенному пути, тем самым манипулируя его поведением неожиданным образом.
Предоставляя ровно столько газа, сколько необходимо для одних шагов, но недостаточно для других, злоумышленник может обойти проверки, оставить контракт в несогласованном состоянии или выборочно вызвать сбои операций. Это приводит к отказу в обслуживании или другим непредвиденным последствиям.
Ярким примером такой манипуляции является "груминг" (намеренное причинение вреда) из-за недостаточного газа при внешних вызовах. Хотя в рекомендациях по устранению упоминаются «явные проверки газа», которые часто означают require(gasleft() > MIN_GAS_NEEDED), данная конкретная атака, связанная с внешними вызовами, наиболее эффективно предотвращается иным способом.
В этом сценарии злоумышленник использует контракт, который вызывает внешний контракт, но не проверяет, успешно ли завершился этот внешний вызов. Злоумышленник формирует транзакцию, указывая ровно столько газа, чтобы выполнить логику вызывающего контракта до момента внешнего вызова и, возможно, даже обновить внутреннее состояние преждевременно, но недостаточно газа для успешного завершения внешнего вызова в целевом контракте. Если вызывающий контракт не проверяет статус успеха, он может завершить свое выполнение, полагая, что все прошло успешно, оставляя систему в несогласованном состоянии, где внутренние записи не соответствуют реальному результату неудачного внешнего взаимодействия.
Продемонстрируем это на примере контракта Relayer:
1. Функция forward контракта Relayer предназначена для выполнения действия через внешний вызов контракта Target.
2. Ключевой момент: функция forward обновляет внутреннее состояние (помечает запрос _data как выполненный для защиты от повторного выполнения), прежде чем совершается внешний вызов или подтверждается его успешность.
3. Функция не проверяет статус успешности, возвращаемый вызовом target.call(...).
4. Злоумышленник вызывает forward, указывая тщательно рассчитанный лимит газа: достаточно для прохождения проверки require и обновления executed[_data] = true, но недостаточно для успешного завершения последующего вызова target.call(...) внутри контракта Target.
5. Вызов target.call исчерпывает выделенный газ и молча завершается с ошибкой (с точки зрения контракта Relayer, поскольку его успешность не проверяется). Однако функция forward завершается «успешно».
6. В результате возникает несогласованное состояние: отображение executed в контракте Relayer показывает, что действие выполнено, но необходимая внешняя операция в контракте Target фактически не произошла. Конкретная транзакция легитимного пользователя (_data) теперь навсегда заблокирована из-за механизма защиты от повторного выполнения, который по сути подвергся цензуре со стороны злоумышленника, воспользовавшегося механикой газа и отсутствием проверки ошибок.
👍3🔥1