Solidity: погружение в разработку Ethereum

В данном руководстве содержится информация для быстрого погружения в разработку смарт-контрактов на языке Solidity для Ethereum блокчейна.

Последнее обновление 24.05.2022

Содержание
  1. Основы Solidity
  2. Типы памяти
  3. Местоположение данных в памяти
  4. Идентификатор лицензии
  5. Версия смарт-контракта
  6. Импорт файлов с исходным кодом
  7. Объявление и название контракта
  8. Комментарии в коде
  9. Структуры контракта
  10. Функции
  11. Объявление функций
  12. Видимость функций
  13. Модификаторы функций
  14. Типы функций
  15. Встроенные методы внешних функций
  16. Специальные функции fallback и receive
  17. Функции virtual и override
  18. События
  19. Ошибки
  20. Cтруктуры
  21. Перечисления
  22. Переменные
  23. Типы переменных
  24. Типы значений
  25. Boolean
  26. Integers
  27. Fixed Point Numbers
  28. Address
  29. Тип контракта
  30. Массивы
  31. Строки
  32. Словари
  33. Единицы измерения и глобальные переменные
  34. Единицы измерения валюты Эфира
  35. Единицы измерения времени
  36. Специальные переменные и функции
  37. Свойства Блока и Транзакций
  38. Функции кодирования и декодирования ABI
  39. Обработка ошибок
  40. Математические и криптографические функции
  41. Методы типа Address
  42. Артефакты контракта
  43. Информация о типе
  44. FAQ – ответы на популярные вопросы
  45. Как получить рандомное число от 0 до 899?
  46. Как вывести лог в консоль Remix для отладки?

Основы Solidity

Типы памяти

Важная вещь, которую вам нужно понять перед началом разработки на Solidity – это где и как хранятся все данные? От этого будет зависеть какие методы работы с памятью вы будите использовать и какое количество газа за это заплатит конечный пользователь вашего смарт-контракта!

  • Storage – это постоянная память смарт-контракта, которых хранит в ней все глобальные переменные. Это самая дорогая память! Потому что данные, хранящиеся в ней, записываются в блокчейн навсегда. Это своего рода хранилище (база данных) вашего смарт-контракта.
  • Memory – это временная память, которая выделятся только на момент вызова функций смарт-контракта. Это своего рода оперативная память необходимая для вычислений. Например, в ней хранятся значения переменных, которые вы передаете в локальные функции или значение переменных которые возвращают эти функции. Стоит она намного дешевле чем storage.
  • Stack – эта память работает по принципам LIFO (“последним пришел, первым вышел”). Главным образом ее использует EVM для всех вычислений. Стоит она также как memory. Использование данного вида памяти лучше оставить на усмотрение компилятора.

Местоположение данных в памяти

Для массивов и структур Solidity автоматически определяет, в зависимости от контекста, где должны располагаться эти данные – в storage или в memory. Например, переменные которые передаются в функцию, объявляются в функции и возвращаются функцией – хранятся в memory, а глобальные переменных хранятся в storage.

Однако, при помощи ключевых слов storage и memory вы можете переопределить это поведение и это повлияет на то, как будет работать оператор присваивания.

  • Когда существующую переменную с типом memory мы присваиваем в переменную с типом storage, то мы копируем данные из memory в storage. При этом новое хранилище storage не создается (т.к. оно уже было выделено при создании контракта)!
  • Когда существующую переменную с типом storage мы присваиваем в переменную с типом memory, то мы копируем данные из storage в memory. При этом выделяется новая память!
  • Когда переменная storage создается внутри функции локально и ей присваивается объект путем поиска из списка глобальных переменных, то она просто ссылается на данные уже размещенные в хранилище storage. При этом новое хранилище не создается!

Мы можем переопределять местоположение данных в памяти только для параметров функции и ее локальных переменных. Всякий раз, когда мы переменную с типом storage присваиваем в переменную с типом memory, у нас создается копия и ее дальнейшая модификация не влияет на состояние контракта.

