Привет, незнакомец!

Похоже, вы здесь новенький. Чтобы принять участие, нажмите одну из кнопок ниже!

Смарт-контракт на Solidity: Часть 5 — токен ERC20

отредактировано November 2017 Раздел: Смарт Контракты

Серия уроков были взяты с сайте inaword.ru

В этом уроке мы разберем стандарт ERC20 и напишем собственную монету.

В мире Ethereum существует стандарт токена или монеты. Если ваш контракт отвечает стандарту, то его можно назвать монетой (валютой или токеном — кому как удобно). И в этом случае кошельки и биржи смогут работать с вашей монетой.

Стандарт монеты на базе эфира называется — ERC20. А требования его примерно следующие:

Чтобы смарт-контракт считался валютой он должен содержать:

  1. Методы

        function totalSupply() constant returns (uint256 totalSupply)
        function balanceOf(address _owner) constant returns (uint256 balance)
        function transfer(address _to, uint256 _value) returns (bool success)
        function transferFrom(address _from, address _to, uint256 _value) returns (bool success)
        function approve(address _spender, uint256 _value) returns (bool success)
        function allowance(address _owner, address _spender) constant returns (uint256 remaining)
    
  2. События

       event Transfer(address indexed _from, address indexed _to, uint256 _value)
       event Approval(address indexed _owner, address indexed _spender, uint256 _value)
    

На момент написания статьи ERC20 сложно было назвать стандартом. Это было скорее соглашение между разработчиками. Во-первых ERC расшифровывается как Ethereum Request for Comments. Т.е это обсуждаемое решение. Во-вторых в ERC20 еще не были включены функции symbol, decimals и name. И в-третьих было указано, что есть важные или обязательные требования:

Однако на сегодняшний день ERC20 уже официально является стандартом.

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

Когда ERC20 еще не был стандартом существовали негласные правила (на вики это называлось — «дополнительная информация») — наличие трех полей в внутри контракта. На сегодняшний в ERC20 перечисленные поля уже включены.

string public constant name = "Token Name";
string public constant symbol = "SYM";
uint8 public constant decimals = 18;

На текущий момент эти правила также входят в стандарт ERC223.

В идеале ваша монета должна выполнять все соглашения.

А теперь давайте рассмотрим эти требования подробней:

  1. Метод
    function totalSupply() constant returns (uint256 totalSupply)
    Возвращает суммарное количество выпущенных монет. Эту функцию может вызвать любой.

  2. Метод
    function balanceOf(address _owner) constant returns (uint256 balance)
    Возвращает количество монет принадлежащих _owner. Может вызвать любой. Кошельки для отображения вашей монеты вызывают именно эту функцию.

  3. Метод
    function transfer(address _to, uint256 _value) returns (bool success)
    Передает _value монет на адрес _to. Когда пользователь будет перемещать свои монеты на другой адрес вызываться будет именно эта функция. Соответственно монеты должны браться с баланса пользователя, который вызвал эту функцию. Метод должен создавать событие Transfer (описан будет далее) в случае успешного перемещения монет.

  4. Метод
    function transferFrom(address _from, address _to, uint256 _value) returns (bool success)
    Передает _ value монет от _from к _to. Пользователь должен иметь разрешение на перемещение монеток между адресами, дабы любой желающий не смог управлять чужими кошельками. Фактически эта функция позволяет вашему доверенному лицу распоряжаться определенным объемом монеток на вашем счету. Дать разрешение на управление средствами можно следующей функцией. Метод должен создавать событие Transfer (описан будет далее) в случае успешного перемещения монет.

  5. Метод
    function approve(address _spender, uint256 _value) returns (bool success)
    Разрешает пользователю _spender снимать с вашего счета (точнее со счета вызвавшего функцию пользователя) средства не более чем _value. На основе этого разрешения должна работать функция transferFrom. Метод должен создавать событие Approval (описан будет далее).

  6. Метод
    function allowance(address _owner, address _spender) constant returns (uint256 remaining)
    Возвращает сколько монет со своего счета разрешил снимать пользователь _owner пользователю _spender.

  7. Событие
    event Transfer(address indexed _from, address indexed _to, uint256 _value)
    Событие, которое должно возникать при любом перемещении монет. Т.е. его нужно создавать внутри функций transfer и transferFrom в случае успешного перемещения монет.

  8. Событие
    event Approval(address indexed _owner, address indexed _spender, uint256 _value)
    Событие должно возникать при получении разрешения на снятие монет. Фактически должно создаваться внутри функции approve.

  9. Поле
    string public constant name;
    Хранит полное название вашей монеты

  10. Поле
    string public constant symbol;
    Хранит короткое название вашей монеты. Иначе говоря — символ. С этим символом ваша валюта будет отображаться на биржах и в кошельках.

  11. Поле
    uint8 public constant decimals = 18;
    Количество знаков после запятой. 18 — это наиболее распространенное значение.

Как видите требований не много и они простые. Давайте напишем нашу монету. Называться она будет «Simple coin token», символ «SCT» и иметь 18 знаков после запятой. Пока реализуем основные поля и функцию transfer .

