Качественные ресурсы для разработки криптографии трудно найти и еще сложнее понять. Более того, некоторые руководства показывают вам, как писать смарт-контракты, в то время как другие показывают, как создавать интерфейсные приложения dApp для взаимодействия со смарт-контрактами, но лишь немногие синтезируют эти части в связное и удобное приложение. Чтобы восполнить этот пробел в учебном материале, в этой статье мы создадим приложение с полным стеком, которое работает «из коробки».

Написание читаемого кода - это основа философии дизайна в лабораториях Carbon-12. В идеальном мире конечные пользователи наших продуктов никогда не будут взаимодействовать с битовой техникой, которая находится под капотом. Подобно бесшумным двигателям, которые мурлыкают в самых плавных автомобилях, наш код беззвучно приводит в действие наши продукты. Наша цель состоит в том, чтобы если бы кто-то действительно взглянул на нашу базу кода, то чтение нашего кода было бы таким же приятным, как чтение романа Мураками (вставьте вилку IQ84).

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

1Q84

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

Учимся писать смарт-контракты: старая философия, новый контекст

Навыки, требуемые от разработчика смарт-контрактов, на самом деле ничем не отличаются от навыков типичного разработчика программного обеспечения, за исключением одного улова: постоянства. Как только смарт-контракт внедряется в производство, он существует… вечно… что означает, что любой технический долг сохраняется. Вот почему для нас так важна концепция «возможности обновления».

Независимо от того, какой тип смарт-контракта мы создаем, будь то токен, биржа или Oracle, мы должны иметь в основе какой-то механизм для возможности обновления. Разработка с учетом возможности обновления с самого начала - это серьезный отход от традиционного программирования; современные разработчики программного обеспечения могут постоянно отправлять код на сервер, на котором работает веб-приложение, но после развертывания смарт-контракта его технически невозможно изменить.

(Отложено) Прокси-серверы

Было проделано много работы над обновляемыми смарт-контрактами «прокси» - прокси - это, по сути, бухгалтерские контракты, которые отслеживают различные реализации логики. Например, предположим, что смарт-контракт Foo_version_0 изначально развернут с единственной функцией «bar ()», которая печатает «Ооо, посмотри на меня!». Прокси-контракт Foo изначально указывает на Foo_version_0, так что если бы пользователь вызвал Foo.bar (), тогда прокси Foo () извлек бы логику bar () из Foo_version_0 и напечатал бы «Ооо, посмотри на меня!». Теперь представьте, что команда разработчиков Foo-Lab хочет обновить логику Foo до Foo_version_1. Некоторые пользователи сообщают, что чувствуют себя некомфортно из-за исходного сообщения Foo_version_0.bar (), поэтому функция bar () версии 1 теперь печатает «Hello, world!». Контракт прокси Foo теперь может обновить свою логику до версии 1, так что любой пользовательский вызов Foo.bar () теперь извлекает логику bar () из Foo_version_1 и печатает «Hello, world!».

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

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

Как видите, программирование смарт-контрактов на самом деле не сильно отличается от обычного программирования. Основные передовые практики модульности и разделения ответственности по-прежнему остаются в силе! Вначале мы обратились к этим принципам и отделили наши логические контракты от наших контрактов на хранение. Наша логика может быть обновлена ​​перед лицом неприятных ошибок, не меняя состояние хранилища и не жертвуя доверием наших пользователей. Таким образом, прокси будет выполнять вызовы функций в соответствии с тем, как это реализуется последней логикой, и, что особенно важно, эти вызовы изменят хранилище прокси. Просто помните, что прокси выполняет вызовы в контексте контракта прокси.

В этой статье мы представляем уникальный способ обновления смарт-контрактов с помощью блокировки с задержкой по времени, при которой фактическое обновление не произойдет до тех пор, пока не пройдет определенное время. Думайте об этом как о шаблоне фиксация-толчок, которым знаменит Git.

Итак, ВЫ хотите создать обновляемый токен ERC-20, соответствующий требованиям завтрашнего дня? (К интересным вещам)