contract ExampleStore {

  /**
   * @dev Определяем структуру объекта
   */
  struct Item {
    uint price;
    uint units; 
  }
  
  /**
   * @dev При создании контракта выделяется storage память
   * для хранения массива наших структур Item
   */
  Item[] public items;

  /**
   * @dev Создаем экземпляр объекта в memory памяти
   * и добавляем его в массиив хранящиися в storage
   */
  function newItem(uint _price, uint _units) public {
    Item memory item = Item(_price, _units);
    items.push(item);
  }

  /**
   * @dev Возвращаем ссылку на конкретный объект 
   * из существующего массива живущего в storage.
   */
  function getUsingStorage(uint _itemIdx) public
  returns (uint) {
    Item storage item = items[_itemIdx];
    return item.units;
  }

  /**
   * @dev Возвращаем копию конкретного объекта
   * из существующего массива живущего в storage.
   */
  function getUsingMemory(uint _itemIdx) public
  returns (uint) {
    Item memory item = items[_itemIdx];
    return item.units;
  }

  /**
   * @dev Берем ссылку на конкретный объект из существующего массива в storage
   * и изменяем его значение (значение в storage меняется)
   */
  function addItemUsingStorage(uint _itemIdx, uint _units) public {
    Item storage item = items[_itemIdx];
    item.units += _units;
  }

  /**
   * @dev Берем копию конкретного объекта из существующего массива в storage
   * и изменяем его значение (значение в storage НЕ меняется)
   */
  function addItemUsingMemory(uint _itemIdx, uint _units) public {
    Item memory item = items[_itemIdx];
    item.units += _units;
  }

}

Идентификатор лицензии

Когда вы публикуете исходный код своих смарт-контрактов, то доверие к ним становится лучше со стороны пользователей. В самом начале файла смарт-контракта принято прописывать идентификатор лицензии по которому распространяется код вашего приложения.

// SPDX-License-Identifier: GPL-3.0

Версия смарт-контракта

Язык Solidity постоянно развивается, в него добавляются разные возможности и управляющие конструкции. Чтобы компилятор правильно распознал все конструкции вашего смарт-контракта, вы должны указать версию Solidity на котором пишется ваш программный код.

pragma solidity >=0.7.0 <0.9.0;

Импорт файлов с исходным кодом

Когда ваш смарт-контракт состоит из нескольких файлов и в одном из этих файлов вы используете функции другого, то в нем важно указать, какой именно файл с исходным кодом вы используете. Это делается при помощи управляющей конструкции import. Обычно она указывается сразу между указанием версии смарт-контракта и блоком с описанием самого контракта. Причем вы можете указать как URL ссылку на смарт-контракт так и путь к файлу на вашем компьютере.

// Importing OpenZeppelin's ERC-721 Implementation
import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/token/ERC721/ERC721.sol";

// Importing OpenZeppelin's SafeMath Implementation
import "https://github.com/OpenZeppelin/openzeppelin-solidity/contracts/utils/math/SafeMath.sol";

Объявление и название контракта

После указания импортированных данных, самое время объявить конструкцию вашего смарт-контракта и присвоить ему имя.

contract YourContractName {
    // ...
}

Комментарии в коде

Есть 2 способа оставлять комментарии в коде, вот они:

// This is a single-line comment.

/*
This is a
multi-line comment.
*/

Структуры контракта

Функции

Объявление функций

Весь код смарт-контракта должен быть разделен на логические блоки, каждый из которых выполняет конкретную задачу. Такие логические блоки принято называть функциями и обозначать в коде ключевым словом function

Функции могут располагаться:

  • внутри контракта (блока contract),
  • внутри библиотеки (блока library),
  • на верхнем уровне, вне контракта и блиблиотек.

Видимость функций

  • external – внешние функции являются частью контракта, что означает, что их можно вызывать из других контрактов и через транзакции. Чтение идет напрямую из calldata. Внешняя функция f не может быть вызвана изнутри (т.е. f () не работает, но this.f () работает).
  • internal – доступ к этим функциям и переменным состояния можно получить только из текущего контракта или его наследников без использования this. Это уровень видимости по умолчанию для переменных состояния.
  • public – публичные функции являются частью интерфейса контракта и могут вызываться либо внутри контракта как internal, либо вызываться внешне как external. При этом происходит копирование массива в memory, тем самым требуя больше памяти чем использование internal или external.
  • private – частные функции и переменные состояния видны только для контрактах, в котором они определены. Но не видны в наследуемых от них контрактах.

