В процессе разработки смарт-контрактов действительно существует множество типов уязвимостей, которые могут привести к потере средств, сбою функций контракта или злонамеренной эксплуатации. Ниже приведены распространенные типы уязвимостей при разработке смарт-контрактов:
Злоумышленник пользуется разблокированным состоянием контракта во время исполнения и неоднократно выводит средства или ресурсы, рекурсивно вызывая функции в контракте.
Мы будем использовать Solidity, язык смарт-контрактов Ethereum, чтобы создать простой контракт на пожертвование, а затем продемонстрировать потенциальную повторную атаку на контракт. Сначала мы создаем контракт, который принимает пожертвования, имеет баланс и позволяет пользователям выводить деньги. Код этого контракта может выглядеть так
pragma solidity ^0.8.0;
contract VulnerableDonation {
mapping (address => uint) public balances;
address payable public owner;
constructor() {
owner = payable(msg.sender);
}
function donate() public payable {
// получать пожертвования
balances[msg.sender] += msg.value;
}
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
// Баланс следует сначала уменьшить, а затем перенести, но порядок обратный.
msg.sender.transfer(_amount);
balances[msg.sender] -= _amount;
}
}
Обратите внимание, проблема здесь в том, что в функции вывода мы сначала пытаемся перевести деньги пользователю и только потом уменьшаем его баланс. Это небезопасно, поскольку операция перевода активирует функцию приема или возврата получателя, что дает злоумышленнику возможность вызвать функцию вывода для повторного вывода денег.
Теперь давайте создадим контракт злоумышленника, который сможет использовать эту уязвимость:
pragma solidity ^0.8.0;
contract Attacker {
VulnerableDonation donationContract;
constructor(address _donationAddress) {
donationContract = VulnerableDonation(_donationAddress);
}
fallback() external payable {
if (address(this).balance > 0) {
// рекурсивный вызов withdraw Функция, продолжать выводить деньги, пока есть баланс
donationContract.withdraw(address(this).balance);
}
}
function attack() public payable {
// первый звонок donate Функция Внесение средств в договор дарения
donationContract.donate{value: msg.value}();
// и сразу позвони withdraw Функция Начать реентерабельную атаку
donationContract.withdraw(address(this).balance);
}
}
В контракте злоумышленника автоматически срабатывает резервная функция при поступлении средств. Если в контракте еще есть баланс, он будет рекурсивно вызывать функцию вывода контракта пожертвования, пытаясь вывести как можно больше, пока средств не останется. осталось перенести пока. Чтобы обеспечить безопасность контракта, правильным подходом является уменьшение баланса пользователя перед переводом, чего можно добиться, просто изменив порядок функции вывода:
function withdraw(uint _amount) public {
require(balances[msg.sender] >= _amount, "Insufficient balance");
balances[msg.sender] -= _amount;
// Передачу следует выполнять после обновления переменных состояния.
msg.sender.transfer(_amount);
}
так,Даже если злоумышленник попытается позвонить еще раз, прежде чем перевести деньги
withdraw
функция, они также обнаружат, что их баланс был обновлен, и они не смогут снова снять деньги.
Когда результат математической операции выходит за пределы диапазона, который может представлять целочисленный тип, это приводит к неправильному переносу значения, что может быть использовано злоумышленником для получения дополнительных токенов или ресурсов.
Предположим, у нас есть смарт-контракт,Он получает депозиты пользователей и сохраняет их в переменной. Если сумма, которую пользователь пытается внести, плюс существующий баланс превышает максимальное значение целого числа (в Solidity,uint256
Максимальное значение типа2^256-1),Произойдёт переполнение.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract OverflowExample {
uint256 public balance;
function deposit(uint256 amount) public {
balance += amount;
}
function getBalance() public view returns (uint256) {
return balance;
}
}
Для тестового переполнение,Мы предполагаем
balance
Ужеuint256
Максимальное значение типа,Попробуйте еще раз сохранить любое положительное число.,вызовет переполнение,То есть результат будет меняться от максимального значения до 0.
// Предположим, что баланс уже равен максимальному значению uint256.
uint256 maxUint256 = type(uint256).max;
balance = maxUint256;
// Попытка сохранить любое положительное число приведет к переполнению.
deposit(1);
// В это время баланс станет 0
Недополнение обычно происходит при операциях вычитания. Если вы вычитаете большее число из меньшего числа, результат будет меньше минимального целочисленного значения (для беззнаковых целых чисел минимальное значение равно 0), что приводит к переполнению.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract UnderflowExample {
uint256 public balance;
function withdraw(uint256 amount) public {
balance -= amount;
}
function getBalance() public view returns (uint256) {
return balance;
}
}
В целых числах без знака отсутствие переполнения фактически приводит к переходу значения от 0 до максимального значения, но обычно это не ожидаемое поведение и по-прежнему считается ошибкой.
// Предположим, баланс равен 0
balance = 0;
// Попытка вынуть любое положительное число приведет к переполнению
withdraw(1);
// В это время баланс станет максимальным значением uint256.
Чтобы избежать целочисленного переполнения и опустошения,Solidityпредоставил Безопасностьматематическая библиотекаSafeMath
,Он содержит целочисленную арифметику, которая проверяет наличие переполнения и потери значения. Начиная с Solidity 0.8.0,Безопасностьматематические операторыcheckedAdd
, checkedSub
, checkedMu
l, и checkedDiv
быть представленным,Исключения могут быть автоматически обнаружены и выброшены.
using SafeMath for uint256;
function deposit(uint256 amount) public {
balance = balance.checkedAdd(amount);
}
function withdraw(uint256 amount) public {
balance = balance.checkedSub(amount);
}
Таким образом, если обнаружено переполнение или опустошение, Solidity автоматически выдаст исключение, предотвращая выполнение транзакции и тем самым защищая контракт от таких ошибок.
Если смарт-контракт имеет недостаточный контроль доступа к критическим функциям, злоумышленник может выполнить операции, которые не следует разрешать, например изменение состояния контракта или вывод средств.
Предположим, у нас есть смарт-контракт,Используется для управления депозитами и снятием средств пользователей. В этом примере,Контракт не ограничивает должным образом тех, кто может звонитьwithdraw
функция。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleBank {
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// Отсутствие контроля доступа, эту функцию может вызвать любой желающий
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
payable(msg.sender).transfer(amount);
balances[msg.sender] -= amount;
}
}
в этом контракте,withdraw
функция Можно позвонить напрямую с любого адреса,Пока адрес имеет достаточный баланс. но,Если в договоре есть логические ошибки или состояние хаотично,Это может привести к незаконному выводу средств.
Злоумышленник может позвонитьwithdraw
функция,Даже если им не хватает баланса,Также возможен успешный вывод средств из-за ошибок в определенных состояниях контракта. например,Если где-то в контракте ошибочно увеличивается баланс атакующего,Злоумышленники могут воспользоваться этим для вывода средств, которые им не принадлежат.
Для решения проблемы несанкционированного доступа,Нам нужно добавить модификатор доступа перед функцией.,Убедитесь, что есть только определенные ролиили Адрес можно назватьwithdraw
функция。Здесь мы используем простойonlyOwner
Модификатор для ограничения звонков владельцу контракта。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SecureBank {
address private owner;
mapping(address => uint256) public balances;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only the contract owner can call this function");
_;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
// Ограничьте вызовы владельцам с помощью модификатора onlyOwner.
function withdraw(uint256 amount) public onlyOwner {
require(balances[msg.sender] >= amount, "Insufficient balance");
payable(msg.sender).transfer(amount);
balances[msg.sender] -= amount;
}
}
Сейчас,Только создатель контракта(Прямо сейчасowner
)Можно позвонитьwithdraw
функция。Это предотвращает непосредственный вывод средств неавторизованными пользователями.,Улучшена производительность контракта.
Уведомление,Этого простого механизма контроля доступа может быть недостаточно для решения сложных сценариев.,Вам может потребоваться более сложная система ролей и разрешений.,Например, используйтеOpenZeppelin
изOwnable
иAccessControl
Библиотекачтобы обеспечить более детальноеизконтроль доступа。
существоватьсмарт-контрактразвиватьсередина,Неправильный порядок наследованияможет привести к неожиданномуиз Поведение,Особенно, когда речь идет о переопределении функции контроля разрешений. Когда контракт наследуется от нескольких родительских контрактов,Порядок выполнения конструкций Function и правила покрытия Function становятся особенно важными.
Предположим, у нас есть два контракта ParentA и ParentB.,И дочерний контракт Child унаследовал от этих двух контрактов. Контракт ParentA содержит конструктор Functionи функцию setOwner.,И ParentB также определяет функцию setOwner.,Но функции у него разные。насиз Цель состоит в том, чтобы сделатьChildКонтракт может Вызов функции setOwner объекта ParentA,но Неправильный порядок наследованияприведет к вызовуиздаParentBиз Версия
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ParentA {
address public owner;
constructor() {
owner = msg.sender;
}
function setOwner(address newOwner) public {
owner = newOwner;
}
}
contract ParentB {
function setOwner(address newOwner) public {
// Реализация здесь отличается от ParentA, но нас не интересуют конкретные детали.
}
}
// Неправильный порядок наследования
contract Child is ParentB, ParentA {
// ...
}
В приведенном выше коде,Дочерний контракт наследует ParentB и ParentA. Однако,В солидности,Если два родительских контракта определяют одно и то же имя. Функция,Порядок наследования определяет, какая функция будет переопределена первой. поэтому,В Детском контракте,setOwnerфункция на самом деле является версией ParentB.,Вместо той версии ParentA, которую мы ожидали.
Чтобы решить эту проблему, нам нужно настроить порядок наследования, чтобы гарантировать, что дочерний контракт может вызывать правильную функцию setOwner. В то же время, чтобы четко указать, какую родительскую функцию контракта мы хотим вызвать, мы можем использовать ключевое слово super, предоставляемое Solidity.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ParentA {
address public owner;
constructor() {
owner = msg.sender;
}
function setOwnerA(address newOwner) public {
owner = newOwner;
}
}
contract ParentB {
function setOwnerB(address newOwner) public {
// Реализация здесь отличается от ParentA.
}
}
// Правильный порядок наследования
contract Child is ParentA, ParentB {
// Вызов функции setOwner объекта ParentA
function setOwner(address newOwner) public {
ParentA.setOwnerA(newOwner); // Явно вызовите setOwnerA для ParentA.
}
}
В этой модифицированной версии,Дочерний контракт сначала наследуется от ParentA.,Это означает, что переменная состояния функций ParentA будет инициализирована раньше, чем переменная состояния ParentB. также,Мы переименовали функцию setOwner в ParentA иParentB, чтобы избежать конфликтов имен.,и В Детском контрактеопределяет новыйизsetOwnerфункция,Он явно вызывает setOwnerAфункция в ParentA.
Таким образом, мы гарантируем, что функция setOwner в контракте Child вызывает версию ParentA, избегая проблем с покрытием функций, вызванных неправильным порядком наследования.
Атака по короткому адресу
(Short Address Attack
)существовать Эфириумсерединада指利用Эфириумадресиз Шестнадцатеричный формат(40персонажи,т.е. 20 байт) и какой-то смарт-контракт, неправильная обработка параметров адреса лазейки,Средство атаки для выполнения вредоносных операций. Эта атака в основном происходит, когда смарт-контракт неправильно проверяет длину параметра адреса.,Хотя фактическая длина адреса Ethereum фиксирована,Однако злоумышленник может попытаться передать более короткую адресную строку.,Попытайтесь обманом заставить контракт выполнить неожиданную функцию.
В Solidity переменные типа адрес всегда занимают 20 байт, поэтому передача короткого адреса напрямую не вызовет проблем, поскольку Solidity автоматически заполнит его до 20 байт. Однако некоторые контракты могут получать данные из внешних вызовов, и если эти данные неправильно интерпретируются как адрес, а контракт не обрабатывает и не проверяет эти данные правильно, могут возникнуть атаки на короткие адреса.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract VulnerableContract {
address public owner;
constructor() {
owner = msg.sender;
}
// Неправильная попытка синтаксического анализа произвольных данных в адрес
function setAddress(bytes data) public {
// Примечание. Здесь используется необычный метод для анализа данных по адресам.
// Фактически, если длина данных меньше 20 байт, это приведет к неверному адресу.
assembly {
owner := mload(add(data, 0x14)) // Загрузите 20 байт данных и назначьте их владельцу.
}
}
function getOwner() public view returns (address) {
return owner;
}
}
В этом примере,VulnerableContract имеет публичную функцию setAddress.,Он принимает данные параметра типа байты.,и попробуйте проанализировать его по адресу на ассемблере низкого уровня,Затем установите его как владельца контракта. Если длина данных, переданных злоумышленником, меньше 20 байт,Solidity автоматически заполнит оставшиеся байты 0.,Это может привести к тому, что в качестве владельца будет установлен неверный адрес.
Предположим, злоумышленник создает данные длиной менее 20 байт (например.,Содержит только 10 байт полезной нагрузки),ивызовsetAddress
функция。ХотяSolidityавтоматически уменьшит недостаточностьиз Частично заполнен0,Но если контракт не проходит должным образом и не обрабатывает эту ситуацию,Тогда владельцу может быть присвоен неожиданный адрес.,Это может быть неверный адрес или адрес, контролируемый злоумышленником.
Для защиты от атак по коротким адресам разработка смарт-контрактов должна:
утверждение(assert
)существоватьсмарт-контрактсередина Используется для обеспечения внутренней логикиизпоследовательностьиправильность,Но при неправильном использовании,Это действительно может привести к несчастным случаямиздоговорпрекращениеили Средства заблокированы。Это потому, чтоassert
В основном используется для тестирования внутренних программ.изошибка,Например, ошибки алгоритма и логические ошибки.,Предполагается, что эти ошибки не возникают при нормальной работе. Как только утверждение не удалось,Транзакция будет немедленно отменена,Плата за газ не возвращается.,Это может иметь катастрофические последствия для пользователей контракта.,Особенно, если это приводит к недоступности критического функционала контракта.
Вот пример неправильного использования Assert, которое может привести к блокировке средств:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract WithdrawalContract {
address payable public owner;
uint256 public balance;
constructor() {
owner = payable(msg.sender);
balance = 0;
}
receive() external payable {
balance += msg.value;
}
function withdraw(uint256 amount) public {
assert(msg.sender == owner); // Гарантирует, что только владелец контракта может вывести средства
require(balance >= amount, "Insufficient funds"); // Убедитесь, что у вас достаточно баланса
balance -= amount;
owner.transfer(amount); // Перевести средства владельцу
}
}
В этом контракте Assert(msg.sender == Owner) используется, чтобы гарантировать, что только владелец контракта может вызвать функцию вывода. Однако если после развертывания контракта в качестве адреса владельца случайно будет установлен недопустимый адрес (например, адрес без закрытого ключа), утверждение всегда будет завершаться неудачно, и средства будут навсегда заблокированы в контракте, поскольку никто не сможет вызовите функцию вывода средств для вывода средств.
Чтобы избежать риска блокировки средств, вы можете рассмотреть следующие стратегии улучшения:
assert
:Для пользовательского вводаилипроверка предварительных условий,использоватьrequire
более уместно,Потому что там четко написано, что это проверка внешних условий,Вместо внутренней логической ошибки.// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ImprovedWithdrawalContract {
address payable public owner;
uint256 public balance;
constructor() {
owner = payable(msg.sender);
}
receive() external payable {
balance += msg.value;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only the contract owner can call this function");
_;
}
function withdraw(uint256 amount) public onlyOwner {
require(balance >= amount, "Insufficient funds");
balance -= amount;
owner.transfer(amount);
}
// Добавить функцию, позволяющую менять владельца
function changeOwner(address payable newOwner) public onlyOwner {
owner = newOwner;
}
}
В этом улучшенном контракте мы использовали требование для проверки условий и добавили функцию ChangeOwner, которая позволяет текущему владельцу при необходимости изменить адрес владельца, избегая тем самым риска постоянной блокировки средств.
режим проксисуществоватьсмарт-контрактразвиватьсерединаочень распространенный,尤Чтодасуществоватьобновлениеи Модульная конструкциясередина。агентский договор(Proxy Contract) часто используется для отделения логической реализации от внешнего интерфейса контракта, позволяя обновлять или заменять базовую реализацию без изменения интерфейса. Однако если процесс инициализации прокси-контракта не выполняется должным образом, он может стать точкой входа для атак.
Предположим, у нас есть следующий шаблон агентского договора.,Чтосерединаimplementation
Переменные указывают на реальную логику выполнения.издоговорадрес:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Proxy {
address private implementation;
constructor (address _implementation) {
implementation = _implementation;
}
fallback() external payable {
address impl = implementation;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0)
assembly {
let free := mload(0x40)
mstore(free, ptr)
mstore(0x40, add(free, 0x20))
}
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
}
Этот прокси-контракт принимает адрес контракта реализации через конструктор и сохраняет его в переменной реализации. После этого любые транзакции, отправленные в прокси-контракт, будут перенаправлены в контракт реализации.
Проблема в том, что если у конструктора нет соответствующих ограничений на то, кто может установить адрес реализации, злоумышленник может воспользоваться этим и напрямую вызвать конструктор прокси-контракта, отправив транзакцию, таким образом изменив адрес реализации, чтобы он указывал на его собственный вредоносный адрес. договор. Таким образом, все последующие вызовы будут перенаправлены на вредоносный контракт, что приведет к нарушению функциональности контракта или краже средств.
Чтобы предотвратить этот тип атаки, нам необходимо убедиться, что процесс инициализации прокси-контракта является безопасным. Вот одно из возможных решений:
решение Пример:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
abstract contract Initializable {
bool initialized = false;
modifier initializer() {
require(!initialized, "Already initialized");
initialized = true;
_;
}
}
contract SecureProxy is Initializable {
address private implementation;
address private admin;
constructor(address _implementation, address _admin) initializer {
implementation = _implementation;
admin = _admin;
}
function setImplementation(address _newImplementation) public {
require(msg.sender == admin, "Only admin can set the implementation");
implementation = _newImplementation;
}
fallback() external payable {
// ... (same as before)
}
}
В этой улучшенной версии мы представили абстрактный контракт Initializable для управления состоянием инициализации и применили модификатор инициализатора к конструктору. Кроме того, мы добавили метод setImplementation, который позволяет владельцу контракта (администратору) обновлять адрес реализации, что еще больше повышает безопасность.
Уязвимости, зависящие от времени, являются распространенной проблемой безопасности в смарт-контрактах, особенно в средах блокчейнов, таких как Ethereum. Это связано с тем, что майнеры могут в определенной степени манипулировать временными метками блоков блокчейна, что делает смарт-контракты, основанные на временных метках, уязвимыми для атак. Злоумышленник может получить несправедливое преимущество или нанести убытки, контролируя временные метки блоков для запуска определенных условий в контракте.
Предположим, у нас есть кредитный договор с повременной оплатой, по которому заемщик должен погасить кредит в течение определенного периода времени, в противном случае ему грозят высокие штрафные ставки или он теряет залог. Контракт может выглядеть следующим образом:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract LoanContract {
address public borrower;
uint256 public loanAmount;
uint256 public deadline;
constructor(address _borrower, uint256 _loanAmount, uint256 _deadline) {
borrower = _borrower;
loanAmount = _loanAmount;
deadline = block.timestamp + _deadline; // Установить срок погашения
}
function repayLoan() public {
require(msg.sender == borrower, "Only borrower can repay");
require(block.timestamp <= deadline, "Deadline passed");
// Логика погашения кредита...
}
function claimCollateral() public {
require(block.timestamp > deadline, "Deadline not yet passed");
// Логика невозврата кредита и конфискации залога...
}
}
В этом контракте срок рассчитывается на основе временной метки текущего блока, и заемщик должен погасить кредит до истечения срока. Однако, если злоумышленник контролирует процесс майнинга, он может задержать отправку новых блоков, искусственно продлевая временную метку блока, чтобы создать впечатление, что крайний срок еще не наступил, тем самым предотвращая конфискацию залога или, наоборот, досрочную отправку новых блоков. , в результате чего срок наступает раньше, что вынуждает заемщика платить штрафные проценты.
Чтобы устранить уязвимость, связанную с зависимостью от времени, можно использовать следующие стратегии:
Например, мы можем изменить приведенный выше кредитный договор, чтобы использовать высоту блока в качестве временной базы:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract LoanContract {
address public borrower;
uint256 public loanAmount;
uint256 public deadlineBlock;
constructor(address _borrower, uint256 _loanAmount, uint256 _deadlineBlocks) {
borrower = _borrower;
loanAmount = _loanAmount;
deadlineBlock = block.number + _deadlineBlocks; // Настройка блоков сроков погашения
}
function repayLoan() public {
require(msg.sender == borrower, "Only borrower can repay");
require(block.number <= deadlineBlock, "Deadline block passed");
// Логика погашения кредита...
}
function claimCollateral() public {
require(block.number > deadlineBlock, "Deadline block not yet passed");
// Логика невозврата кредита и конфискации залога...
}
}
Путем изменения зависимости времени на зависимость высоты блока,Мы уменьшили возможность майнеров манипулировать временными метками.,Это повышает справедливость договора. Однако,Каждое решение имеет свои компромиссы,Например, использование высоты блока может привести к неопределенности, связанной со временем генерации блока.,Поэтому в практическом применении Требует тщательной оценкии Выберите наиболее подходящийизплан。
Лимит газа и DoS (Отказ of Атаки обслуживания (отказ в обслуживании) являются распространенной угрозой в средах блокчейнов, особенно для таких платформ, как Ethereum, где Gas — это единица измерения стоимости выполнения транзакции. Первоначальное намерение Газового механизма – предотвратить бесконечное цикли Злоупотребление ресурсами, но оно также предоставляет злоумышленникам пространство для использования.
В Ethereum каждая транзакция несет определенное количество газа. Это делается для того, чтобы любые выполняемые операции не потребляли слишком много вычислительных ресурсов, что позволяет избежать перегрузки сети или истощения ресурсов. Когда транзакция начинает выполняться, она вычитает комиссию из общей суммы газа, предоставленного трейдером, до тех пор, пока исполнение контракта не будет завершено или газ не будет исчерпан. Если во время выполнения Gas будет исчерпан, транзакция будет отменена, и израсходованный Gas не будет возвращен пользователю.
Злоумышленник может намеренно потреблять большое количество газа, создавая очень сложные транзакции или смарт-контракты, чтобы обычные транзакции не могли быть включены в блок. Например, злоумышленник может создать контракт, который выполняет большое количество вычислений или операций хранения при получении сообщения, потребляя количество газа, близкое к максимальному пределу газа. Когда в сеть одновременно отправляется множество таких транзакций, они занимают большую часть или даже всю емкость газа, в результате чего обычные транзакции других пользователей не подтверждаются, тем самым достигая эффекта отказа в обслуживании.
Другой способ DoS-атаки — превратить смарт-контракт в бесконечный. цикл,Это приведет к немедленному исчерпанию газа.,Транзакция не удалась и была отменена. Эта атака обычно происходит при наличии ошибки в логике контракта.,Например, условия выхода из цикла обрабатываются неправильно.,илисуществоватьрекурсивный вызовсередина缺少прекращение条件。Когда договор вступит в силубесконечный циклчас,Он попытается израсходовать весь доступный газ.,В конце концов транзакция не удалась,и может сделать контракт недоступным.
Чтобы защититься от такого типа DoS-атак, разработчикам необходимо принять некоторые меры предосторожности при написании смарт-контрактов:
Благодаря этим мерам,Позволяет существенно снизить риск DoS-атак на смарт-контракт,Обеспечьте стабильность сети и активов пользователя. Однако,Из-за сложности среды блокчейна,Постоянное понимание безопасности и новейшие практики безопасности имеют важное значение.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract InfiniteLoopVulnerable {
function loopUntilZero(uint256 startValue) public payable {
uint256 currentValue = startValue;
while (currentValue > 0) {
currentValue--;
}
// Нормальная работа...
}
}
в этом контракте,Функция LoopUntilZero войдет в бесконечный цикл.,Если startValue установлено достаточно большим,Тогда этот контур будет потреблять весь доступный газ.,Вызывает сбой транзакции и ее откат.
Злоумышленник может вызвать функцию LoopUntilZero, передав очень большое значение, например 2^256-1, что сделает завершение цикла практически невозможным и израсходует весь газ.
InfiniteLoopVulnerable contract = new InfiniteLoopVulnerable();
contract.loopUntilZero(2**256-1);
Чтобы предотвратить этот бесконечный DoS-атаки цикла, нам необходимо добавить некоторые ограничения и оптимизации в дизайн контракта:
Вот отремонтированный контракт. Пример:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SafeInfiniteLoop {
function safeLoopUntilZero(uint256 startValue) public payable {
require(startValue <= 10000, "Value too large"); // Установите максимальное количество петель
uint256 currentValue = startValue;
while (currentValue > 0) {
currentValue--;
}
// Нормальная работа...
}
}
Неправильное управление разрешениями — одна из распространенных проблем безопасности в смарт-контрактах, особенно когда администратору или конкретным учетным записям предоставляются чрезмерные разрешения. Если ключевые функции контракта, такие как передача активов, изменение статуса контракта и обновление логики контракта, могут выполняться по желанию неавторизованными лицами, это создаст серьезный риск. Ниже я приведу пример, показывающий Неправильное управление разрешениями о возможных последствиях и о том, как смягчить этот риск посредством разумного проектирования.
Предположим, у нас есть смарт-контракт,Используется для управления выпуском и передачей цифрового актива. в этом контракте,Учетным записям администраторов предоставляются неограниченные полномочия.,Новые активы могут быть отчеканены и переведены на любой счет без ограничений.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MismanagedPermissions {
mapping(address => uint256) public balances;
address public admin;
constructor() {
admin = msg.sender;
}
function mint(address to, uint256 amount) public {
require(msg.sender == admin, "Only admin can mint");
balances[to] += amount;
}
function transfer(address from, address to, uint256 amount) public {
require(balances[from] >= amount, "Insufficient balance");
balances[from] -= amount;
balances[to] += amount;
}
}
В этом контракте функция mint позволяет учетным записям администраторов создавать новые активы без ограничений. Хотя это может показаться разумным разрешением, но если безопасность учетной записи администратора скомпрометирована или разработчик контракта по ошибке устанавливает ненадежный адрес в качестве администратора, это открывает дверь для злоумышленников.
Злоумышленник может получить закрытый ключ учетной записи администратора различными способами, либо разработчик контракта может случайно установить вредоносный адрес в качестве администратора. Как только злоумышленник получит контроль над учетной записью администратора, он сможет по своему желанию вызвать функцию mint, создать неограниченное количество активов и перенести их на свою учетную запись, тем самым получая незаконную прибыль.
MismanagedPermissions contract = new MismanagedPermissions();
contract.mint(msg.sender, 1000000); // Злоумышленники вычеканили большое количество активов
Чтобы предотвратить проблемы безопасности, вызванные неправильным управлением разрешениями, мы можем принять следующие меры:
Ниже приведен улучшенный пример контракта, который добавляет ограничения разрешений и механизм мультиподписи:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SafePermissions {
mapping(address => uint256) public balances;
mapping(address => bool) public admins;
uint256 public dailyMintLimit;
uint256 public dailyMinted;
constructor(uint256 _dailyMintLimit) {
dailyMintLimit = _dailyMintLimit;
admins[msg.sender] = true; // Начальный администратор
}
modifier onlyAdmin() {
require(admins[msg.sender], "Only admin can perform this action");
_;
}
function mint(address to, uint256 amount) public onlyAdmin {
require(dailyMinted + amount <= dailyMintLimit, "Daily mint limit exceeded");
balances[to] += amount;
dailyMinted += amount;
}
function addAdmin(address newAdmin) public onlyAdmin {
admins[newAdmin] = true;
}
function removeAdmin(address adminToRemove) public onlyAdmin {
delete admins[adminToRemove];
}
}
В этом улучшенном контракте,Мы ввели концепцию нескольких администраторов,и установите дневной лимит на отчеканенные активы,Чтобы предотвратить неограниченное создание активов. в то же время,Мы также предоставляем возможность добавлять и удалять администраторов.,Для этого требуются существующие права администратора.
благодаря этим улучшениям,Мы можем значительно повысить безопасность контракта.,Уменьшите риск Неправильного управления разрешениями. в практическом применении,Его также необходимо сочетать с конкретными бизнес-сценариями и потребностями в безопасности.,Дальнейшая доработка механизма управления разрешениями и безопасности.
существоватьсмарт-контрактразвиватьсередина,Вызов ненадежных внешних контрактов является распространенной точкой риска. Это потому, что,Когда вы вызываете функцию другого контракта,Вы фактически выполняете код этого контракта.,И это может привести к поведению, которого вы не ожидали,В том числе злонамеренное поведение. Ниже я проиллюстрирую этот риск с помощью примера.,и предложить соответствующие стратегии смягчения последствий.
Предположим, у нас есть смарт-контракт, который позволяет пользователям выполнять определенную задачу, например погашение токенов, путем вызова внешнего контракта. Здесь мы предполагаем, что внешний контракт предоставляет функцию TransferFrom для перевода токенов с одного аккаунта на другой.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ExternalCallVulnerable {
address public externalTokenContract;
constructor(address _externalTokenContract) {
externalTokenContract = _externalTokenContract;
}
function exchangeTokens(uint256 amount) public {
IERC20(externalTokenContract).transferFrom(msg.sender, address(this), amount);
}
}
В этом контракте функция ExchangeTokens вызывает функцию TransferFrom внешнего контракта. Однако здесь есть потенциальная проблема: внешний контракт может содержать вредоносный код, либо его логика может не соответствовать ожиданиям, что приведет к потере средств или другим нежелательным последствиям.
Злоумышленник может развернуть вредоносный контракт токена ERC20 и передать этот адрес контракта нашему контракту. Вредоносный контракт может содержать дополнительную логику в функции TransferFrom, например, вызов других функций нашего контракта при передаче токенов или выполнение каких-либо несанкционированных операций.
// Вредоносный контракт Пример
contract MaliciousToken is IERC20 {
function transferFrom(address, address, uint256) public override returns (bool) {
// Обычная логика передачи токена...
// Выполнять дополнительные вредоносные действия, такие как вызов другой функции в контракте.
ExternalCallVulnerable(0x...).someUnsafeFunction();
return true;
}
}
Когда пользователь пытается обменять токены во вредоносном контракте через наш контракт, будет вызвана функция TransferFrom вредоносного контракта для выполнения вредоносных операций.
Чтобы снизить риски, вызванные внешними вызовами, мы можем принять следующие меры:
Ниже приведен улучшенный контракт Пример, который реализует Механизм. белого списка:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function transferFrom(address, address, uint256) external returns (bool);
}
contract SafeExternalCall {
mapping(address => bool) public approvedContracts;
address public externalTokenContract;
constructor(address _externalTokenContract) {
approveContract(_externalTokenContract);
externalTokenContract = _externalTokenContract;
}
function exchangeTokens(uint256 amount) public {
require(approvedContracts[externalTokenContract], "Contract not approved");
IERC20(externalTokenContract).transferFrom(msg.sender, address(this), amount);
}
function approveContract(address contractAddress) public {
approvedContracts[contractAddress] = true;
}
}
существоватьсмарт-контрактразвиватьсередина,Вызов ненадежных внешних контрактов является распространенной точкой риска. Это потому, что,Когда вы вызываете функцию другого контракта,Вы фактически выполняете код этого контракта.,И это может привести к поведению, которого вы не ожидали,В том числе злонамеренное поведение. Ниже я проиллюстрирую этот риск с помощью примера.,и предложить соответствующие стратегии смягчения последствий.
Предположим, у нас есть смарт-контракт,Он позволяет пользователям выполнять определенные задачи, вызывая внешний контракт.,Например, обмен токенов. здесь,Мы предполагаемвнешнийдоговорпредоставилодинtransferFrom
функция,Используется для перевода токенов с одного аккаунта на другой.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ExternalCallVulnerable {
address public externalTokenContract;
constructor(address _externalTokenContract) {
externalTokenContract = _externalTokenContract;
}
function exchangeTokens(uint256 amount) public {
IERC20(externalTokenContract).transferFrom(msg.sender, address(this), amount);
}
}
в этом контракте,exchangeTokens
функциявызов了внешнийдоговоризtransferFrom
функция。Однако,Здесь есть потенциальная проблема: внешние контракты могут содержать вредоносный код.,или Логика может не соответствовать ожиданиям,Что приводит к потере средств и другим неблагоприятным последствиям.
Злоумышленник может развернуть вредоносный контракт токена ERC20.,ипомести этодоговорадрес传递给насиздоговор。злонамеренныйдоговорвозможныйсуществоватьtransferFrom
функциясередина Содержит дополнительныеизлогика,Например, при передаче токенов,Вызов другой функции в нашем контракте,Злоумышленник выполняет некоторые несанкционированные операции.
// Вредоносный контракт Пример
contract MaliciousToken is IERC20 {
function transferFrom(address, address, uint256) public override returns (bool) {
// Обычная логика передачи токена...
// Выполнять дополнительные вредоносные действия, такие как вызов другой функции в контракте.
ExternalCallVulnerable(0x...).someUnsafeFunction();
return true;
}
}
Когда пользователь пытается обменять токены из вредоносного контракта через наш контракт,злонамеренныйдоговоризtransferFrom
функциябудет вызван,Выполнять вредоносные действия.
Чтобы снизить риски, вызванные внешними вызовами, мы можем принять следующие меры:
Ниже приведен улучшенный контракт Пример, который реализует Механизм. белого списка:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function transferFrom(address, address, uint256) external returns (bool);
}
contract SafeExternalCall {
mapping(address => bool) public approvedContracts;
address public externalTokenContract;
constructor(address _externalTokenContract) {
approveContract(_externalTokenContract);
externalTokenContract = _externalTokenContract;
}
function exchangeTokens(uint256 amount) public {
require(approvedContracts[externalTokenContract], "Contract not approved");
IERC20(externalTokenContract).transferFrom(msg.sender, address(this), amount);
}
function approveContract(address contractAddress) public {
approvedContracts[contractAddress] = true;
}
}
В этом улучшенном контракте,Мы добавилиapprovedContracts
картографирование,Используется для хранения утвержденных адресов внешних контрактов. Только если адрес внешнего контракта внесен в белый список,Его можно вызвать через наш договор.
благодаря этим улучшениям,Мы можем значительно снизить риск, вызванный вызовом ненадежных внешних контрактов. Однако,в практическом применении,Также необходимо продолжать уделять внимание новым угрозам безопасности и передовому опыту.,Поддерживать безопасность характера контракта.
Как правило, генерацию случайных чисел в блокчейне обычно сложно осуществить, и она зависит от предсказуемых факторов, таких как хеши блоков. Это может привести к тому, что злоумышленники смогут предсказать результаты. Ниже приведены некоторые распространенные сценарии.
contract GuessTheNumber {
function guess(bool isHigher) public {
uint256 randomNumber = block.timestamp % 100; // Временная метка используется здесь как источник случайных чисел.
if ((randomNumber > 50) == isHigher) {
// Если игрок угадал правильно, награда распределяется
}
}
}
Злоумышленник может предсказать временные метки будущих транзакций, отслеживая временные метки транзакций в блокчейне, и всегда делать правильные предположения.
contract Auction {
function endAuction() public {
uint256 random = ExternalRandomService.getLastBlockHash() % bidders.length;
// Предположим, что участники торгов представляют собой массив, и для выбора победителя торгов используется случайный метод.
}
}
Злоумышленник может наблюдать за транзакцией контракта, которая вот-вот завершит аукцион, а затем отправить собственную транзакцию до того, как контракт вызовет getLastBlockHash(), влияя на хэш блока и, следовательно, на окончательное случайное число.
contract Game {
function play() public {
uint256 random = OracleService.getRandomNumber();
// Используйте случайные числа, предоставленные оракулами
}
}
Если сервис оракула контролируется злоумышленником, он может предоставить ложные случайные числа и повлиять на исход игры.
Для решения вышеперечисленных проблем можно использовать несколько стратегий:
Неправильная структура храненияили Вычислительно интенсивные операции могут привести к высокимGasрасходыи Узкое место в производительности。
Предположим, вы создаете систему голосования, в которой каждое предложение имеет независимый счетчик голосов. Чтобы добиться этого, вы можете сначала рассмотреть возможность использования карты, где ключами являются идентификаторы предложений, а значениями — массив, хранящий адреса всех избирателей, проголосовавших за это предложение.
// Неправильная структура хранения
contract VotingSystem {
mapping(uint => address[]) public voters;
function vote(uint proposalId, address voter) public {
voters[proposalId].push(voter);
}
function getVotesCount(uint proposalId) public view returns (uint) {
return voters[proposalId].length;
}
}
Предложения по оптимизации Чтобы снизить затраты на газ и повысить производительность, мы можем перепроектировать структуру данных, чтобы использовать сопоставление для отслеживания того, проголосовал ли каждый избиратель за определенное предложение, вместо сохранения массива избирателей.
// Оптимизированная структура хранения
contract OptimizedVotingSystem {
mapping(uint => mapping(address => bool)) public hasVoted;
function vote(uint proposalId, address voter) public {
require(!hasVoted[proposalId][voter], "Already voted");
hasVoted[proposalId][voter] = true;
}
function getVotesCount(uint proposalId) public view returns (uint) {
uint count;
for (address voter = address(1); voter != address(0); voter = address(uint(voter) + 1)) {
if (hasVoted[proposalId][voter]) {
count++;
}
}
return count;
}
}
Хотя использование карты может значительно повысить эффективность, перебор всех адресов для подсчета голосов в функции getVotesCount по-прежнему неэффективен. На практике вы можете ввести дополнительные сопоставления или переменные для отслеживания общего количества голосов по каждому предложению, чтобы избежать полного обхода адресного пространства.
// Дальнейшая оптимизация
contract FurtherOptimizedVotingSystem {
mapping(uint => mapping(address => bool)) public hasVoted;
mapping(uint => uint) public votesCount;
function vote(uint proposalId, address voter) public {
require(!hasVoted[proposalId][voter], "Already voted");
hasVoted[proposalId][voter] = true;
votesCount[proposalId]++;
}
function getVotesCount(uint proposalId) public view returns (uint) {
return votesCount[proposalId];
}
}
так,Просто обновляйте счетчик голосов каждый раз, когда вы голосуете.,Затраты на газ значительно сокращаются, а скорость запросов увеличивается. в смарт-контрактразвивать,Разумный дизайн и оптимизированная структура хранения имеют решающее значение для снижения затрат и повышения производительности.