Оглавление

Как создаются смарт-контракты?

Ключевое слово «это»

Абстрактные контракты

Наследование контрактов

Ключевое слово «супер»

Переопределение функции

Деактивировать и самоуничтожиться

Как создаются смарт-контракты?

EVM в Ethereum знает, что смарт-контракт создается, когда внешняя учетная запись (EOA):

  • отправляет транзакцию
  • указывает нулевой адрес (0x0000000000000000000000000000000000000000 ) в качестве получателя.

Поле данных транзакции содержит скомпилированный байт-код смарт-контракта, написанный на Solidity.

Хотите посмотреть, как выглядит это поле данных/байт-код?

Давайте сначала установим компилятор solc.

brew update
brew upgrade
brew tap ethereum/ethereum
brew install solidity

Чтобы убедиться, что solc успешно установлен, выполните следующую команду:

solc --version

Давайте создадим новый файл HelloWorld.sol и добавим следующий код Solidity:

pragma solidity >=0.4.22 <0.6.0;
contract HelloWorld {
    string public text  = "hello world";
}

Откройте терминал, перейдите в папку, в которой у вас есть файл HelloWorld.sol, и выполните следующую команду:

solc HelloWorld.sol --bin

Возникли какие-либо проблемы? Не стесняйтесь обращаться к документации по установке компилятора solc.»

Вы должны получить следующий вывод:

======= HelloWorld.sol:HelloWorld =======
Binary:
60806040526040518060400160405280600b81526020017f68656c6c6f20776f726c640000000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610107565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100a357805160ff19168380011785556100d1565b828001600101855582156100d1579182015b828111156100d05782518255916020019190600101906100b5565b5b5090506100de91906100e2565b5090565b61010491905b808211156101005760008160009055506001016100e8565b5090565b90565b6101ab806101166000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80631f1bd69214610030575b600080fd5b6100386100b3565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561007857808201518184015260208101905061005d565b50505050905090810190601f1680156100a55780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b60008054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101495780601f1061011e57610100808354040283529160200191610149565b820191906000526020600020905b81548152906001019060200180831161012c57829003601f168201915b50505050508156fea26469706673582212203c453aaf3f2976515227f11fba099d190cba5cfbcdcc0ad3adddbf3921c3950064736f6c637826302e362e342d646576656c6f702e323032302e332e312b636f6d6d69742e62363561313635640057

Ключевое слово this