Модификаторы функций

Кроме того, в Solidity есть такое понятие, как модификатор функции. Это своего рода заранее выделенное в отдельный блок кода именованное предусловие. Они обозначаются ключевым словом modifier . Если к вашей функции применить один из таких предопределенных модификаторов, то ваша фукнция будет выполняться только в том случае, если в ее модификаторе заданные в нем условия успешно прошли проверки.

Ниже пример функции и модификатора:

pragma solidity >=0.7.0 <0.9.0;

contract MyContractName {
    address public seller;

    modifier onlySeller() { // Modifier
        require(
            msg.sender == seller,
            "Only seller can call this."
        );
        _;
    }

    function abort() public onlySeller { // Modifier usage
        // ...
    }
}

Типы функций

Функции бывают внутренние и внешние. Внутренние функции могут быть вызваны только из текущего контракта, в котором объявлена эта функция. Внешние функции состоят из адреса и сигнатуры, которые могут быть переданы через возвращаемое значение вызова внешней функции.

function (<parameter types>) {internal|external} [pure|view|payable] [returns (<return types>)]
  • <parameter types> – это параметры, которые вы передаете в функцию. Если вы ничего не передаете в функцию, то параметры не указываются.
  • {internal|external} – это указание на то, является ли функция внутренней или внешней. Если не указывать, то по умолчанию, функция будет считаться внутренней (internal). Если же функция внешняя, то вы должны явно указать ей значение external.
  • [pure|view|payable] – это определение характера функции, они могут быть следующими:
    • pure – функция, которая делает расчет только на основе переданных в нее аргументов, при этом она не читает и не изменяет переменные состояния самого контракта.
    • view – функция доступная только для чтения данных, что дает гарантию о том, что переменные состояния в ней не изменятся (ставится по умолчанию, если не задан другой характер функции).
    • payable – позволяет функции получать эфир при её вызове.
  • returns (<return types>) – это возвращаемые типы функции. Если ваша функция ничего не должна возвращать, то returns не указывается. Если возвращаемых переменных несколько, то указыаете их через запятую с объявлением их типа.

Есть 2 способа вызвать функцию, – либо по ее имени f(), либо используя this.f(). При этом, если вы не объявили функцию с таким именем, либо удалили ее при помощи ключевого слова delete до ее вызова, то это вызовет исключение. Как правило, если происходит вызов внутренней функции, то используется формат f(), а если внешней, – то используется формат this.f().

Встроенные методы внешних функций

Для внешних функций вы можете использовать 2 встроенных метода:

  • .address – возвращает адрес контракта функции.
  • .selector – возвращает селектор функций ABI.

Ранее использовалось еще 2 метода .gas (uint) и .value (uint). Они были объявлены устаревшими в Solidity 0.6.2 и удалены в Solidity 0.7.0. Вместо этого используйте {gas: …} и {value: …}, чтобы указать количество газа или количество wei, отправленных функции, соответственно.

pragma solidity >=0.6.4 <0.9.0;

contract Example {
    function f() public payable returns (bytes4) {
        assert(this.f.address == address(this));
        return this.f.selector;
    }

    function g() public {
        this.f{gas: 10, value: 800}();
    }
}

Специальные функции fallback и receive

  • fallback функция исполняется при вызове контракта, если ни одна из других его функций не соответствует заданной сигнатуре, т.е. по сути если вызывается несуществующая в контракте функция. Также fallback исполняется если вызвали контракт без передачи каких-либо данных либо перевели эфир, но в контракте нет функции receive, которая его бы обработала. Fallback функция всегда по умолчанию принимает любые данные, но чтобы принимать эфир, она должна быть помечена как payable.
  • receive функция исполняется при вызове контракта с пустым значением calldata. Это функция, которая вызывается при передаче простого эфира на адрес контракта, например, при помощи функций send и transfer. Если при передаче эфира receive функции не существует, но в контракте определена fallback функция, то будет вызываться она. Если же не будет определена и fallback функция, то контракт не сможет принимать эфир, а все попытки передать эфир на данный контракт будут вызывать исключение.
