
Solidity Security
Этот пост задуман как относительно подробное и актуальное вводное руководство, описывающее прошлые ошибки, которые совершали разработчики Solidity, чтобы помочь будущим разработчикам не повторять историю.
Одна из особенностей смарт-контрактов Ethereum — возможность вызывать и использовать код других внешних контрактов. Контракты также обычно работают с эфиром и поэтому часто отправляют эфир на различные внешние адреса пользователей. Операция вызова внешнего контракта или отправки эфира на адрес требует от контракта выполнения внешнего вызова. Такие внешние вызовы могут быть перехвачены атакующими, которые заставляют контракт выполнять дополнительный код (например, через fallback-функцию) , включая вызовы к самому себе. Таким образом, исполнение кода "повторно входит" в контракт. Атаки такого рода были использованы в печально известном взломе DAO.
Для дальнейшего чтения об атаках повторного входа см. Атака Reentrancy на смарт-контракты и Consensus - Лучшие практики для смарт-контрактов Ethereum.
Эта атака может произойти, когда контракт отправляет эфир на неизвестный адрес. Атакующий может тщательно создать контракт на внешнем адресе, содержащий вредоносный код в fallback-функции. Таким образом, когда контракт отправляет эфир на этот адрес, будет вызван вредоносный код. Обычно вредоносный код выполняет функцию на уязвимом контракте, совершая операции, которых разработчик не ожидал. Название "re-entrancy" происходит из того факта, что внешний вредоносный контракт вызывает функцию уязвимого контракта и "повторно входит" в исполнение кода в произвольном месте уязвимого контракта.
Чтобы прояснить это, рассмотрим простой уязвимый контракт, который действует как хранилище Ethereum и позволяет вкладчикам выводить только 1 эфир в неделю.
EtherStore.sol:```solidity contract EtherStore {
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
require(msg.sender.call.value(_weiToWithdraw)());
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
}
}
Этот контракт имеет две публичные функции: `depositFunds()` и `withdrawFunds()`. Функция `depositFunds()` просто увеличивает баланс отправителя. Функция `withdrawFunds()` позволяет отправителю указать сумму wei для вывода. Она будет выполнена только в том случае, если запрашиваемая сумма вывода меньше 1 ether и за последнюю неделю не было вывода средств. Или нет?...
Уязвимость находится на строке \[17\], где мы отправляем пользователю запрошенное количество ether. Рассмотрим злонамеренного атакующего, создающего следующий контракт,
Attack.sol:```solidity
import "EtherStore.sol";
contract Attack {
EtherStore public etherStore;