Что такое библиотеки в Solidity?
“Библиотеки можно рассматривать, как неявные базовые смарт-контракты для смарт-контрактов, которые их используют” из документации языка Solidity
Библиотека в Solidity - это тип смарт-контракта, содержащий многократно используемый код. После развертывания в блокчейне (развёртывается только один раз) ему присваивается определённый адрес, а его свойства / методы могут многократно использоваться другими смарт-контрактами в сети Ethereum.
Они позволяют вести разработку более модульным способом. Иногда полезно думать о библиотеке как о куске кода, который можно вызвать из любого смарт-контракта без необходимости его повторного развертывания.
Преимущества
Однако библиотеки в Solidity не ограничиваются только одной возможностью по повторному использованию. Вот некоторые другие преимущества:
Удобство использования: функции в библиотеке могут использоваться многими смарт-контрактами. Если у вас много смарт-контрактов, которые содержат некоторый общий для каждого смарт-контракта код, то вы можете вынести этот общий код в библиотеку.
Экономично: использование кода в виде библиотеки сэкономит вам gas, так как расход gas’а также зависит от размера смарт-контракта. Использование базового смарт-контракта вместо библиотеки для разделения общего кода не сэкономит gas, потому что в Solidity наследование работает путем копирования кода.
Дополнения: библиотеки в Solidity можно использовать для добавления функций к типам данных. Например, подумайте о библиотеках как о стандартных библиотеках или пакетах, которые вы можете использовать в других языках программирования для выполнения сложных математических операций над числами.
Ограничения
В Solidity библиотеки не имеют своего состояния и, следовательно, имеют следующие ограничения:
Они не имеют хранилища (поэтому не могут иметь изменяемых переменных состояния)
Они не могут хранить
ether
(поэтому не могут иметь функцию отката (fallback
))Не позволяет использовать
payable
функции (так как они не могут хранить ether)Не могут ни наследовать, ни быть наследуемыми
Не могут быть уничтоженными (нет функции
selfdestruct()
с версии 0.4.20)
"Библиотеки не предназначены для изменения состояния смарт-контракта, они должны использоваться только для выполнения простых операций на основе входных данных и возврата результата".
Однако библиотеки позволяют сэкономить значительное количество газа (и, следовательно, не загрязнять блокчейн повторяющимся кодом), поскольку один и тот же код не нужно развертывать снова и снова. Различные смарт-контракты на Ethereum могут просто полагаться на одну и ту же уже развернутую библиотеку.
Тот факт, что несколько смарт-контрактов зависят от одного и того же фрагмента кода, может сделать среду более безопасной. Представьте себе, что у вас есть не только хорошо проверенный код для ваших проектов (как например заготовки кодов OpenZeppelin), но и возможность полагаться на уже развернутый в блокчейне библиотечный код, который уже используют другие смарт-контракты. Это, безусловно, помогло бы в данном случае.
2. Как создать библиотечный смарт-контракт?
Определите библиотеку и допустимые переменные
Вы определяете библиотечный смарт-контракт с помощью ключевого слова library
вместо традиционного ключевого слова contract
, используемого для стандартных смарт-контрактов. Просто объявите libary под оператором pragma solidity (версия компилятора). Смотрите пример кода ниже.
library libraryName {
// объявление переменной struct, enum или constant
// определение функции с телом
}
Как мы уже видели, библиотечные смарт-контракты не имеют хранилища. Поэтому они не могут хранить переменные состояния (переменные состояния, которые не являются константами). Однако библиотеки все же могут реализовать некоторые типы данных:
struct
иenum
: это переменные, определяемые пользователем.любая другая переменная, определенная как
constant
(неизменяемая), поскольку неизменяемые переменные хранятся в байткоде смарт-контракта, а не в памяти.
Начнем с простого примера: библиотека для математических операций.
Ниже мы создали простую математическую библиотеку MathLib, которая содержит базовую арифметическую операцию, принимающую на вход 2 беззнаковых целых числа и возвращающую результат арифметической операции.
library MathLib {
struct MathConstant {
uint256 Pi; // π (Pi) ≈ 3.1415926535...
uint256 Phi; // Золотая пропорция ≈ 1.6180339887...
uint256 Tau; // Tau (2pi) ≈ 6.283185...
uint256 Omega; // Ω (Omega) ≈ 0.5671432904...
uint256 ImaginaryUnit; // i (мнимая единица) = √-1
uint256 EulerNb; // Число Эйлера ≈ 2.7182818284590452...
uint256 PythagoraConst; // Константа Пифагора (√2) ≈ 1.41421...
uint256 TheodorusConst; // Константа Теодоруса (√3) ≈ 1.73205...
}
}
Библиотека SafeMath, доступная в коллекции смарт-контрактов OpenZeppelin, используется многими проектами для защиты от переполнения.
3. Как развернуть библиотеки?
Развертывание библиотеки немного отличается от развертывания обычного смарт-контракта. Вот два сценария:
Встроенная библиотека: если смарт-контракт использует библиотеку, которая имеет только внутренние (
internal
) функции, то EVM просто встраивает библиотеку в смарт-контракт. Вместо того чтобы использоватьdelegatecall
для вызова функции, он просто использует операторJUMP
(обычный вызов метода). В этом сценарии нет необходимости в отдельном развертывании библиотеки.Связанная библиотека: с другой стороны, если библиотека содержит публичные (
public
) или внешние (external
) функции, то ее необходимо развернуть как отдельный смарт-контракт. При развертывании библиотеки генерируется уникальный адрес в блокчейне. Этот адрес должен быть связан с вызывающим смарт-контрактом.
4. Как использовать библиотеку в смарт-контракте?
Шаг 1: Импорт библиотеки
Под pragma просто укажите следующее утверждение:
import "./libfile.sol" as LibraryName;
Файл library-file может содержать несколько библиотек. Используя фигурные скобки {} в операторе import, вы можете указать, какую библиотеку вы хотите импортировать. Представьте, что у вас есть файл libfile.sol следующего вида :
library Library1 {
// Код из библиотеки 1
}
library Library2 {
// Код из библиотеки 2
}
library Library3 {
// Код из библиотеки 3
}
Вы можете указать, какую библиотеку вы хотите использовать в вашем основном смарт-контракте, следующим образом:
// Мы решили использовать здесь только библиотеки 1 и 3, и исключить библиотеку 2
import {Library1, Library3} from "./libfile.sol";
contract MyContract {
// Код вашего контракта здесь
}
Шаг 2: Использование библиотеки
Чтобы использовать библиотеку в смарт-контракте, мы используем синтаксис
using LibraryName for Type.
Эта указание используется для присоединения библиотечных функций (из библиотеки LibraryName
) к любому типу (Type
).
LibraryName - имя библиотеки, которую мы хотим импортировать.
Type - тип переменной, для которой мы собираемся использовать библиотеку. (Мы также можем использовать оператор подстановки *, чтобы прикрепить функции из библиотеки ко всем типам).
// Использовать библиотеку для целых беззнаковых чисел
using MathLib for uint256;
// Использовать библиотеку для любого типа данных
using MathLib for *;
В этих случаях к смарт-контракту присоединяются все функции из библиотеки, включая те, где тип первого параметра не совпадает с типом объекта. Тип проверяется в момент вызова функции, и выполняется перегрузка функции.
При добавлении библиотеки её типы данных, включая библиотечные функции, становятся доступными в контакте без необходимости добавления дополнительного кода. Когда вы вызываете библиотечную функцию, библиотечные функции получают объект, на котором они вызываются, в качестве первого параметра, подобно переменной self в Python.
using MathLib for uint256;
uint256 a = 10;
uint256 b = 10;
uint256 c = a.subUint(b);
Мы все еще можем выполнить uint c = a - b; Это вернет тот же результат - 0. Однако библиотека MathLib имеет некоторые дополнительные свойства для защиты от переполнения, например, assert(a >= b); которая проверяет, что первый операнд a больше или равен второму операнду b, чтобы операция вычитания не привела к отрицательному значению.
Еще один хороший приём, который делает наш код понятнее - это использование ключевого слова using с библиотечной информацией в качестве метода. В нашем примере с MathConstant это будет:
using MathLib for MathLib.MathConstant;
5. Как взаимодействовать с библиотеками?
Немного теории
Если вызываются библиотечные функции, их код выполняется в контексте вызывающего смарт-контракта.
Давайте немного углубимся в документацию Solidity. В которой говорится следующее: "Библиотеки можно рассматривать как неявные базовые смарт-контракты для смарт-контрактов, которые их используют":
Они не будут явно видны в иерархии наследования
...но вызовы библиотечных функций выглядят так же, как вызовы функций явных базовых смарт-контрактов (L.f(), если L - имя библиотеки).
Вызов функции из библиотеки использует специальную инструкцию в EVM: опкод DELEGATECALL. Это приведет к тому, что контекст вызова будет передан библиотеке, как если бы это был код, выполняемый в самом смарт-контракте. Рассмотрим пример кода:
library MathLib {
function multiply(uint256 a, uint256 b)
public
view
returns (uint256, address)
{
return (a * b, address(this));
}
}
contract Example {
using MathLib for uint256;
address owner = address(this);
function multiplyExample(uint256 _a, uint256 _b)
public
view
returns (uint256, address)
{
return _a.multiply(_b);
}
}
Если мы вызовем функцию multiply()
библиотеки MathLib из нашего смарт-контракта (через функцию multiplyExample
), будет возвращен адрес Example, а не адрес библиотеки. Функция multiply() компилируется как внешний вызов (DELEGATECALL
), но если мы проверим адрес, возвращаемый в операторе return
, то увидим адрес нашего смарт-контракта (а не адрес смарт-контракта библиотеки).
Исключение: библиотечные функции можно вызывать напрямую (без использования DELEGATECALL), только если они не изменяют состояние (view
или pure
функции).
Более того, внутренние функции библиотек видны во всех смарт-контрактах, как если бы библиотека была базовым смарт-контрактом.
Давайте изучим процесс вызова внутренней (internal
) библиотечной функции, чтобы понять, что именно происходит:
Контракт A вызывает внутреннюю функцию B из библиотеки L.
Во время компиляции EVM подтягивает внутреннюю функцию B в смарт-контракт A. Это было бы похоже на то, как если бы функция B была жестко закодирована в смарт-контракте A.
Вместо DELEGATECALL будет использоваться обычный вызов JUMP.
Вызовы внутренних функций используют ту же конвенцию внутреннего вызова:
могут быть переданы все внутренние типы.
типы, хранящиеся в памяти, будут передаваться по ссылке и не копироваться.
Несмотря на то, что у библиотек нет хранилища, они могут модифицировать хранилище своего связанного смарт-контракта, передавая аргумент хранилища в параметры своей функции. Таким образом, любое изменение, сделанное библиотекой через свою функцию, будет сохранено в собственном хранилище смарт-контракта.
Поэтому ключевое слово this
будет указывать на смарт-контракт вызова, а точнее, ссылаться на хранилище вызывающего смарт-контракта.
Поскольку библиотека - это изолированный кусок кода, она может получить доступ к переменным состояния из вызывающего смарт-контракта, только если они явно предоставлены. В противном случае у нее не было бы возможности назвать их.
Как использовать библиотечную функцию
Чтобы использовать библиотечную функцию, мы выбираем переменную, к которой хотим применить библиотечную функцию, и добавляем символ .
, за которым следует имя библиотечной функции, которую мы хотим использовать.
Type variable = variable.libraryFunction(Type argument);
Ниже приведен пример.
contract MyContract {
using MathLib for uint256;
uint256 a = 10;
uint256 b = 10;
uint256 c = a.add(b);
}
library MathLib {
function add(uint256 a, uint256 b) external pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
6. Понимание функций в библиотеках
В библиотеке могут быть нереализованные функции (как в интерфейсах). В результате, функции такого типа должны быть объявлены как публичные или внешние. Нереализованные фукнции не могут быть приватными или внутренними.
Вот внутренняя функция, реализованная в функции testConnection()
:
function testConnection(address _from, address _to)
internal
returns (bool)
{
if (true) {
emit testConnection(_from, _to, connection_success_message);
return true;
}
}
Примером нереализованных функций в нашей ситуации могут быть функции обратного вызова при подключении и отключении. При этом вы можете реализовать свой собственный код в теле функции основного смарт-контракта.
function onConnect() public;
function onDisconnect() public;
7. События и библиотеки
Точно так же, как у библиотек нет своего хранилища, у них нет и журнала событий. Однако библиотеки могут отправлять события.
Любое событие, созданное библиотекой, будет сохранено в журнале событий того смарт-контракта, который вызывает функцию, создающую событие в библиотеке.
Единственная проблема заключается в том, что на данный момент ABI смарт-контракта не отражает события, которые могут создавать используемые им библиотеки. Это сбивает с толку клиентов, таких как web3, которые не могут декодировать вызванное событие или понять, как декодировать его аргументы.
8. Отображения в библиотеках
Использование типа отображения (mapping
) внутри библиотек отличается от использования этого типа в традиционных смарт-контрактах Solidity. Здесь мы поговорим об использовании этого типа в качестве параметра функции.
Вы можете использовать отображение в качестве параметра функции с любой видимостью: публичной (public
), частной (private
), внешней (internal
) и внутренней (external
).
Для сравнения, отображения можно передавать в качестве параметра только для internal
или private
функций внутри смарт-контрактов.
Предупреждение: местоположение данных может быть только в хранилище, если отображение передается в качестве параметров функции.
Поскольку мы используем отображение внутри библиотеки, мы не можем создать его внутри библиотеки (помните, что библиотеки не могут хранить переменные состояния). Давайте рассмотрим "возможный" пример того, как это может выглядеть:
library Messenger {
event LogFunctionWithMappingAsInput(
address from,
address to,
string message
);
function sendMessage(address to, mapping(string => string) storage aMapping)
public
{
emit LogFunctionWithMappingAsInput(msg.sender, to, aMapping["test1"]);
}
}
9. Использование структур внутри библиотек
library Library3 {
struct hold {
uint256 a;
}
function subUint(hold storage s, uint256 b) public view returns (uint256) {
// Make sure it doesn’t return a negative value.
require(s.a >= b);
return s.a - b;
}
}
Обратите внимание, как функция subUint получает struct в качестве аргумента. В Solidity v0.4.24 это невозможно в смарт-контрактах, но возможно в Solidity библиотеках.
11. Использование модификаторов внутри библиотек
В библиотеках можно использовать модификаторы. Однако мы не можем экспортировать их в наш смарт-контракт, поскольку модификаторы являются функциями времени компиляции (своего рода макросами).
Смотрите здесь: Solidity modifiers in library?
Поэтому модификаторы применяются только к функциям самой библиотеки и не могут быть использованы внешним смарт-контрактом библиотеки.
12. Что нельзя делать с библиотеками в Solidity?
Библиотеки не могут содержать изменяемые переменные. Если вы попытаетесь добавить переменную, которую можно изменить, вы получите следующую ошибку в Remix:
TypeError: library can't have non-constant state variable
Библиотеки не имеют журналов событий.
Уничтожить библиотеку невозможно.
Библиотека не может наследоваться.
13. Некоторые популярные существующие библиотеки
Вот список некоторых библиотек, написанных на Solidity, и их авторов:
Modular network: включает несколько библиотечных инструментов, таких как ArrayUtils, BasicMath, CrowdSale, LinkedList, StringUtils, Token, Vesting и Wallet
OpenZeppelin: библиотеки, такие как AccessControl, ECDSA, MerkleProof, SafeERC20, ERC165Checker, Math, SafeMath (для защиты от переполнения), Address, Arrays
Dapp-bin от Ethereum: включает такие интересные библиотеки, как IterableMapping, DoublyLinkedList, и еще одну StringUtils