contract TestPayable {
    uint x;
    uint y;
    // Эта функция исполняется при отправке любого сообщения на этот контракт
    // включая прием отправленного эфира (когда не определена функция receive).
    // Любой вызов с не пустой calldata на этот контракт будет исполнять
    // fallback функцию (даже если вместе с вызовом отправлеяется эфир).
    fallback() external payable { x = 1; y = msg.value; }

    // Эта функция исполняется при отправке эфира на этот контракт,
    // т.е. для всех вызовов контрактк с пустым calldata.
    receive() external payable { x = 2; y = msg.value; }
}

Функции virtual и override

Базовые функции можно переопределить в унаследованных контрактах, чтобы изменить их поведение. Такие функции должны быть помечены как virtual.

Переопределяемая virtual функция должна быть помечена ключевым словом override.

Правила переопределения override функции:

  • видимость может быть изменена только с external на public,
  • если virtual функция не была payable, то она может быть перепоределена в pure и view,
  • view может быть переопределена в pure,
  • payable не может быть переопределена в какой-либо другой модификатор видимости.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Base {
    function foo() virtual external view {}
}

contract Inherited is Base {
    function foo() override public pure {}
}

События

События позволяют вам удобным образом отображать в логах виртуальной машины Ethereum необходимую информацию о наступлении какого-либо события.

Чтобы определить событие в коде, используется ключевое слово event. А чтобы вызвать это событие, используется ключевое слово emit.

pragma solidity >=0.7.0 <0.9.0;

contract MyContractName {
    event HighestBidIncreased(address bidder, uint amount); // Event

    function bid() public payable {
        // ...
        emit HighestBidIncreased(msg.sender, msg.value); // Triggering event
    }
}

Ошибки

Вы можете заранее описать возможные ошибки и присвоить им имена используя ключевое слово error. При возникновении нетипичной ситуации в коде, вы можете вызвать ошибку по ее имени так же, как и функцию используя ключевое слово revert. Это намного дешевле, чем использовать строковые описания ошибок и дает возможность передававать дополнительные данные.

pragma solidity >=0.7.0 <0.9.0;

/// Not enough funds for transfer. Requested `requested`,
/// but only `available` available.
error NotEnoughFunds(uint requested, uint available);

contract Token {
    mapping(address => uint) balances;
    
    function transfer(address to, uint amount) public {
        uint balance = balances[msg.sender];
        
        if (balance < amount)
            revert NotEnoughFunds(amount, balance);
            
        balances[msg.sender] -= amount;
        balances[to] += amount;
        // ...
    }
}

Cтруктуры

Структура это по сути описание какого-либо объекта с набором его свойств, которая задается ключевым словом struct. Например, вы хотите описать характеристики персонажа в вашей игре. Для этого вы создаете структуру Human и внутри него описываете его характеристики в виде переменных. Структуры разрешено размещать как вне контракта, так и внутри него.

pragma solidity >=0.7.0 <0.9.0;

contract Game {
    struct Human { // Struct
        uint weight;
        uint age;
        bool isMerried;
        address delegate;
    }
}

Перечисления

Это объект, который может хранить в себе только заранее предопределенные в нем значения. Он задается ключевым словом enum. Например, определим в нашей программе объект enum, в котором будем хранить набор 3 цветов. А затем создадим переменную с типом данного объекта и присвоим ей одно из значений данного набора.

pragma solidity >=0.7.0 <0.9.0;

contract MyContractName {
    // define enum colors
    enum Color { Red, Green, Yellow } // Enum

    // set enum value
    Color constant defaultColor = Color.Green;
}

Переменные

Типы переменных

  • State Variable – переменная состояния, значения которых хранятся в хранилище контракта.
  • Local Variable – переменные, которые существуют до выполнения функции.
  • Global Variable – глобальные переменные, которые позволяют получать информацию о блокчейне.

Типы значений

Boolean

