Давайте изучим некоторые продвинутые концепции Solidity!


Расширенные темы Solidity

На первом курсе мы рассмотрели базовый синтаксис Solidity. Мы рассмотрели переменные, типы данных, функции, циклы, условные потоки и массивы.

Однако в Solidity есть еще несколько вещей, которые будут важны при выполнении заданий по кодированию на втором курсе и далее. В этом уроке мы рассмотрим некоторые более важные темы Solidity.

Предпочитаете видео?

Если вы предпочитаете учиться на видео, мы разместили запись этого урока на нашем YouTube. Она разделена на 2 части и имеет временные метки. Посмотрите видео, нажав на скриншот ниже, или переходите к чтению учебника!

Часть 1

[![Часть

1](https://raw.githubusercontent.com/LearnWeb3DAO/Advanced-Solidity-Topics/main/assets/video-preview1.webp)](https://www.youtube.com/watch?v=Z5P3rKBRmEM)

Часть 2

[![Часть

2](https://raw.githubusercontent.com/LearnWeb3DAO/Advanced-Solidity-Topics/main/assets/video-preview2.webp)](https://www.youtube.com/watch?v=ILY3fIbwjk0)

Индекс

  • Отображения
  • Перечисления
  • Структуры
  • Представление и чистые функции
  • Модификаторы функций
  • События
  • Конструкторы
  • Наследование
  • Передача ETH
  • Вызов внешних контрактов
  • Операторы импорта
  • Библиотеки Solidity

Карты

Сопоставления в Solidity действуют подобно хэшмапам или словарям в других языках программирования. Они используются для хранения данных в парах ключ-значение.

Сопоставления создаются с помощью синтаксиса mapping (keyType => valueType).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Mapping {
    // Mapping from address to uint
    mapping(address => uint) public myMap;

    function get(address _addr) public view returns (uint) {
        // Mapping always returns a value.
        // If the value was never set, it will return the default value.
        // The default value for uint is 0
        return myMap[_addr];
    }

    function set(address _addr, uint _i) public {
        // Update the value at this address
        myMap[_addr] = _i;
    }

    function remove(address _addr) public {
        // Reset the value to the default value.
        delete myMap[_addr];
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы также можем создавать вложенные отображения, где key указывает на второе вложенное отображение. Для этого мы устанавливаем valueType в само отображение.

contract NestedMappings {
    // Mapping from address => (mapping from uint to bool)
    mapping(address => mapping(uint => bool)) public nestedMap;

    function get(address _addr1, uint _i) public view returns (bool) {
        // You can get values from a nested mapping
        // even when it is not initialized
        // The default value for a bool type is false
        return nestedMap[_addr1][_i];
    }

    function set(
        address _addr1,
        uint _i,
        bool _boo
    ) public {
        nestedMap[_addr1][_i] = _boo;
    }

    function remove(address _addr1, uint _i) public {
        delete nestedMap[_addr1][_i];
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Слово Enum означает Enumerable. Это определяемые пользователем типы, которые содержат человекочитаемые имена для набора констант, называемых членами. Они обычно используются для ограничения переменной только одним из нескольких предопределенных значений. Поскольку они являются лишь абстракцией для читаемых человеком констант, на самом деле они внутренне представлены как uints.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Enum {
    // Enum representing different possible shipping states
    enum Status {
        Pending,
        Shipped,
        Accepted,
        Rejected,
        Canceled
    }

    // Declare a variable of the type Status
    // This can only contain one of the predefined values
    Status public status;

    // Since enums are internally represented by uints
    // This function will always return a uint
    // Pending = 0
    // Shipped = 1
    // Accepted = 2
    // Rejected = 3
    // Canceled = 4
    // Value higher than 4 cannot be returned
    function get() public view returns (Status) {
        return status;
    }

    // Pass a uint for input to update the value
    function set(Status _status) public {
        status = _status;
    }

    // Update value to a specific enum members
    function cancel() public {
        status = Status.Canceled; // Will set status = 4
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Структуры

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract TodoList {
    // Declare a struct which groups together two data types
    struct TodoItem {
        string text;
        bool completed;
    }

    // Create an array of TodoItem structs
    TodoItem[] public todos;

    function createTodo(string memory _text) public {
        // There are multiple ways to initialize structs

        // Method 1 - Call it like a function
        todos.push(TodoItem(_text, false));

        // Method 2 - Explicitly set its keys
        todos.push(TodoItem({ text: _text, completed: false }));

        // Method 3 - Initialize an empty struct, then set individual properties
        TodoItem memory todo;
        todo.text = _text;
        todo.completed = false;
        todos.push(todo);
    }

    // Update a struct value
    function update(uint _index, string memory _text) public {
        todos[_index].text = _text;
    }

    // Update completed
    function toggleCompleted(uint _index) public {
        todos[_index].completed = !todos[_index].completed;
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Представление и чистые функции

Вы могли заметить, что некоторые функции, которые мы писали, указывают в заголовке функции одно из ключевых слов view или pure. Это специальные ключевые слова, которые указывают на специфическое поведение функции.

Геттерные функции (те, которые возвращают значения) могут быть объявлены либо view, либо pure.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ViewAndPure {
    // Declare a state variable
    uint public x = 1;

    // Promise not to modify the state (but can read state)
    function addToX(uint y) public view returns (uint) {
        return x + y;
    }

    // Promise not to modify or read from state
    function add(uint i, uint j) public pure returns (uint) {
        return i + j;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Modifiers {
    address public owner;

    constructor() {
        // Set the contract deployer as the owner of the contract
        owner = msg.sender;
    }

    // Create a modifier that only allows a function to be called by the owner
    modifier onlyOwner() {
        require(msg.sender == owner, "You are not the owner");

        // Underscore is a special character used inside modifiers
        // Which tells Solidity to execute the function the modifier is used on
        // at this point
        // Therefore, this modifier will first perform the above check
        // Then run the rest of the code
        _;
    }

    // Create a function and apply the onlyOwner modifier on it
    function changeOwner(address _newOwner) public onlyOwner {
        // We will only reach this point if the modifier succeeds with its checks
        // So the caller of this transaction must be the current owner
        owner = _newOwner;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

События

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Events {
    // Declare an event which logs an address and a string
    event TestCalled(address sender, string message);

    function test() public {
        // Log an event 
        emit TestCalled(msg.sender, "Someone called test()!");
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Конструкторы

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

P.S. — Если вы помните, мы действительно использовали конструкторы в учебниках по криптовалютам и NFT для первокурсников!

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract X {
    string public name;

    // You will need to provide a string argument when deploying the contract
    constructor(string memory _name) {
        // This will be set immediately when the contract is deployed
        name = _name;
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наследование

Наследование — это процедура, с помощью которой один контракт может наследовать атрибуты и методы другого контракта. Solidity поддерживает множественное наследование. Контракты могут наследовать другие контракты с помощью ключевого слова is.

Примечание: На самом деле мы также использовали наследование в учебниках по криптовалютам для первокурсников и NFT — там мы наследовали от контрактов ERC20 и ERC721 соответственно.

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

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

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

/* Graph of inheritance
    A
   / 
  B   C
 /   /
D   E

*/

contract A {
    // Declare a virtual function foo() which can be overridden by children
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    // Override A.foo();
    // But also allow this function to be overridden by further children
    // So we specify both keywords - virtual and override
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is A {
    // Similar to contract B above
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}

// When inheriting from multiple contracts, if a function is defined multiple times, the right-most parent contract's function is used.
contract D is B, C {
    // D.foo() returns "C"
    // since C is the right-most parent with function foo();
    // override (B,C) means we want to override a method that exists in two parents
    function foo() public pure override (B, C) returns (string memory) {
        // super is a special keyword that is used to call functions
        // in the parent contract
        return super.foo();
    }
}

contract E is C, B {
    // E.foo() returns "B"
    // since B is the right-most parent with function foo();
    function foo() public pure override (C, B) returns (string memory) {
        return super.foo();
    }
}

Вход в полноэкранный режим Выход из полноэкранного режима

Передача ETH

Существует три способа перевода ETH с контракта на другой адрес. Однако два из них больше не рекомендуются Solidity в последних версиях, поэтому мы их пропустим.

В настоящее время рекомендуемым способом перевода ETH с контракта является использование функции call. Функция call возвращает bool, указывая на успех или неудачу перевода.

Как получить Ether на адрес обычного счета Ethereum

Если вы переводите ETH на обычный счет (например, на адрес Metamask), вам не нужно делать ничего особенного, поскольку все такие счета могут автоматически принимать переводы ETH.

Как получить Ether в контракте

Однако, если вы пишете контракт, который вы хотите иметь возможность получать переводы ETH напрямую, вы должны иметь хотя бы одну из следующих функций

  • receive() external payable
  • fallback() external payable

receive() вызывается, если msg.data — пустое значение, а fallback() используется в противном случае.

msg.data — это способ указать произвольные данные вместе с транзакцией. Обычно вы не будете использовать его вручную.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ReceiveEther {
    /*
    Which function is called, fallback() or receive()?

           send Ether
               |
         msg.data is empty?
              / 
            yes  no
            /     
receive() exists?  fallback()
         /   
        yes   no
        /      
    receive()   fallback()
    */

    // Function to receive Ether. msg.data must be empty
    receive() external payable {}

    // Fallback function is called when msg.data is not empty
    fallback() external payable {}

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

contract SendEther {
    function sendEth(address payable _to) public payable {
        // Just forward the ETH received in this payable function
        // to the given address
        uint amountToSend = msg.value;
        // call returns a bool value specifying success or failure
        (bool success, bytes memory data) = _to.call{value: msg.value}("");
        require(success == true, "Failed to send ETH");
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вызов внешних контрактов

Контракты могут вызывать другие контракты, просто вызывая функции на экземпляре другого контракта, например A.foo(x, y, z). Для этого у вас должен быть интерфейс для A, который сообщает вашему контракту, какие функции существуют. Интерфейсы в Solidity ведут себя как заголовочные файлы и служат для тех же целей, что и ABI, который мы использовали при вызове контрактов из внешнего интерфейса. Это позволяет контракту знать, как кодировать и декодировать аргументы функций и возвращаемые значения для вызова внешних контрактов.

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

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

interface MinimalERC20 {
    // Just include the functions we are interested in
    // in the interface
    function balanceOf(address account) external view returns (uint256);
}

contract MyContract {
    MinimalERC20 externalContract;

    constructor(address _externalContract) {
        // Initialize a MinimalERC20 contract instance
        externalContract = MinimalERC20(_externalContract);
    }

    function mustHaveSomeBalance() public {
        // Require that the caller of this transaction has a non-zero
        // balance of tokens in the external ERC20 contract
        uint balance = externalContract.balanceOf(msg.sender);
        require(balance > 0, "You don't own any tokens of external contract");
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Импортные утверждения

Чтобы сохранить читабельность кода, вы можете разделить код Solidity на несколько файлов. Solidity позволяет импортировать как локальные, так и внешние файлы.

Локальный импорт

Предположим, что у нас есть такая структура папок:

├── Import.sol
└── Foo.sol
Вход в полноэкранный режим Выход из полноэкранного режима

где Foo.sol — это

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Foo {
    string public name = "Foo";
}
Войти в полноэкранный режим Выход из полноэкранного режима

Мы можем импортировать Foo и использовать его в Import.sol следующим образом

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// import Foo.sol from current directory
import "./Foo.sol";

contract Import {
    // Initialize Foo.sol
    Foo public foo = new Foo();

    // Test Foo.sol by getting it's name.
    function getFooName() public view returns (string memory) {
        return foo.name();
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

ПРИМЕЧАНИЕ: Когда мы используем Hardhat, мы также можем установить контракты как модули node через npm, а затем импортировать контракты из папки node_modules. Это также считается локальным импортом, так как технически, когда вы устанавливаете пакет, вы загружаете контракты на свою локальную машину.

Внешний импорт

Вы также можете импортировать из Github, просто скопировав URL. Мы делали это в учебниках по криптовалютам и NFT в курсе для новичков.

// https://github.com/owner/repo/blob/branch/path/to/Contract.sol
import "https://github.com/owner/repo/blob/branch/path/to/Contract.sol";

// Example import ERC20.sol from openzeppelin-contract repo
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";

Вход в полноэкранный режим Выход из полноэкранного режима

Библиотеки Solidity

Библиотеки похожи на контракты в Solidity, с некоторыми ограничениями. Библиотеки не могут содержать переменные состояния и не могут передавать ETH.

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

library SafeMath {
    function add(uint x, uint y) internal pure returns (uint) {
        uint z = x + y;
        // If z overflowed, throw an error
        require(z >= x, "uint overflow");
        return z;
    }
}

contract TestSafeMath {
    function testAdd(uint x, uint y) public pure returns (uint) {
        return SafeMath.add(x, y);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вот и все, друзья! Поздравляю, что вы дошли до этого 😀


Эта статья подготовлена LearnWeb3 DAO. Бесплатная, всеобъемлющая программа обучения по блокчейну от А до Я для разработчиков по всему миру.

Все от «Что такое блокчейн» до «Взлома смарт-контрактов» — и все, что между ними, а также многое другое!
Присоединяйтесь к нам сейчас, чтобы начать строить вместе с 25 000+ разработчиками.

Сайт
Discord
Twitter

Оцените статью
Procodings.ru
Добавить комментарий