Carbon-12 Labs считает, что стейблкоины сформируют подвижный базовый слой будущей цифровой экономики. Давайте теперь погрузимся в код: в основе каждой стабильной монеты лежит действительно надежный токен. Итак, тема этого первого блога - аккуратно направить читателей в создание обновляемого токена ERC20. В конце этого руководства вы сможете развернуть базовый токен ERC20 с простым интерфейсом dApp, добавленным для развлечения.

Дорожная карта

  1. Реализация токена V0: это будет стандартный ERC20, способный чеканить новые токены, сжигать токены и, конечно, передавать
  2. Реализация токена V1: мы добавим несколько функций безопасности в V0, такие как возможность приостановки и защита от атак с двойным расходом.
  3. Прокси-сервер токена: с точки зрения пользователя, он не будет знать, что он на самом деле взаимодействует и использует «прокси-контракт» в отличие от одного из контрактов токена V0 / V1, но прокси является ключом к обеспечению гибкости возможности обновления.
  4. Развертывание наших смарт-контрактов в тестовой сети: эта часть может быть сложной, поэтому мы будем использовать Infura для удобства, и я покажу вам, как писать полезные сценарии развертывания.
  5. dApp time: мы будем использовать React, Redux и Material-UI, чтобы создать простой и элегантный кошелек для взаимодействия с нашими смарт-контрактами!

Готовый? Давайте начнем.

0. Хранилище токенов

Чтобы действительно понять суть, во имя модульности и приверженности принципам единого истинного Бога - чистого кода - я отделю логику смарт-контрактов от хранилища. Это окажется более полезным, когда мы начнем строить контракт прокси, который должен использовать тот же тип хранилища, что и контракты его реализации. Этот контракт на хранение, по общему признанию, очень простой, но он вдохновлен этим отличным постом о Вечном хранилище. Суть статьи в том, что копирование данных в цепочке стоит дорого, поэтому изоляция хранилища данных от логики поддерживает будущие обновления.

Все токены и прокси токенов будут унаследованы от класса TokenStorage:

pragma solidity ^0.4.24;
import "./AllowanceSheet.sol";
import "./BalanceSheet.sol";
/**
* @title TokenStorage
*/
contract TokenStorage {
    /**
        Storage
    */
    BalanceSheet public balances;
    AllowanceSheet public allowances;
/**
    * @dev a TokenStorage consumer can set its storages only once, on construction
    *
    **/
    constructor (address _balances, address _allowances) public {
        balances = BalanceSheet(_balances);
        allowances = AllowanceSheet(_allowances);
    }
/**
    * @dev claim ownership of balance sheet passed into constructor.
    **/
    function claimBalanceOwnership() public {
        balances.claimOwnership();
    }
/**
    * @dev claim ownership of allowance sheet passed into constructor.
    **/
    function claimAllowanceOwnership() public {
        allowances.claimOwnership();
    }
}

(TokenStorage.sol)

Любой токен, наследуемый от TokenStorage, будет хранить ссылки на BalanceSheet и AllowanceSheet, код которых следует. Обратите внимание, что хотя BalanceSheet и AllowanceSheet изолированы от логики Token, их адреса не могут быть изменены после построения. Это дизайнерское решение и имеет важные компромиссы: изменяемые адреса хранилища предотвращают сценарии аварий или взломов, которые нарушают контракты на хранилище, но они также заставляют конечных пользователей доверять владельцам контрактов на хранилище. В этом примере мы решили сделать хранилище недоступным для обновления, но с возможностью обновления логики.

pragma solidity ^0.4.24;
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import '../../helpers/Ownable.sol';
/**
* @title BalanceSheet
* @notice A wrapper around the balanceOf mapping. 
*/
contract BalanceSheet is Ownable {
    using SafeMath for uint256;
mapping (address => uint256) public balanceOf;
    uint256 public totalSupply;
function addBalance(address _addr, uint256 _value) public onlyOwner {
        balanceOf[_addr] = balanceOf[_addr].add(_value);
    }
function subBalance(address _addr, uint256 _value) public onlyOwner {
        balanceOf[_addr] = balanceOf[_addr].sub(_value);
    }
function setBalance(address _addr, uint256 _value) public onlyOwner {
        balanceOf[_addr] = _value;
    }
function addTotalSupply(uint256 _value) public onlyOwner {
        totalSupply = totalSupply.add(_value);
    }
function subTotalSupply(uint256 _value) public onlyOwner {
        totalSupply = totalSupply.sub(_value);
    }
function setTotalSupply(uint256 _value) public onlyOwner {
        totalSupply = _value;
    }
}