Булевая переменная, которая может хранить в себе только 2 состояния: true и false. Пример:

bool public paused = false;

Применяемые операторы: !, &&, ||, ==, !=

Integers

int / uint – целочисленный тип данных со знаком и без знака. Чаще всего указывается с количеством зарезервированных байт под объявляемую переменную. Например, int / uint является аналогом int256 / uint256 м 

int / uint – целочисленный тип данных со знаком и без знака. Чаще всего указывается с количеством зарезервированных битов от 8 до 256 под объявляемую переменную. Например, int / uint является аналогом int256 / uint256.

// переменные int*
int8	= От -128 до 127
int16	= От -32 768 до 32 767
int32	= От -2 147 483 648 до 2 147 483 647
int64	= От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807
int128  = От -170141183460469231731687303715884105728 до 170141183460469231731687303715884105727
int256  = От -57896044618658097711785492504343953926634992332820282019728792003956564819968 до 57896044618658097711785492504343953926634992332820282019728792003956564819967

// переменные uint*
uint8	= От 0 до 255
uint16	= От 0 до 65 535
uint32	= От 0 до 4 294 967 295
uint64	= От 0 до 18 446 744 073 709 551 615
uint256 = От 0 до 115792089237316195423570985008687907853269984665640564039457584007913129639935

Применяемые операторы:
– Сравнение: <=, <, ==, !=, >=, >
– Битовые: &, |, ^, ~
– Сдвига: <<, >>
– Арифметические: +, , унарный –, *, /, %, **

Fixed Point Numbers

Числа с фиксированной точкой сейчас имеют еще не полную поддержку в Solidity. Это значит, что вы можете объявлять этот тип, но не можете его назначить или не можете назначать другие переменных от значения данного типа.

Определяются они ключевыми словами fixedMxN и ufixedMxN соответственно для чисел со знаком и без знака. Где вместо М проставляется количество битов занятых хранимым значением от 8 до 256, а N – количество доступных десятичных знаков от 0 до 80. Причем ufixed и fixed – является аналогом для ufixed128x18 и fixed128x18 соответственно.

Применяемые операторы:
– Сравнение: <=, <, ==, !=, >=, >
– Арифметические: +, , унарный –, *, /, %

Address

Это фиксированное 20 байтовое значение (длинна адреса в Ethereum), которое является основой во всех контрактах. При помощи данного типа вы можете указывать участников, адреса смарт-контактов и адреса собственных токенов.

address nameReg = 0xdfc4bccf1aec515932c2d1ae499f92bb4ce04113;

Адрес также может быть объявлен как payable, в таком случае на этот адрес появляется возможность отправлять эфир при помощи двух дополнительных методов, которые он приобретает: transfer и send.

address payable nameReg = 0xdfc4bccf1aec515932c2d1ae499f92bb4ce04113;

Если вы планируете объявить переменную с типом address на который хотите переводить эфир, то сделайте эту переменную с типом address payable.

Применяемые операторы: <=, <, ==, !=, >=, >

Тип контракта

Каждый контракт имеет свой тип. Контракты могут быть преобразованы в тип адреса и обратно. Причем, преобразование контракта в тип address payable можно только в том случае, если сам контракт имеет функцию “получения и возврата” (receive or payable fallback function).

Если вы объявите локальную переменную контракта, то можете вызывать функции этого контракта. Также вы можете создавать экземпляры контракта при помощи слова new, это значит что контракт будет создаваться заново. Передав переменную контракта C в функцию type(C), вы сможете увидеть информацию о контракте.

Массивы

Массивы могут иметь статический или динамический размер. Массив Т фиксированного размера k записывается как T[k], а динамический массив записывается как T[]. Например, массив из 5 динамических массивов записывается как uint[][5]. Обратите внимание, что это обратная нотация по сравнению с большинством других языков (в них было бы наоборот).

Элементами массива могут быть переменные любого типа, включая mapping и struct. Если пометить массивы как public то Solidity создаст для них геттер (функцию получения из него элементов). Числовой индекс будет являться обязательным параметром для этого геттера. Доступ к несуществующему элементу массива вызывает ошибку. При помощи методов .push() и .push(value) вы можете добавить новый элемент в конец массива, причем вызов метода .push() добавит пустой элемент и возвратит ссылку на него.

