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

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

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

отредактировано December 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. І змінимо наш контракт відповідно до загальноприйнятої практики.

Продовження читати тут.

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