(BalanceSheet.sol: оболочка для отображения баланса ERC20)

pragma solidity ^0.4.24;
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import '../../helpers/Ownable.sol';
/**
* @title AllowanceSheet
* @notice A wrapper around an allowance mapping. 
*/
contract AllowanceSheet is Ownable {
    using SafeMath for uint256;
mapping (address => mapping (address => uint256)) public allowanceOf;
function addAllowance(address _tokenHolder, address _spender, uint256 _value) public onlyOwner {
        allowanceOf[_tokenHolder][_spender] = allowanceOf[_tokenHolder][_spender].add(_value);
    }
function subAllowance(address _tokenHolder, address _spender, uint256 _value) public onlyOwner {
        allowanceOf[_tokenHolder][_spender] = allowanceOf[_tokenHolder][_spender].sub(_value);
    }
function setAllowance(address _tokenHolder, address _spender, uint256 _value) public onlyOwner {
        allowanceOf[_tokenHolder][_spender] = _value;
    }
}

(AllowanceSheet.sol: оболочка для сопоставления допуска ERC20)

  1. Token_V0

Token_V0 - это токен ERC20, который позволяет владельцу контракта чеканить новые токены, а любому владельцу токена - сжигать токены. Token_V0 наследует TokenStorage и поэтому хранит ссылки на свои классы хранения.

Сейчас хорошее время, чтобы упомянуть, почему мы решили реализовать двухэтапную передачу прав собственности. Многие смарт-контракты наследуют общий контракт Ownable от проекта OpenZeppelin для обеспечения прав пользователей. Этот договор предусматривает концепцию единственного собственника, который может в одностороннем порядке передавать право собственности на другой адрес. Однако, если владелец контракта ошибается при вводе адреса предполагаемого нового владельца, контракт может стать безвозвратно потерянным. Это аналогично хорошо известному сценарию «заблокированного эфира», в результате которого несколько миллионов долларов США (более 7000 эфиров) безвозвратно заблокированы по адресу 0x0.

pragma solidity ^0.4.24;
/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions". This adds two-phase
 * ownership control to OpenZeppelin's Ownable class. In this model, the original owner 
 * designates a new owner but does not actually transfer ownership. The new owner then accepts 
 * ownership and completes the transfer.
 */
contract Ownable {
  address public owner;
  address public pendingOwner;
event OwnershipTransferred(
    address indexed previousOwner,
    address indexed newOwner
  );
/**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  constructor() public {
    owner = msg.sender;
    pendingOwner = address(0);
  }
/**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }
/**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyPendingOwner() {
    require(msg.sender == pendingOwner);
    _;
  }
/**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param _newOwner The address to transfer ownership to.
   */
  function transferOwnership(address _newOwner) public onlyOwner {
    require(_newOwner != address(0));
    pendingOwner = _newOwner;
  }
/**
   * @dev Allows the pendingOwner address to finalize the transfer.
   */
  function claimOwnership() onlyPendingOwner public {
    emit OwnershipTransferred(owner, pendingOwner);
    owner = pendingOwner;
    pendingOwner = address(0);
  }
}

(Ownable.sol: двухэтапная передача права собственности)

В двухэтапной модели владения первоначальный владелец назначает нового «ожидающего» владельца, но еще не передает право собственности. Новый владелец должен принять право собственности, чтобы завершить передачу.