Ключевое слово this в Solidity относится к текущему контракту. Он преобразуется в тип address и позволяет:

  • получить адрес договора: address(this)`
  • получить количество эфира по контракту: address(this).balance
  • получить один из селекторов функций контракта (= подпись функции): this.yourFunction.selector
  • вызвать функцию в рамках контракта, определенного как external, используя синтаксис this.externalFunction().

Вот несколько фрагментов кода о том, как использовать ключевое слово this.

function getContractAddress() public view returns (address) {
    return address(this);
}
function getContractBalance() public view returns (uint) {
    return address(this).balance;
}
// get function signature of `getContractBalance()`
function getFunctionSelector() public pure returns(bytes4) {
    return this.getContractBalance.selector;
}

Абстрактные контракты

Что такое абстрактный договор?

Solidity поддерживает еще более объектно-ориентированный дизайн, например abstract контрактов. Они содержат только определения функций, без реализации (или хотя бы одной). Затем другие контракты могут наследовать их и выполнять их функции.

Но почему это полезно? Отделение определения контракта от его реализации дает несколько преимуществ:

  • Лучшая расширяемость и самодокументирование
  • Облегчайте шаблоны, такие как Шаблонный метод.
  • Удалить дублирование кода

Абстрактные контракты полезны так же, как определение методов в интерфейсе. Как указано в документации Solidity:

Это способ для дизайнера абстрактного контракта сказать: «Любой мой ребенок должен реализовать этот метод.

Когда контракт должен быть помечен abstract ?

Контракт необходимо пометить как abstract в двух случаях:

  • хотя бы одна из его функций не реализована.
  • Он наследуется от контракта abstract и не реализует все его нереализованные функции.

В приведенном ниже примере функция utterance() была определена, но реализация не была предоставлена ​​(поскольку в фигурных скобках { } ничего нет).

pragma solidity >=0.4.0 <0.7.0;
abstract contract Feline {
    function utterance() public virtual returns (bytes32);
}

Абстрактные контракты имеют одну особенность: они не могут быть реализованы напрямую. То же самое применимо, даже если в контракте реализованы все его функции, но они определены с помощью ключевого слова abstract.

Примечание: обратите внимание, что функция без реализации отличается от типа функции, хотя их синтаксис очень похож.

Наследование контрактов

В Solidity контракты ведут себя как классы в объектно-ориентированном программировании: они могут наследоваться и быть унаследованными. Эта функция позволяет создавать сложные шаблоны проектирования, такие как шаблон Delegate Proxy.

Как использовать наследование?

Чтобы использовать наследование, просто используйте ключевое слово is и укажите контракт, из которого оно получено.

contract A {}
contract B is A {}

Контракт также может наследоваться от нескольких контрактов, как показано ниже:

contract A {}
contract B {}
contract C is A, B {}

Используя предыдущий пример, мы можем подумать, что для развертывания контракта C мы должны сначала развернуть контракты A и B. Однако это не так.

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

Это означает, что все внутренние вызовы функций базовых контрактов будут использовать внутренние вызовы функций.

Порядок наследования

Порядок наследования контракта основан на линеаризации C3. Документация Solidity объясняет следующее:

порядок, в котором базовые классы даны в директиве 'is”, важен: вы должны перечислить прямые базовые контракты в порядке от «наиболее похожих на базовые» до «наиболее производных».

Таким образом, если контракт наследует более одного контракта, он должен наследовать от «наиболее базового»сначала до «наиболее производного»последнего, как в следующем примере.

contract Person {}
contract Employee is Person {}
contract Teacher is Employee, Person {} // will not compile!
contract Teacher is Person, Employee {} // will compile

В приведенном выше примере вы получите сообщение об ошибке:

“Linearization of inheritance graph impossible”

Порядок, в котором родительские контракты объявляются в дочерних контрактах, определяет структуру хранилища.

Для получения дополнительной информации я рекомендую этот очень подробный пост в блоге от Gnosis о контрактах делегирования прокси

Супер ключевое слово

Можно вызывать функции выше в иерархии наследования, используя ключевое слово super. Используйте super.functionName(), если вы хотите вызвать функцию на один уровень выше в плоской иерархии наследования.

Вернемся к нашему примеру контракта Employee. Наша функция greetings(), которую мы переопределили, не подходит для общего назначения. Если вы хотите поприветствовать своих коллег в своем офисе, просто скажите "Здравствуйте".

Эта функция не обрабатывает оба случая, поэтому нам может понадобиться написать новый.

pragma solidity ^0.6.0;
contract Person {
    
    function greetings() public pure virtual returns (string memory) 
    {
        return "Hello";
    }
    
}
contract Employee is Person {
    
    function greetings() public pure virtual override returns (string memory) {
        return "Hello. How can I help you? :)";
    }
    
    function welcomeMessage(bool isClient) public pure returns (string memory) {
        
        return isClient ? greetings() : super.greetings();
}
    
}

Наш новый welcomeMessage() теперь справляется с обоими случаями.

  • Если мы разговариваем с клиентом, мы вызываем функциюgreetings() внутри Employee контракта.
  • Если мы не разговариваем с клиентом, мы звоним в super.greetings(). Это позволяет нам ссылаться на функцию greetings() на один уровень выше, в контракте Person.

Одним из дополнительных преимуществ использования super является то, что он использует JUMP внутри, а не вызов сообщения.

Переопределение функции

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

Мы повторно используем наш предыдущий граф наследования Person -> Employee -> Teacher и напишем несколько функций.

Введение

Примечание.Первые примеры кода — это просто вводная информация, позволяющая получить общий обзор. Они применяются только для Solidity 0.5.0.
Что касается версии 0.6.0, см. следующий раздел под названием«Виртуальные и переопределяемые ключевые слова».

Напишем первую функцию внутри контракта Person.

pragma solidity ^0.6.0;
contract Person {
    
    function greetings() public pure returns (string memory) {
        return "Hello";
    }
}

Контракт Employee напрямую наследует эту функцию. Но что, если вы хотите адаптировать приветственное сообщение к сотруднику?

Если сотрудник работает в отделе обслуживания клиентов, сообщение должно быть адаптировано к тому, что следует сказать клиенту, например «Здравствуйте. Чем я могу вам помочь?».

Мы уже видели, что переопределение функций позволяет легко это сделать.

pragma solidity ^0.5.0;
contract Person {
    
    function greetings() public pure returns (string memory) {
        return "Hello";
    }
}
contract Employee is Person {
    
    function greetings() public pure returns (string memory) {
        return "Hello. How can I help you? :) ";
    }
    
}

Все идет нормально. Но давайте посмотрим на следующий образец. Какое сообщение вы должны получить, позвонив sayHello()`? "Здравствуйте" или "Здравствуйте, чем я могу вам помочь?" ?

pragma solidity ^0.5.0;
contract Person {
    
    function greetings() public pure returns (string memory) {
        return "Hello";
    }
}
contract Employee is Person {
    
    function greetings() public pure returns (string memory) {
        return "Hello. How can I help you? :) ";
    }
    
}
contract Teacher is Person, Employee {
    
    function sayHello() public pure returns(string memory) {
        greetings();
    }
}

Те, кто знаком с полиморфизмом, скажут " Hello. How can I help you? :) . Однако есть более подробное объяснение из документации Solidity:

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

Если базовый контракт уже искали, он пропускается.

Полиморфизм означает, что вызов функции (внутренний и внешний) всегда выполняет функцию с тем же именем (и типами параметров) в самом производном контракте в иерархии наследования.

Это основа переопределения функций.

Виртуальное и переопределяемое ключевое слово

Начиная с версии Solidity 0.6.0, переопределяющие функции должны указываться явно. Каждая одноименная функция иерархии наследования должна быть определена с помощью:

  • ключевое слово virtual, чтобы включить переопределение в будущих контрактах
  • ключевое слово override, чтобы указать, что функция переопределяется