Массивы типа bytes и string.

Массивы с типом bytes и string представляют из себя специальные массивы. Например, bytes похож на byte[], но плотно упакован в памяти и функции обратного вызова, а строка равна байтам, но не разрешает доступа к длине или индексу.

Вы можете использовать bytes вместо byte[], т.к. это дешевле, поскольку byte[] добавляет 31 байт между элементами. Используйте bytes для необработанных байтовых данных произвольной длинны (UTF-8). А если вы можете ограничит длину произвольным количеством байтов от 1 до 32 то используйте типы bytes1 .. bytes32, так как это намного дешевле. Если вы хотите объединить несколько переменных типа bytes, то используйте встроенную функцию bytes.concat.

bytes s = "Storage";
function f(bytes calldata c, string memory m, bytes16 b) public view {
    bytes memory a = bytes.concat(s, c, c[:2], "Literal", bytes(m), b);
    assert((s.length + c.length + 2 + 7 + bytes(m).length + 16) == a.length);
}
Выделение массивов Memory

Динамические массивы memory можно создать при помощи оператора new. В отличие от storage массивов, у них нельзя изменить размер, поэтому вы должны заранее рассчитать под них требуемый размер.

uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
Методы массива
  • length – позволяет понять количество элементов в массиве,
  • push() – позволяет добавить новый пустой элемент в конец массива, возвращает ссылку на добавленный элемент,
  • push(x) – позволять добавить новый элемент “х” в конец массива, ничего не возвращает,
  • pop – удаляет последний элемент массива.
Срезы массивов

Это представление непрерывной части массива начиная и заканчивая определенным его индексом.

Вызывается это как x[start:end], где start это начальный индекс, а end – конечный, которым заканчивается представление части массива. Причем, указание start и end носит опциональный характер, т.е. если не указывать start то это будет считаться значением 0, а если не указывать end, то это будет считаться последним элементом массива.

Строки

В переменных типа string вы можете хранить строковые значения, которые заключаются в одинарные или двойные кавычки. В них также есть поддержка escape символов, таких как \n, \xNN и т.д.

string public constant name = "This is my string value;

Solidity не имеет возможности манипулирования строками, но есть сторонние строковые библиотеки. Например, вы можете сравнить две строки по их хэшу: keccak256(abi.encodePacked(s1)) == keccak256(abi.encodePacked(s2)) либо объединить две строки при помощи bytes.concat(bytes(s1), bytes(s2)).

Unicode литералы

Обычные строки задаются в кодировке ASCII, но если в строке вы планируете использовать UTF-8, то нужно перед двойными или одинарными кавычками указать ключевое слово unicode.

string memory a = unicode"Hello 😃";
HEX литералы

Шестнадцатеричные значения задаются также как строки в двойных или одинарных кавычках, но перед ними нужно указать ключевое слово hex. При указании hex значения, по желанию, вы можете разделять байты знаком подчеркивания.

string memory a = hex"001122FF"

Словари

Словари (Mapping Type) используются для сопоставления между собой двух наборов значений, как правило разного типа. 

Синтаксис: mapping(_KeyType => _ValueType).

_KeyType может быть любым встроенным типом значения, байтами, строкой или любым контрактом или типом перечисления. _ValueType может быть любого типа, включая сопоставления, массивы и структуры.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

contract MappingUser {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        m.update(100);
        return m.balances(address(this));
    }
}
Как проверить существование ключа в словаре?

В Solidity нет такого понятие как “существование ключа в словаре”, т.к. по умолчанию он существует всегда, но равен 0. Поэтому, чтобы проверить отсутствие ключа в словаре, вам нужно проверить значение по вашему ключу возвращает 0 в соответствии с его типом. Т.е. для адреса это mapping[key] == address(0x0), для байтового массива это mapping[key] = bytes4(0x0) и т.д. Для структур – обычно в самой структуре задают дополнительный атрибут типа bool, который при заполнении структуры всегда устанавливают в true, ну и соответственно проверку наличия ключа делают по этому флагу (пример: Game[id].isValue == false).