pragma solidity ^0.4.24;
import "./dataStorage/TokenStorage.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import "openzeppelin-solidity/contracts/AddressUtils.sol";
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import '../helpers/Ownable.sol';
/**
* @title Token_V0
* @notice A basic ERC20 token with modular data storage
*/
contract Token_V0 is ERC20, TokenStorage, Ownable {
    using SafeMath for uint256;
/** Events */
    event Mint(address indexed to, uint256 value);
    event Burn(address indexed burner, uint256 value);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
constructor (address _balances, address _allowances) public 
    TokenStorage(_balances, _allowances) {}
/** Modifiers **/
/** Functions **/
function mint(address _to, uint256 _amount) public onlyOwner {
        return _mint(_to, _amount);
    }
function burn(uint256 _amount) public {
        _burn(msg.sender, _amount);
    }
function approve(address _spender, uint256 _value) 
    public returns (bool) {
        allowances.setAllowance(msg.sender, _spender, _value);
        emit Approval(msg.sender, _spender, _value);
        return true;
    }
function transfer(address _to, uint256 _amount) public returns (bool) {
        require(_to != address(0),"to address cannot be 0x0");
        require(_amount <= balanceOf(msg.sender),"not enough balance to transfer");
balances.subBalance(msg.sender, _amount);
        balances.addBalance(_to, _amount);
        emit Transfer(msg.sender, _to, _amount);
        return true;
    }
function transferFrom(address _from, address _to, uint256 _amount) 
    public returns (bool) {
        require(_amount <= allowance(_from, msg.sender),"not enough allowance to transfer");
        require(_to != address(0),"to address cannot be 0x0");
        require(_amount <= balanceOf(_from),"not enough balance to transfer");
        
        allowances.subAllowance(_from, msg.sender, _amount);
        balances.addBalance(_to, _amount);
        balances.subBalance(_from, _amount);
        emit Transfer(_from, _to, _amount);
        return true;
    }
/**
    * @notice Implements balanceOf() as specified in the ERC20 standard.
    */
    function balanceOf(address who) public view returns (uint256) {
        return balances.balanceOf(who);
    }
/**
    * @notice Implements allowance() as specified in the ERC20 standard.
    */
    function allowance(address owner, address spender) public view returns (uint256) {
        return allowances.allowanceOf(owner, spender);
    }
/**
    * @notice Implements totalSupply() as specified in the ERC20 standard.
    */
    function totalSupply() public view returns (uint256) {
        return balances.totalSupply();
    }
/** Internal functions **/
function _burn(address _tokensOf, uint256 _amount) internal {
        require(_amount <= balanceOf(_tokensOf),"not enough balance to burn");
        // no need to require value <= totalSupply, since that would imply the
        // sender's balance is greater than the totalSupply, which *should* be an assertion failure
        balances.subBalance(_tokensOf, _amount);
        balances.subTotalSupply(_amount);
        emit Burn(_tokensOf, _amount);
        emit Transfer(_tokensOf, address(0), _amount);
    }
function _mint(address _to, uint256 _amount) internal {
        balances.addTotalSupply(_amount);
        balances.addBalance(_to, _amount);
        emit Mint(_to, _amount);
        emit Transfer(address(0), _to, _amount);
    }
}

(Token_V0.sol: сжигаемый, Mintable, стандартный токен ERC20)

2. Token_V1

Давайте сделаем обновленную версию Token_V0, которая включает пару обновлений безопасности. Мы добавляем защиту от атак с двойным расходом и включаем механизм «паузы» экстренного торможения.

Уязвимость атаки с двойным расходом ERC20: стандартный интерфейс ERC-20 имеет конструктивный недостаток: если какой-то пользователь Алиса хочет изменить разрешение, предоставленное другому пользователю Бобу, то Алиса проверяет, потратил ли уже Боб свое разрешение, прежде чем выполнить транзакцию, чтобы изменить разрешение Боба. разрешение. Однако Боб все еще может потратить исходную квоту до того, как транзакция, изменяющая квоту, будет добыта, что, таким образом, позволяет Бобу потратить как квоты до изменения, так и после изменения. Чтобы иметь высокую вероятность успешного расходования квоты до изменения после того, как жертва убедится, что она еще не израсходована, злоумышленник ждет, пока будет произведена транзакция по изменению квоты, а затем выдает транзакцию расходования с необычно большим объемом газа. цена, чтобы гарантировать, что транзакция расходов будет добыта до изменения резерва. Подробнее об этом недостатке можно прочитать здесь и здесь.

Чтобы упростить изменение разрешений, мы обновили их до Token_V1, который включает методы увеличенияApproval () и уменьшенияApproval (), которые добавляют или вычитают существующие разрешения, а не перезаписывают их. Это эффективно перемещает проверку того, потратил ли Боб свой лимит, на время добычи транзакции, что лишает Боба возможности удвоить расходы.

Поскольку в стандарте ERC20 требуется метод Approve (), Token_V1 по умолчанию запрещает пользователям вызывать этот небезопасный метод с использованием контракта Lockable. Любой метод с модификатором whenUnlocked по умолчанию отключен и может быть вызван только в том случае, если владелец контракта «разблокирует» заблокированные методы.

pragma solidity ^0.4.24;
import './Ownable.sol';
/**
* @title Lockable
* @dev Base contract which allows children to lock certain methods from being called by clients.
* Locked methods are deemed unsafe by default, but must be implemented in children functionality to adhere by
* some inherited standard, for example. 
*/
contract Lockable is Ownable {
// Events
 event Unlocked();
 event Locked();
// Fields
 bool public isMethodEnabled = false;
// Modifiers
 /**
 * @dev Modifier that disables functions by default unless they are explicitly enabled
 */
 modifier whenUnlocked() {
  require(isMethodEnabled);
  _;
 }
// Methods
 /**
 * @dev called by the owner to enable method
 */
 function unlock() onlyOwner public {
  isMethodEnabled = true;
  emit Unlocked();
 }
/**
 * @dev called by the owner to disable method, back to normal state
 */
 function lock() onlyOwner public {
  isMethodEnabled = false;
  emit Locked();
 }
}

(Lockable.sol: по умолчанию блокирует whenUnlocked методы, владелец может вызывать unlock ())

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

pragma solidity ^0.4.24;
import "./Ownable.sol";
/**
 * @title Pausable
 * @dev Base contract which allows children to implement an emergency stop mechanism. Identical to OpenZeppelin version
 * except that it uses local Ownable contract
 */
contract Pausable is Ownable {
  event Pause();
  event Unpause();
bool public paused = false;
/**
   * @dev Modifier to make a function callable only when the contract is not paused.
   */
  modifier whenNotPaused() {
    require(!paused);
    _;
  }
/**
   * @dev Modifier to make a function callable only when the contract is paused.
   */
  modifier whenPaused() {
    require(paused);
    _;
  }
/**
   * @dev called by the owner to pause, triggers stopped state
   */
  function pause() onlyOwner whenNotPaused public {
    paused = true;
    emit Pause();
  }
/**
   * @dev called by the owner to unpause, returns to normal state
   */
  function unpause() onlyOwner whenPaused public {
    paused = false;
    emit Unpause();
  }
}

(Pausable.sol: поддерживает приостановку значительных операций по изменению состояния, эффективно реализуя «экстренное торможение» контракта)

pragma solidity ^0.4.24;
import './Token_V0.sol';
import "../helpers/Lockable.sol";
import "../helpers/Pausable.sol";
/**
* @title Token_V1
* @notice Adds pausability and disables approve() to defend against double-spend attacks in addition
* to inherited Token_V0 behavior
*/
contract Token_V1 is Token_V0, Pausable, Lockable {
    using SafeMath for uint256;
/** Events */
constructor (address _balances, address _allowances) public 
    Token_V0(_balances, _allowances) {}
/** Modifiers **/
/** Functions **/
function mint(address _to, uint256 _amount) public whenNotPaused {
        super.mint(_to, _amount);
    }
function burn(uint256 _amount) public whenNotPaused {
        super.burn(_amount);
    }
/**
    * @notice Implements ERC-20 standard approve function. Locked or disabled by default to protect against
    * double spend attacks. To modify allowances, clients should call safer increase/decreaseApproval methods.
    * Upon construction, all calls to approve() will revert unless this contract owner explicitly unlocks approve()
    */
    function approve(address _spender, uint256 _value) 
    public whenNotPaused whenUnlocked returns (bool) {
        super.approve(_spender, _value);
    }
/**
     * @dev Increase the amount of tokens that an owner allowed to a spender.
     * @notice increaseApproval should be used instead of approve when the user's allowance
     * is greater than 0. Using increaseApproval protects against potential double-spend attacks
     * by moving the check of whether the user has spent their allowance to the time that the transaction 
     * is mined, removing the user's ability to double-spend
     * @param _spender The address which will spend the funds.
     * @param _addedValue The amount of tokens to increase the allowance by.
     */
    function increaseApproval(address _spender, uint256 _addedValue) 
    public whenNotPaused returns (bool) {
        increaseApprovalAllArgs(_spender, _addedValue, msg.sender);
        return true;
    }
/**
     * @dev Decrease the amount of tokens that an owner allowed to a spender.
     * @notice decreaseApproval should be used instead of approve when the user's allowance
     * is greater than 0. Using decreaseApproval protects against potential double-spend attacks
     * by moving the check of whether the user has spent their allowance to the time that the transaction 
     * is mined, removing the user's ability to double-spend
     * @param _spender The address which will spend the funds.
     * @param _subtractedValue The amount of tokens to decrease the allowance by.
     */
    function decreaseApproval(address _spender, uint256 _subtractedValue) 
    public whenNotPaused returns (bool) {
        decreaseApprovalAllArgs(_spender, _subtractedValue, msg.sender);
        return true;
    }
function transfer(address _to, uint256 _amount) public whenNotPaused returns (bool) {
        super.transfer(_to, _amount);
    }
/**
    * @notice Initiates a transfer operation between address `_from` and `_to`. Requires that the
    * message sender is an approved spender on the _from account.
    * @dev When implemented, it should use the transferFromConditionsRequired() modifier.
    * @param _to The address of the recipient. This address must not be blacklisted.
    * @param _from The address of the origin of funds. This address _could_ be blacklisted, because
    * a regulator may want to transfer tokens out of a blacklisted account, for example.
    * In order to do so, the regulator would have to add themselves as an approved spender
    * on the account via `addBlacklistAddressSpender()`, and would then be able to transfer tokens out of it.
    * @param _amount The number of tokens to transfer
    * @return `true` if successful 
    */
    function transferFrom(address _from, address _to, uint256 _amount) 
    public whenNotPaused returns (bool) {
        super.transferFrom(_from, _to, _amount);
    }
/** Internal functions **/
    
    function decreaseApprovalAllArgs(address _spender, uint256 _subtractedValue, address _tokenHolder) internal {
        uint256 oldValue = allowances.allowanceOf(_tokenHolder, _spender);
        if (_subtractedValue > oldValue) {
            allowances.setAllowance(_tokenHolder, _spender, 0);
        } else {
            allowances.subAllowance(_tokenHolder, _spender, _subtractedValue);
        }
        emit Approval(_tokenHolder, _spender, allowances.allowanceOf(_tokenHolder, _spender));
    }
function increaseApprovalAllArgs(address _spender, uint256 _addedValue, address _tokenHolder) internal {
        allowances.addAllowance(_tokenHolder, _spender, _addedValue);
        emit Approval(_tokenHolder, _spender, allowances.allowanceOf(_tokenHolder, _spender));
    }
}

(Token_V1.sol: добавляет возможность приостановки и защищает от уязвимости атаки с двойным расходом)

3. TokenProxy

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

Но недостатком является то, что вы не можете изменить исходный код смарт-контракта после его развертывания. Разработчики, работающие над централизованными приложениями (например, Facebook или Airbnb), привыкли часто обновлять их, чтобы исправлять ошибки или вводить новые функции. Это невозможно сделать на Ethereum с помощью традиционных шаблонов.

Шаблон прокси-архитектуры таков, что все вызовы сообщений проходят через контракт прокси, который перенаправляет их на последнюю развернутую логику контракта. Для обновления развертывается новая версия вашего контракта, и прокси обновляется для ссылки на новый адрес контракта.

Прокси-контракт использует код операции delegatecall для перенаправления вызовов функций в целевой контракт, который может быть обновлен. Поскольку delegatecall сохраняет состояние вызова функции, логика целевого контракта может быть обновлена, и состояние останется в прокси-контракте для использования обновленной логикой целевого контракта. Как и в случае с delegatecall, msg.sender останется таковым у вызывающего контракта прокси.

pragma solidity ^0.4.24;
import "./dataStorage/TokenStorage.sol";
import "zos-lib/contracts/upgradeability/UpgradeabilityProxy.sol";
import '../helpers/Ownable.sol';
/**
* @title TokenProxy
* @notice A proxy contract that serves the latest implementation of TokenProxy.
*/
contract TokenProxy is UpgradeabilityProxy, TokenStorage, Ownable {
    constructor(address _implementation, address _balances, address _allowances) 
    UpgradeabilityProxy(_implementation) 
    TokenStorage(_balances, _allowances) public {}
/**
    * @dev Upgrade the backing implementation of the proxy.
    * Only the admin can call this function.
    * @param newImplementation Address of the new implementation.
    */
    function upgradeTo(address newImplementation) public onlyOwner {
        _upgradeTo(newImplementation);
    }
/**
    * @return The address of the implementation.
    */
    function implementation() public view returns (address) {
        return _implementation();
    }
}

(TokenProxy.sol)

Сначала я разверну TokenProxy в блокчейне, используя Token_V0 в качестве его логики, что означает, что пользователи могут вызывать метод Approve () для нашего токена, и его нельзя будет приостановить. Но владелец контракта может перейти на Token_V1, чтобы добавить эти изменения в токен. Важно отметить, что конечный пользователь ВСЕГДА будет выполнять вызовы на адрес прокси-сервера токена, даже если основная логика изменится.

4. DelayedUpgradeabilityProxy

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

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

pragma solidity ^0.4.24;
import "zos-lib/contracts/upgradeability/UpgradeabilityProxy.sol";
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
/** 
 * @title DelayedUpgradeabilityProxy
 * @notice Implements an upgradeability proxy with the option of
 * introducing pending implementations. 
 */
contract DelayedUpgradeabilityProxy is UpgradeabilityProxy {
    using SafeMath for uint256;
address public pendingImplementation;
    bool public pendingImplementationIsSet;
    uint256 public pendingImplementationApplicationDate; // Date on which to switch all contract calls to the new implementation
    uint256 public UPGRADE_DELAY = 4 weeks;
event PendingImplementationChanged(address indexed oldPendingImplementation, address indexed newPendingImplementation);
constructor(address _implementation) public UpgradeabilityProxy(_implementation) {}
/**
    * @notice Sets the pending implementation address of the proxy.
    * This function is internal--uses of this proxy should wrap this function
    * with a public function in order to make it externally callable.
    * @param implementation Address of the new implementation.
    */
    function _setPendingUpgrade(address implementation) internal {
        address oldPendingImplementation = pendingImplementation;
        pendingImplementation = implementation;
        pendingImplementationIsSet = true;
        emit PendingImplementationChanged(oldPendingImplementation, implementation);
        pendingImplementationApplicationDate = block.timestamp.add(UPGRADE_DELAY);
    }
/**
    * @notice Overrides the _willFallback() function of Proxy, which enables some code to
    * be executed prior to the fallback function. In this case, the purpose of this code
    * is to automatically switch the implementation to the pending implementation if the 
    * wait period of UPGRADE_DELAY (28 days) has been satisfied.
    */
    function _willFallback() internal {
        if (pendingImplementationIsSet && block.timestamp > pendingImplementationApplicationDate) {
            _upgradeTo(pendingImplementation);
            pendingImplementationIsSet = false;
            super._willFallback();
        }
        else {
            super._willFallback();
        }
    }
}

(DelayedUpgradeabilityProxy.sol: общий отложенный обновляемый прокси произвольно обновляет контракт через 4 недели)