Давайте перепишем и обновим наш предыдущий пример контракта до версии 0.6.0.

pragma solidity ^0.6.0;
contract Person {
    
    function greetings() public pure virtual returns (string memory) {
        return "Hello";
    }
    
}
contract Employee is Person {
    
    function greetings() public pure virtual override returns (string memory) {
        return "Hello. How can I help you? :)";
    }
    
}

Функция greetings внутри нашего контракта Person содержит ключевое слово virtual. Это означает, что его можно переопределить.

Функция greetings внутри нашего контракта Employee содержит ключевые слова virtual и override. Это означает, что 1) он переопределяет функцию greetings() и 2) он может быть переопределен снова в будущих контрактах, которые его наследуют.

Единственное «бремя» заключается в том, что вы должны указать все переопределенные контракты в круглых скобках, как показано ниже:

contract Teacher is Person, Employee {
    
    function greetings() public pure override(Employee, Person) returns (string memory) {
        return "Hello students ! Welcome to 'All About Solidity' series";
    }
    
}

Что делать, если указано только одно ключевое слово?

  • только virtual: будущие производные контракты могут изменить поведение этой функции.
  • only override : вы переопределяете функцию из базового контракта, но вы не сможете переопределить ее в будущих производных контрактах.

Для множественного наследования наиболее производные базовые контракты, определяющие одну и ту же функцию, должны быть явно указаны после ключевого слова override.

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

Кроме того, если контракт наследует одну и ту же функцию от нескольких (несвязанных) баз, он должен явно переопределить ее:

Примечание: функции с видимостью private не могут быть virtual.

NB2: функции без реализации должны быть помечены virtual за пределами интерфейсов.

Переопределение внешних функций с помощью общедоступных переменных

Переменные открытого состояния могут переопределять функции, только если:

  • функция отмечена видимостью external.
  • возвращаемый тип функции соответствует типу переменной, объявленной как public.
pragma solidity >=0.5.0 <0.7.0;
contract A
{
    function f() external pure virtual returns(uint) { return 5; }
}
contract B is A
{
    uint public override f;
}

Единственное ограничение состоит в том, что переменные public (в нашем примере f) могут переопределять, но нельзя переопределять.

Другими словами, такие переменные не могут быть помечены как virtual.

Deactivate and selfdestruct

Функция selfdestruct в Solidity уничтожает текущий контракт, отправляя все содержащиеся в нем эфиры в address, переданный в качестве параметра функции.

selfdestruct(address payable recipient)

До версии 0.5.0 существовала функция suicide с той же семантикой, что и selfdestruct.

Смертельныйконтракт из документации Solidity представляет собой хороший пример функции selfdestruct.

pragma solidity ^0.5.0;

contract owned {
    address payable owner;
    constructor() public { 
        owner = msg.sender; 
    }
    
}

contract mortal is owned {
    function kill() public {
        if (msg.sender == owner) selfdestruct(owner);
    }
}

Удаляет ли самоуничтожение код из блокчейна?

Согласно документации Solidity, selfdestruct — это единственный способ удалить контракт из блокчейна.

Рассмотрим сценарий, в котором вызов selfdestruct выполняется по контракту A в блоке 1 000 000. Контракт A больше нельзя будет вызывать и использовать в будущих блоках, следующих за блоком 1 000 000, поскольку код контракта и хранилище удалены из состояния.

Однако в документации Solidity упоминается важный момент:

Даже если контракт удаляется selfdestruct, он все равно остается частью истории блокчейна и, вероятно, сохраняется большинством узлов Ethereum. Таким образом, использование «самоуничтожения» — это не то же самое, что удаление данных с жесткого диска.

Преимущества использования самоуничтожения

Функция selfdestruct в Solidity соответствует коду операции SELFDESTRUCT (0xFF) в виртуальной машине Ethereum.

Эта функция считается полезной, когда вы заканчиваете контракт, потому что это стоит гораздо меньше газа, чем просто отправка баланса с помощью address.transfer(this.balance).

Фактически, код операции SELFDESTRUCT использует отрицательный газ, потому что операция освобождает место в блокчейне, очищая все данные контракта.

Этот отрицательный газ вычитается из общей стоимости газа транзакции, поэтому, если вы сначала выполняете некоторые операции по очистке, SELFDESTRUCT может снизить ваши расходы на газ.

См. Обмен стеками Ethereum: https://ethereum.stackexchange.com/a/347

Соображения безопасности относительно самоуничтожения

При использовании функции selfdestruct в Solidity возникают две основные проблемы.

Проблема безопасности 1:

Удаление контракта с помощью selfdestruct может показаться хорошей идеей в теории, но это потенциально опасно. Кто-то (= любой другой контракт или адрес) все еще может отправить эфир на удаленный контракт. В этом случае Эфиры навсегда потеряны.

Проблема безопасности 2:

Если код контракта не содержит вызова selfdestruct, он все равно может выполнить эту операцию, используя delegatecall или callcode.

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