pragma solidity ^0.4.13;

contract SimpleTokenCoin {

    string public constant name = "Simple Coint Token";

    string public constant symbol = "SCT";

    uint32 public constant decimals = 18;

    uint public totalSupply = 0;

    mapping (address => uint) balances;

    function balanceOf(address _owner) constant returns (uint balance) {
        return balances[_owner];
    }

    function transfer(address _to, uint _value) returns (bool success) {
        balances[msg.sender] -= _value; 
        balances[_to] += _value;
        Transfer(msg.sender, _to, _value);
        return true;
    }

    function transferFrom(address _from, address _to, uint _value) returns (bool success) {
        return true; 
    }

    function approve(address _spender, uint _value) returns (bool success) {
        return false;
    }

    function allowance(address _owner, address _spender) constant returns (uint remaining) {
        return 0;
    }

    event Transfer(address indexed _from, address indexed _to, uint _value);

    event Approval(address indexed _owner, address indexed _spender, uint _value);

}

Балансы всех пользователей у нас хранятся в поле balances. В котором каждому адресу пользователя соответствует количество монет.

Функция transfer довольно простая. В своей первой строчке она просто вычитает из баланса пользователя msg.sender (тот кто вызвал функцию) _value монет. А во второй строчке прибавляет к балансу пользователя _to то же количество монет. Потом создается событие и возвращается true в знак того что перемещение монет успешно выполнено.

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

Вернемся к transfer. Сейчас если мы попытаемся взять монет больше чем у пользователя есть, то у него появится отрицательное количество монет на балансе. Нам нужно это предотвратить.

function transfer(address _to, uint _value) returns (bool success) {
        if(balances[msg.sender] >= _value) {
            balances[msg.sender] -= _value; 
            balances[_to] += _value;
            Transfer(msg.sender, _to, _value);
            return true;
        } 
        return false;
}

Стоит отметить что переменные могут хранить какое-то максимальное число. Если попытаться сохранить число больше то мы переменная переполнится и станет отрицательной. Именно поэтому добавляют еще проверку на переполнение. В нашем случае оно будет таким:

balances[_to] + _value >= balances[_to]

А полностью функция transfer будет выглядеть так

function transfer(address _to, uint _value) returns (bool success) {
        if(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]) {
            balances[msg.sender] -= _value; 
            balances[_to] += _value;
            Transfer(msg.sender, _to, _value);
            return true;
        } 
        return false;
}

Добавьте такие же проверки в transferFrom самостоятельно.

После всех наших махинаций transferFrom должна выглядеть примерно так:

function transferFrom(address _from, address _to, uint _value) returns (bool success) {
        if(balances[_from] >= _value && balances[_to] + _value >= balances[_to]) {
            balances[_from] -= _value; 
            balances[_to] += _value;
            Transfer(_from, _to, _value);
            return true;
        } 
        return false;
}

Давайте посмотрим как работает transfer и transferFrom. Для того чтобы мы смогли выпускать новые монеты на баланс любого пользователя — добавьте функцию mint (в переводе — чеканить):

function mint(address _to, uint _value) {
      assert(totalSupply + _value >= totalSupply && balances[_to] + _value >= balances[_to]);
      balances[_to] += _value;
      totalSupply += _value;
}

После этого залейте контракт в Remix.

Сейчас у нас ни у кого из пользователей нет наших монет. Проверим это вызвав balanceOf для произвольного адреса, например для 0xea15adb66dc92a4bbccc8bf32fd25e2e86a2a770.

Как видим баланс равен нулю. Теперь попробуем переместить с нашего баланса 100 монет на баланс другого пользователя с помощью transfer.

В некоторых случаях remix напрямую не показывает результат выполнения. Тогда его можно посмотреть в консоле. Для этого нужно нажать на кнопочку details напротив нашей операции.

И тогда в консоле появится табличка с подробностями выполнения операции. Найдите строку с «decoded output» в левом столбце и увидите в правом результат.

Как видим функция вернула false. Потому что монет на нашем счету сейчас нет. Значит наша проверка отработала. То что монеты не перемещались вы также можете убедится еще раз вызвав balanceOf для адреса на который пытались переместить.

Давайте выпустим 100 монет на наш баланс, для этого вызовем функцию mint с нашим адресом. Напоминаю ваш текущий адрес в тестовой среде Remix указан в поле Account. Чтобы его скопировать нужно нажать на кнопочку рядом с полем как на картинке.

Итак вызываем mint

А затем balanceOf на тот же адрес, чтобы проверить что монетки выпустились

Как видим на балансе у нас 100 монеток.

Теперь давайте переместим 40 монеток c нашего адреса на 0xea15adb66dc92a4bbccc8bf32fd25e2e86a2a770 с помощью transfer.

Посмотрим что вернул transfer в консоле.

transfer вернул нам true, значит успешно отработал. Теперь на аккаунте 0xea15adb66dc92a4bbccc8bf32fd25e2e86a2a770 должно быть 40 монет. Проверим с помощью balanceOf

Как мы и ожидали! А на балансе нашего аккаунта должно остаться 60, проверяем