pragma solidity ^0.4.24;
import "./dataStorage/TokenStorage.sol";
import '../upgradeability/DelayedUpgradeabilityProxy.sol';
import '../helpers/Ownable.sol';
/**
* @title TokenProxyDelayed
* @notice A proxy contract that serves the latest implementation of TokenProxy. This proxy
* upgrades only after a set amount of time (denominated in blocks mined) 
*/
contract TokenProxyDelayed is DelayedUpgradeabilityProxy, TokenStorage, Ownable {
    constructor(address _implementation, address _balances, address _allowances) 
    DelayedUpgradeabilityProxy(_implementation) 
    TokenStorage(_balances, _allowances) public {}
/**
    * @dev Upgrade the backing implementation of the proxy.
    * Only the admin can call this function.
    * @param newImplementation Address of the new implementation.
    */
    function upgradeTo(address newImplementation) public onlyOwner {
        _setPendingUpgrade(newImplementation);
    }
/**
    * @return The address of the implementation.
    */
    function implementation() public view returns (address) {
        return _implementation();
    }
}

(TokenProxyDelayed.sol: контракт с обновляемым прокси-сервером токена, который использует механизм отложенного обновления)

5. Пора внедрять наши контракты!

Во-первых, мы заранее развертываем наши базовые классы структуры данных (BalanceSheet, AllowanceSheet), чтобы мы могли предоставить их нашим конструкторам верхнего уровня.

Во-вторых, мы развернем наши классы Token и TokenProxy. Поскольку мы предполагаем, что пользователи взаимодействуют только с нашими обновляемыми прокси-классами, мы передадим пустые контракты хранения данных в конструкторы токенов и передадим реальные контракты прокси-серверу. Это служит нашему намерению, чтобы контракты токенов были контрактами «реализации», которые существуют как модели логики, которой прокси-сервер должен делегировать функции.

Наконец, прежде чем мы сможем немного повеселиться с нашими недавно развернутыми контрактами, нам нужно выполнить несколько важных шагов по настройке. Помните, как наши контракты на хранилище изолированы от контрактов с прокси / токенами? Более того, обратите внимание, что BalanceSheet и AllowanceSheet являются собственными контрактами, тем самым защищая их от манипуляций со стороны любого обычного Джо. Следовательно, мы должны сделать прокси-контракт владельцем обоих контрактов на хранилище, чтобы он мог управлять своим собственным балансом и сопоставлениями разрешений! Это точка 5_setup.js, которая выполняет двухэтапную передачу владения хранилищами данных контракту прокси.

Я написал сценарии развертывания в migrations/, которые вы можете запустить для развертывания этих смарт-контрактов. Я развернул эти смарт-контракты на Ropsten с помощью Infura, что вы также можете сделать, запустив (вам нужно будет создать .env файл с вашей мнемоникой MetaMask и ключом Infura API!):

truffle migrate --network ropsten

6. Wallet dApp

Я создал простой интерфейс для взаимодействия пользователей со смарт-контрактами UpgradeableERC20. Этот dApp работает с изначально развернутым TokenProxy, который использует логику Token_V0, но его можно легко настроить для работы с обновленной логикой Token_V1. Он также настроен на работу, если контракты развернуты в Ropsten, а не в Mainnet.

В dApp есть три раздела: панель инструментов, профиль и администратор. Обычные пользователи могут использовать панель управления для передачи или записи токенов. Владелец контракта может использовать портал администратора для чеканки новых токенов; если не владелец попытается чеканить новые токены, их транзакция вернется.

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

Конечные очки

Репо смарт-контрактов здесь, включая юнит-тесты

Репо Wallet dApp здесь

Ознакомьтесь с последним продуктом Carbon - линейкой фиатных и стабильных монет на fiat.carbon.money

Недавно я перешел со своей роли трейдера корпоративных облигаций на работу над дизайном продукта в Carbon, где мы создаем стейблкоин, используя новый гибридный подход. Если вам понравился этот урок, вы хотите сотрудничать в будущем проекте или у вас есть вопросы, не стесняйтесь обращаться ко мне по адресу [email protected] или проверять открытые позиции на https://angel.co/carbonmoney / jobs »! Удачного кодирования!