// с версии 0.8.6 можно определять наличие ключа в словаре еще и так
if (abi.encodePacked(balances[user_id]).length > 0) { delete balances[addr]; }

Единицы измерения и глобальные переменные

Единицы измерения валюты Эфира

Когда в контракте необходимо указать стоимость в эфирах, вы можете использовать это суффиксы wei, gwei и ether. Если вы не указываете еденицу измерения, то по умолчанию считается, что вы указываете стоимость в wei

assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);
Номиналы finney и szabo были удалены в версии 0.7.0.

Единицы измерения времени

Секунды в Solidity считаются базовой единицей. Также вы можете использовать следующие суффиксы для указания других интервалов измерения времени: seconds, minutes, hours, days и weeks.

1 == 1 seconds
1 minutes == 60 seconds
1 hours == 60 minutes
1 days == 24 hours
1 weeks == 7 days
Суффикс years был удален в версии 0.5.0

Специальные переменные и функции

Свойства Блока и Транзакций

  • blockhash(uint blockNumber) returns (bytes32): хэш данного блока, если номер блока является одним из 256 самых последних блоков; в противном случае возвращает 0. Ранее эта функция называлась block.blockhash, но была удалена в версии 0.5.0.
  • block.chainid (uint): текущий идентификатор цепочки
  • block.coinbase (address payable): текущий адрес майнера блока
  • block.difficulty (uint): текущая сложность блока
  • block.gaslimit (uint): лимит газа в текущем блоке
  • block.number (uint): текущий номер блока
  • block.timestamp (uint): текущая временная метка блока в секундах с эпохи unix
  • gasleft() returns (uint256): оставшийся газ. Ранее эта функция называлась msg.gas, но была удалена в версии 0.5.0.
  • msg.data (bytes calldata): полные данные calldata
  • msg.sender (address): отправитель сообщения (текущий вызов)
  • msg.sig (bytes4): первые четыре байта calldata (т.е. идентификатор функции)
  • msg.value (uint): количество wei, отправленных с сообщением
  • tx.gasprice (uint): цена газа за транзакцию
  • tx.origin (address): отправитель транзакции (полная цепочка вызовов)
Значения всех методов msg, включая msg.sender и msg.value, могут изменяться для каждого вызова внешней функции, включая вызовы библиотечных функций.
Не полагайтесь на block.timestamp или blockhash как на источник случайности.

Функции кодирования и декодирования ABI

abi.decode(bytes memory encodedData, (…)) returns (…): ABI-декодер заданныех данных, а типы указаны в скобках в качестве второго аргумента.
Пример:

(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))

abi.encode(…) returns (bytes memory): ABI-кодер, кодирует заданные аргументы

abi.encodePacked(…) returns (bytes memory): Выполняет пакетное кодирование заданных аргументов. Обратите внимание, что пакетное кодирование может быть неоднозначным!

abi.encodeWithSelector(bytes4 selector, …) returns (bytes memory): ABI-кодер, кодирует заданные аргументы, начиная со второго, и добавляет в начало заданный четырехбайтовый селектор.

abi.encodeWithSignature(string memory signature, …) returns (bytes memory): Идентичен abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), …)

Эти функции кодирования могут использоваться для обработки данных вызова внешних функций без фактического вызова этих функции. Кроме того, keccak256 (abi.encodePacked (a, b)) – это способ вычисления хэша структурированных данных (хотя имейте в виду, что можно создать «конфликт хэша», используя различные типы параметров функции).

Обработка ошибок

assert(bool condition) – вызывает паническую ошибку и, следовательно, не вносит изменение состояния, если условие не выполняется. Используется при внутренних ошибках.

require(bool condition) – откат если условие не выполняется. Используется при генерации ошибок на входах или для внешних компонентов.

require(bool condition, string memory message) – откат если условие не выполняется. Используется при генерации ошибок на входах или для внешних компонентов. Также выдает сообщение об ошибке.

revert() – прервать выполнение и вернуть изменения состояния.