Мы убедились что tranfer работает как надо. Протестируйте самостоятельно работу функции transferFrom.

Сейчас функция transferFrom может быть вызвана любым пользователем. А надо чтобы она отрабатывала только в том случае если у пользователя ее вызвавшего есть разрешение на снятие монет. Насколько мы помним разрешение может быть предоставлено посредством функции approve.

Давайте подумаем как хранить и представлять разрешения. На снятие монет с адреса может быть предоставлено сколько угодно разрешений. По одному для каждого пользователя. При этом каждое разрешение должно хранить сумму которую доверенное лицо может снять.

Тут лучше всего подходит двойной mapping. Первый ключ — адрес на снятие с которого предоставляется разрешение. Второй ключ — пользователь, которому предоставляется разрешение.

mapping (address => mapping (address => uint)) allowed;

Добавьте это поле в контракт.

Займемся функцией allowance. Сумма которая разрешена пользователю _spender для снятия со счета _owner

allowed[_owner][_spender]

Тогда наша функция allowance будет выглядеть так

function allowance(address _owner, address _spender) constant returns (uint remaining) {
        return allowed[_owner][_spender];
}

Теперь напишем функцию approve.

Чтобы пользователю _spender предоставить разрешение на снятие _value монет со счета выполняющего контракт нужно записать

allowed[msg.sender][_spender] = _value;

И не забудем добавить создания события Approval. И вот код approve

function approve(address _spender, uint _value) returns (bool success) {
        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);
        return true;
}

Осталось исправить transferFrom таким образом, чтобы была проверка на разрешенное количество монет.

allowed[_from][msg.sender] >= _value

И если снятие монет успешно, то уменьшаем разрешенное количество монет на величину снятия.

allowed[_from][msg.sender] -= _value;

Новая версия transferFrom теперь выглядит так:

function transferFrom(address _from, address _to, uint _value) returns (bool success) {
        if( allowed[_from][msg.sender] >= _value &&
            balances[_from] >= _value 
            && balances[_to] + _value >= balances[_to]) {
            allowed[_from][msg.sender] -= _value;
            balances[_from] -= _value; 
            balances[_to] += _value;
            Transfer(_from, _to, _value)
            return true;
        } 
        return false;
}

Теперь осталось только сделать так чтобы выпуск новых монет был разрешен только владельцу контракта. В четвертом уроке для этого мы написали контракт Ownablе который содержит необходимый модификатор onlyOwner унаследуем наш контракт монеты от Ownable и добавим к mint модификатор onlyOwner.

Теперь контракт нашей монетки выглядит так.

pragma solidity ^0.4.13;

contract Ownable {

    address owner;

    function Ownable() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    function transferOwnership(address newOwner) onlyOwner {
        owner = newOwner;
    }

}

contract SimpleTokenCoin is Ownable {

    string public constant name = "Simple Coint Token";

    string public constant symbol = "SCT";

    uint32 public constant decimals = 18;

    uint public totalSupply = 0;

    mapping (address => uint) balances;

    mapping (address => mapping(address => uint)) allowed;

    function mint(address _to, uint _value) onlyOwner {
        assert(totalSupply + _value >= totalSupply && balances[_to] + _value >= balances[_to]);
        balances[_to] += _value;
        totalSupply += _value;
    }

    function balanceOf(address _owner) constant returns (uint balance) {
        return balances[_owner];
    }

    function transfer(address _to, uint _value) returns (bool success) {
        if(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]) {
            balances[msg.sender] -= _value; 
            balances[_to] += _value;
            Transfer(msg.sender, _to, _value);
            return true;
        } 
        return false;
    }

    function transferFrom(address _from, address _to, uint _value) returns (bool success) {
        if( allowed[_from][msg.sender] >= _value &&
            balances[_from] >= _value 
            && balances[_to] + _value >= balances[_to]) {
            allowed[_from][msg.sender] -= _value;
            balances[_from] -= _value; 
            balances[_to] += _value;
            Transfer(_from, _to, _value);
            return true;
        } 
        return false;
    }

    function approve(address _spender, uint _value) returns (bool success) {
        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);
        return true;
    }

    function allowance(address _owner, address _spender) constant returns (uint remaining) {
        return allowed[_owner][_spender];
    }

    event Transfer(address indexed _from, address indexed _to, uint _value);

    event Approval(address indexed _owner, address indexed _spender, uint _value);

}

Если сравните требования ERC20 с нашим контрактом то заметите, что нет функции totalSupply. Дело в том, что для публичных полей функции возврата, так называемые геттеры, создаются автоматически, когда уже контракт будет залит в блокчейн.

Попробуйте поиграться и проверить как работают функции предоставления разрешения на снятие средств.

В этом уроке мы написали контракт нашей первой монетки. И он полностью совместим с ERC20! На Эфире уже написано большое количество монет и сложились некоторые общие шаблоны. В следующем уроке мы рассмотрим один из распространенных способов написания монеты ERC20. И изменим наш контракт в соответствии с общепринятой практикой.

Продолжение читать тут.

Войдите или Зарегистрируйтесь чтобы комментировать.