revert(string memory reason) – прервать выполнение, вернуть изменения состояния и вывести причину ошибки.

Математические и криптографические функции

addmod(uint x, uint y, uint k) returns (uint) – вычисляет (x + y) % k, где суммирование происходит с произвольной точностью и не повторяется при 2**256. Утверждают, что k! = 0 начиная с версии 0.5.0.

mulmod(uint x, uint y, uint k) returns (uint) – вычисляет (x* y) % k, где умножение происходит с произвольной точностью и не повторяется при 2**256. Утверждают, что k! = 0 начиная с версии 0.5.0.

keccak256(bytes memory) returns (bytes32) – вычислить хэш Keccak-256 аргументов. Раньше для keccak256 существовал псевдоним sha3, который был удален в версии 0.5.0.

sha256(bytes memory) returns (bytes32) – вычислить хэш SHA-256 аргументов.

ripemd160(bytes memory) returns (bytes20) – вычислить хэш RIPEMD-160 аргументов.

ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) – восстановить адрес, связанный с открытым ключом, из сигнатуры эллиптической кривой или вернуть ноль в случае ошибки. Обратите внимание, что ecrecover возвращает address, а не address payable. Параметры функции соответствуют значениям ECDSA подписи:

r = первые 32 байта подписи

s = вторые 32 байта подписи

v = финальный 1 байт подписи

Методы типа Address

<address>.balance – баланс адреса в wei.

<address>.code (bytes memory) – код адреса, может быть пустым

<address>.codehash (bytes32) – хэшкод адреса.

<address payable>.transfer(uint256 amount) – отправить заданное количество amount Wei на Адрес, при ошибке откатывает изменения

<address payable>.send(uint256 amount) returns (bool) – отправить заданное количество amount Wei на Адрес, при ошибке откатывает изменения

<address>.call(bytes memory) returns (bool, bytes memory) – запуск низкоуровневого call с полезной нагрузкой, возвращает успешные условия и данные, использует весь доступный газ

<address>.delegatecall(bytes memory) returns (bool, bytes memory) – запуск низкоуровневого DELEGATECALL с полезной нагрузкой, возвращает успешные условия и данные, использует весь доступный газ

<address>.staticcall(bytes memory) returns (bool, bytes memory) – запуск низкоуровневого STATICCALL с полезной нагрузкой, возвращает успешные условия и данные, использует весь доступный газ

Артефакты контракта

this – текущий контракт, явно конвертируемый в Address

selfdestruct(address payable recipient) – удаление текущего контракта с отправкой его средств на указанный адрес и завершение исполнения. Обратите внимание, что самоуничтожение имеет некоторые особенности, унаследованные от EVM:

  • функция получения контракта-получателя не выполняется,
  • контракт действительно уничтожается только в конце транзакции, и откат может «отменить» уничтожение. Более того, все функции текущего контракта могут быть вызваны напрямую, включая текущую функцию.

Информация о типе

Выражение type(X) может использоваться для получения информации о типе X. В настоящее время эта функция ограничена (X может быть только либо контрактом, либо целочисленным типом).

Для типов доступны следующие свойства:

  • type(C).name – имя контракта
  • type(C).creationCode – массив байтов памяти, содержащий байт-код создания контракта.
  • type(C).runtimeCode – массив байтов памяти, содержащий байт-код времени выполнения контракта.
  • type(I).interfaceId – значение bytes4, содержащее идентификатор текущего интерфейса EIP-165.
  • type(T).min – наименьшее значение, представимое типом T.
  • type(T).max – наибольшее значение, представимое типом T.

При подготовке данного руководства использовались официальные источники – https://docs.soliditylang.org/

FAQ – ответы на популярные вопросы

Как получить рандомное число от 0 до 899?

// Basically you get a number from 0 - 899, and then add 100 to match your offset.
function random() internal returns (uint) {
    return uint(keccak256(abi.encodePacked(now, msg.sender, nonce))) % 900;
}

Как вывести лог в консоль Remix для отладки?

import "hardhat/console.sol";
...
console.log(accounts[_address].isActive);

 

Рейтинг
( 8 оценок, среднее 4.5 из 5 )
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: