OpenZeppelin ERC20源碼分析

ERC20:Ethereum Request for Comments 20,是一個基於以太坊代幣的接口標準(協議)。全部符合ERC-20標準的代幣都能當即兼容以太坊錢包,它能讓用戶和交易所,都能很是方便的管理多種代幣,轉帳、存儲、ICO等等。javascript

OpenZeppelin的Token中實現了ERC20的一個安全的合約代碼,本篇主要來分析一下源碼,瞭解一下ERC20的實現,因爲代碼之間的調用可能略複雜,直接每一個文件每一個文件的來看會有點繞,我直接畫了一個繼承和調用關係的思惟導圖,能夠幫助更容易地看源碼。java

OpenZeppeline-ERC20

ERC20Basic.sol

pragma solidity ^0.4.23;

contract ERC20Basic {
  function totalSupply() public view returns (uint256);
  function balanceOf(address who) public view returns (uint256);
  function transfer(address to, uint256 value) public returns (bool);
  event Transfer(address indexed from, address indexed to, uint256 value);
}

ERC20Basic合約主要定義了ERC20的基本接口,定義了必需要實現的方法:git

  • totalSupply 返回總共發行量
  • balanceOf 查詢指定address的餘額
  • transfer 發送指定數目的token到指定帳戶,同時發送後須要觸發Transfer事件

Transfer事件,任何token發送發生時,必須觸發該事件,即便是0額度。 當一個token合約建立時,應該觸發一個Transfer事件,token的發送方是0x0,也就是說憑空而來的token,簡稱空氣幣。github

ERC20.sol

pragma solidity ^0.4.23;

import "./ERC20Basic.sol";

contract ERC20 is ERC20Basic {
  function allowance(address owner, address spender)
    public view returns (uint256);

  function transferFrom(address from, address to, uint256 value)
    public returns (bool);

  function approve(address spender, uint256 value) public returns (bool);
  event Approval(
    address indexed owner,
    address indexed spender,
    uint256 value
  );
}

ERC20合約繼承了ERC20Basic,另外定義了approve相關的方法:安全

  • allowance 獲取指定用戶的批准額度,控制代幣的交易,如可交易帳號及資產, 控制Token的流通
  • transferFrom 從一個地址向另一個地址轉帳指定額度的token,這個方法能夠理解爲一個收款流程,容許合約來表明token持有者發送代幣。好比,合約能夠幫助你向另一我的發送token或者索要token。前提是token擁有者必需要經過某些機制對這個請求進行確認,好比經過MetaMask進行confirm。不然,執行將失敗。 跟transfer同樣,即便發送0代幣,也要觸發Transfer事件。
  • approve 批准額度,容許一個帳戶最多能從你的帳戶你取現指定額度。重複調用時,以最後一次的額度爲主。爲了防止攻擊,最開始這個額度必須設置爲0。

Approval事件,當approve被調用時,須要觸發該事件。app

DetailedERC20.sol

pragma solidity ^0.4.23;

import "./ERC20.sol";

contract DetailedERC20 is ERC20 {
  string public name;
  string public symbol;
  uint8 public decimals;

  constructor(string _name, string _symbol, uint8 _decimals) public {
    name = _name;
    symbol = _symbol;
    decimals = _decimals;
  }
}

DetailedERC20 主要定義了token的展現信息:函數

  • name token的名稱,好比"XXXToken"
  • symbol token的符號,好比"XXX"
  • decimals token精確的小數點位數,好比18

BasicToken.sol

pragma solidity ^0.4.23;

import "./ERC20Basic.sol";
import "../../math/SafeMath.sol";


/**
 * @title 實現ERC20基本合約的接口 
 * @dev 基本的StandardToken,不包含allowances.
 */
contract BasicToken is ERC20Basic {
  using SafeMath for uint256;

  mapping(address => uint256) balances;

  uint256 totalSupply_;

  /**
  * @dev 返回存在的token總數
  */
  function totalSupply() public view returns (uint256) {
    return totalSupply_;
  }

  /**
  * @dev 給特定的address轉token
  * @param _to 要轉帳到的address
  * @param _value 要轉帳的金額
  */
  function transfer(address _to, uint256 _value) public returns (bool) {
    //作相關的合法驗證
    require(_to != address(0));
    require(_value <= balances[msg.sender]);
    // msg.sender餘額中減去額度,_to餘額加上相應額度
    balances[msg.sender] = balances[msg.sender].sub(_value);
    balances[_to] = balances[_to].add(_value);
    //觸發Transfer事件
    emit Transfer(msg.sender, _to, _value);
    return true;
  }

  /**
  * @dev 獲取指定address的餘額
  * @param _owner 查詢餘額的address.
  * @return An uint256 representing the amount owned by the passed address.
  */
  function balanceOf(address _owner) public view returns (uint256) {
    return balances[_owner];
  }

}

經過SafeMath來作運算很重要,在咱們本身寫合約的時候也儘可能使用,能夠避免一些計算過程的溢出等安全問題。源碼分析

StandardToken.sol

pragma solidity ^0.4.23;

import "./BasicToken.sol";
import "./ERC20.sol";

/**
 * @title 標準 ERC20 token
 *
 * @dev 實現基礎的標準token
 * @dev https://github.com/ethereum/EIPs/issues/20
 * @dev Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
 */
contract StandardToken is ERC20, BasicToken {
  mapping (address => mapping (address => uint256)) internal allowed;

  /**
   * @dev 從一個地址向另一個地址轉token
   * @param _from 轉帳的from地址
   * @param _to address 轉帳的to地址
   * @param _value uint256 轉帳token數量
   */
  function transferFrom(
    address _from,
    address _to,
    uint256 _value
  )
    public
    returns (bool)
  {
    // 作合法性檢查
    require(_to != address(0));
    require(_value <= balances[_from]);
    require(_value <= allowed[_from][msg.sender]);
    //_from餘額減去相應的金額
    //_to餘額加上相應的金額
    //msg.sender能夠從帳戶_from中轉出的數量減小_value
    balances[_from] = balances[_from].sub(_value);
    balances[_to] = balances[_to].add(_value);
    allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
    // 觸發Transfer事件
    emit Transfer(_from, _to, _value);
    return true;
  }

  /**
   * @dev 批准傳遞的address以表明msg.sender花費指定數量的token
   *
   * Beware that changing an allowance with this method brings the risk that someone may use both the old
   * and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
   * race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
   * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
   * @param _spender 花費資金的地址
   * @param _value 能夠被花費的token數量
   */
  function approve(address _spender, uint256 _value) public returns (bool) {
    //記錄msg.sender容許_spender動用的token
    allowed[msg.sender][_spender] = _value;
    //觸發Approval事件
    emit Approval(msg.sender, _spender, _value);
    return true;
  }

  /**
   * @dev 函數檢查全部者容許的_spender花費的token數量
   * @param _owner address 資金全部者地址.
   * @param _spender address 花費資金的spender的地址.
   * @return A uint256 指定_spender仍可用token的數量。
   */
  function allowance(
    address _owner,
    address _spender
   )
    public
    view
    returns (uint256)
  {
    //容許_spender從_owner中轉出的token數
    return allowed[_owner][_spender];
  }

  /**
   * @dev 增長全部者容許_spender花費代幣的數量。
   *
   * allowed[_spender] == 0時approve應該被調用. 增長allowed值最好使用此函數避免2此調用(等待知道第一筆交易被挖出)
   * From MonolithDAO Token.sol
   * @param _spender 花費資金的地址
   * @param _addedValue 用於增長容許動用的token牌數量
   */
  function increaseApproval(
    address _spender,
    uint _addedValue
  )
    public
    returns (bool)
  {
    //在以前容許的數量上增長_addedValue
    allowed[msg.sender][_spender] = (
      allowed[msg.sender][_spender].add(_addedValue));
    //觸發Approval事件
    emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
    return true;
  }

  /**
   * @dev 減小全部者容許_spender花費代幣的數量
   *
   * allowed[_spender] == 0時approve應該被調用. 減小allowed值最好使用此函數避免2此調用(等待知道第一筆交易被挖出)
   * From MonolithDAO Token.sol
   * @param _spender  花費資金的地址
   * @param _subtractedValue 用於減小容許動用的token牌數量
   */
  function decreaseApproval(
    address _spender,
    uint _subtractedValue
  )
    public
    returns (bool)
  {
    uint oldValue = allowed[msg.sender][_spender];
    if (_subtractedValue > oldValue) {
    //減小的數量少於以前容許的數量,則清零
      allowed[msg.sender][_spender] = 0;
    } else {
    //減小對應的_subtractedValue數量
      allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
    }
    //觸發Approval事件
    emit Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
    return true;
  }

}

上面合約定義的 mapping allowed,它用來記錄某個地址容許另一個地址動用多少token。假設錢包地址爲B,有另一個合約其合約地址爲C,合約C會經過支付XXX Token來作一些事情,根據ERC20的定義,每一個地址只能操做屬於本身的Token,則合約C沒法直接使用B地址所擁有的Token,這時候allowed Mapping就派上用場了,它上面能夠記錄一個容許操做值,像是「B 錢包地址容許 C 合約地址動用屬於 B 錢包地址的 1000 XXX Token」,以 Mapping 的結構來講標記爲「B => C => 1000」ui

BurnableToken.sol

pragma solidity ^0.4.23;

import "./BasicToken.sol";

/**
 * @title 可銷燬 Token
 * @dev Token能夠被不可逆轉地銷燬
 */
contract BurnableToken is BasicToken {

  event Burn(address indexed burner, uint256 value);

  /**
   * @dev 銷燬指定數量的token.
   * @param _value 被銷燬的token數量.
   */
  function burn(uint256 _value) public {
    _burn(msg.sender, _value);
  }

  function _burn(address _who, uint256 _value) internal {
    require(_value <= balances[_who]);
    //不須要驗證value <= totalSupply,由於這意味着發送者的餘額大於總供應量,這應該是斷言失敗 
    balances[_who] = balances[_who].sub(_value);
    totalSupply_ = totalSupply_.sub(_value);
    emit Burn(_who, _value);
    emit Transfer(_who, address(0), _value);
  }
}

該合約比較簡單,就是調用者能夠銷燬必定數量的token,而後totalSupply減去對應銷燬的數量this

StandardBurnableToken.sol

pragma solidity ^0.4.23;

import "./BurnableToken.sol";
import "./StandardToken.sol";

/**
 * @title 標準可銷燬token
 * @dev 將burnFrom方法添加到ERC20實現中
 */
contract StandardBurnableToken is BurnableToken, StandardToken {

  /**
   * @dev 從目標地址銷燬特定數量的token並減小容許量
   * @param _from address token全部者地址
   * @param _value uint256 被銷燬的token數量
   */
  function burnFrom(address _from, uint256 _value) public {
    require(_value <= allowed[_from][msg.sender]);
    // Should https://github.com/OpenZeppelin/zeppelin-solidity/issues/707 be accepted,
    // 此方法須要觸發具備更新批准的事件。
    allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
    _burn(_from, _value);
  }
}

MintableToken.sol

pragma solidity ^0.4.23;

import "./StandardToken.sol";
import "../../ownership/Ownable.sol";


/**
 * @title 可增發 token
 * @dev 簡單的可增發的 ERC20 Token 示例
 * @dev Issue: * https://github.com/OpenZeppelin/openzeppelin-solidity/issues/120
 * Based on code by TokenMarketNet: https://github.com/TokenMarketNet/ico/blob/master/contracts/MintableToken.sol
 */
contract MintableToken is StandardToken, Ownable {
  event Mint(address indexed to, uint256 amount);
  event MintFinished();
  //初始化增髮狀態爲false
  bool public mintingFinished = false;

  modifier canMint() {
    // 檢查沒有增髮結束
    require(!mintingFinished);
    _;
  }

  modifier hasMintPermission() {
    //owner只能爲msg.sender
    require(msg.sender == owner);
    _;
  }

  /**
   * @dev 增發token方法
   * @param _to 獲取增發token的地址_to.
   * @param _amount 增發的token數量.
   * @return A boolean that indicates if the operation was successful.
   */
  function mint(
    address _to,
    uint256 _amount
  )
    hasMintPermission
    canMint
    public
    returns (bool)
  {
    // 總髮行量增長_amount數量的token
    totalSupply_ = totalSupply_.add(_amount);
    // 獲取增發的地址增長_amount數量的token
    balances[_to] = balances[_to].add(_amount);
    // 觸發增發事件
    emit Mint(_to, _amount);
    // 觸發Transfer事件
    emit Transfer(address(0), _to, _amount);
    return true;
  }

  /**
   * @dev 中止增發新token.
   * @return True if the operation was successful.
   */
  function finishMinting() onlyOwner canMint public returns (bool) {
    // 改變增髮狀態爲已完成
    mintingFinished = true;
    // 觸發增發已完成事件
    emit MintFinished();
    return true;
  }
}

增發token的合約也很簡單,就是經過增發必定量的token給對應的address,並給總髮行量增長對應的增發token,能夠經過調用finishMinting來完成增發。

CappedToken.sol

pragma solidity ^0.4.23;

import "./MintableToken.sol";

/**
 * @title 上限 token
 * @dev 設置一個頂的可增發token.
 */
contract CappedToken is MintableToken {

  uint256 public cap;

  constructor(uint256 _cap) public {
    require(_cap > 0);
    cap = _cap;
  }

  /**
   * @dev 增發token
   * @param _to 獲取增發token的地址_to.
   * @param _amount 增發token數量.
   * @return A boolean that indicates if the operation was successful.
   */
  function mint(
    address _to,
    uint256 _amount
  )
    public
    returns (bool)
  {
    // 驗證總髮行量+增髮量小於所設置的上限
    require(totalSupply_.add(_amount) <= cap);
    // 調用父合約的增發方法
    return super.mint(_to, _amount);
  }

}

CappedToken 也很簡單,就是在可增發合約上加了一個"cap",來限制增發的上限

RBACMintableToken.sol

pragma solidity ^0.4.23;

import "./MintableToken.sol";
import "../../ownership/rbac/RBAC.sol";

/**
 * @title RBACMintableToken
 * @author Vittorio Minacori (@vittominacori)
 * @dev Mintable Token, with RBAC minter permissions
 */
contract RBACMintableToken is MintableToken, RBAC {
  /**
   * 指定一個增發者的常量名.
   */
  string public constant ROLE_MINTER = "minter";

  /**
   * @dev 重寫Mintable token合約的 modifier,增長角色有關的邏輯 
   */
  modifier hasMintPermission() {
    // 調用RBAC合約中的角色檢查
    checkRole(msg.sender, ROLE_MINTER);
    _;
  }

  /**
   * @dev 將一個地址添加爲可增發者角色
   * @param minter address
   */
  function addMinter(address minter) onlyOwner public {
    addRole(minter, ROLE_MINTER);
  }

  /**
   * @dev 將一個地址移除可增發者角色
   * @param minter address
   */
  function removeMinter(address minter) onlyOwner public {
    removeRole(minter, ROLE_MINTER);
  }
}

RBACMintableToken 合約將增發操做中添加了RBAC邏輯,就是角色權限管理的邏輯,將一個地址這是爲增發者角色,也能夠移除一個地址的增發者角色,只有擁有"minter"角色的address纔有權限增發token

SafeERC20.sol

pragma solidity ^0.4.23;

import "./ERC20Basic.sol";
import "./ERC20.sol";

/**
 * @title SafeERC20
 * @dev 圍繞ERC20操做發生故障的包裝程序.
 * 能夠在合約中經過這樣使用這個庫 `using SafeERC20 for ERC20;` 來使用安全的操做`token.safeTransfer(...)`
 */
library SafeERC20 {
  function safeTransfer(ERC20Basic token, address to, uint256 value) internal {
    require(token.transfer(to, value));
  }

  function safeTransferFrom(
    ERC20 token,
    address from,
    address to,
    uint256 value
  )
    internal
  {
    require(token.transferFrom(from, to, value));
  }

  function safeApprove(ERC20 token, address spender, uint256 value) internal {
    require(token.approve(spender, value));
  }
}

SafeERC20 是一個ERC20的安全操做庫,在下面的TokenTimelock鎖按期後釋放token的合約中咱們能夠看到用法

TokenTimelock.sol

pragma solidity ^0.4.23;

import "./SafeERC20.sol";

/**
 * @title TokenTimelock 鎖按期釋放token
 * @dev TokenTimelock 是一個令token持有人合同,將容許一個受益人在給定的發佈時間以後提取token
 */
contract TokenTimelock {
  //這裏用到了上面的SafeERC20
  using SafeERC20 for ERC20Basic;

  // ERC20 basic token contract being held
  ERC20Basic public token;

  // token被釋放後的受益人address
  address public beneficiary;

  // token能夠被釋放的時間戳
  uint256 public releaseTime;
  // 對token,受益人address和釋放時間初始化
  constructor(
    ERC20Basic _token,
    address _beneficiary,
    uint256 _releaseTime
  )
    public
  {
    require(_releaseTime > block.timestamp);
    token = _token;
    beneficiary = _beneficiary;
    releaseTime = _releaseTime;
  }

  /**
   * @notice 將時間限制內的token轉移給受益人.
   */
  function release() public {
    require(block.timestamp >= releaseTime);

    uint256 amount = token.balanceOf(this);
    require(amount > 0);

    token.safeTransfer(beneficiary, amount);
  }
}

TokenTimelock 合約經過初始化受益人以及釋放的時間和鎖定的token,經過release來將鎖按期事後釋放的token轉給受益人

TokenVesting.sol

pragma solidity ^0.4.23;

import "./ERC20Basic.sol";
import "./SafeERC20.sol";
import "../../ownership/Ownable.sol";
import "../../math/SafeMath.sol";

/**
 * @title TokenVesting 按期釋放token
 * @dev token持有人合同能夠逐漸釋放token餘額典型的歸屬方案,有斷崖時間和歸屬期, 可選擇可撤銷的全部者。
 */
contract TokenVesting is Ownable {
  using SafeMath for uint256;
  using SafeERC20 for ERC20Basic;

  event Released(uint256 amount);
  event Revoked();

  // 釋放後的token收益人
  address public beneficiary;

  uint256 public cliff; //斷崖表示「鎖倉4年,1年以後一次性解凍25%」中的一年
  uint256 public start;//起始時間
  uint256 public duration;//持續鎖倉時間

  bool public revocable;

  mapping (address => uint256) public released;
  mapping (address => bool) public revoked;

  /**
   * @dev 建立一份歸屬權合同,將任何ERC20 token的餘額歸屬給_beneficiary,逐漸以線性方式,直到_start + _duration 全部的餘額都將歸屬。
   * @param _beneficiary 授予轉讓token的受益人的地址
   * @param _cliff 持續時間以秒爲單位,代幣將開始歸屬
   * @param _start 歸屬開始的時間(如Unix時間)
   * @param _duration 持續時間以token的歸屬期限爲單位
   * @param _revocable 歸屬是否可撤銷
   */
  constructor(
    address _beneficiary,
    uint256 _start,
    uint256 _cliff,
    uint256 _duration,
    bool _revocable
  )
    public
  {
    require(_beneficiary != address(0));
    require(_cliff <= _duration);

    beneficiary = _beneficiary;
    revocable = _revocable;
    duration = _duration;
    cliff = _start.add(_cliff);
    start = _start;
  }

  /**
   * @notice 將歸屬代幣轉讓給受益人.
   * @param token ERC20 token which is being vested
   */
  function release(ERC20Basic token) public {
    uint256 unreleased = releasableAmount(token);

    require(unreleased > 0);

    released[token] = released[token].add(unreleased);

    token.safeTransfer(beneficiary, unreleased);

    emit Released(unreleased);
  }

  /**
   * @notice容許全部者撤銷歸屬。 token已經歸屬合約,其他歸還給全部者。  
   * @param token ERC20 token which is being vested
   */
  function revoke(ERC20Basic token) public onlyOwner {
    require(revocable);
    require(!revoked[token]);

    uint256 balance = token.balanceOf(this);

    uint256 unreleased = releasableAmount(token);
    uint256 refund = balance.sub(unreleased);

    revoked[token] = true;

    token.safeTransfer(owner, refund);

    emit Revoked();
  }

  /**
   * @dev 計算已歸屬但還沒有釋放的金額。
   * @param token ERC20 token which is being vested
   */
  function releasableAmount(ERC20Basic token) public view returns (uint256) {
    return vestedAmount(token).sub(released[token]);
  }

  /**
   * @dev 計算已歸屬的金額.
   * @param token ERC20 token which is being vested
   */
  function vestedAmount(ERC20Basic token) public view returns (uint256) {
    uint256 currentBalance = token.balanceOf(this);
    uint256 totalBalance = currentBalance.add(released[token]);

    if (block.timestamp < cliff) {
      return 0;
    } else if (block.timestamp >= start.add(duration) || revoked[token]) {
      return totalBalance;
    } else {
      return totalBalance.mul(block.timestamp.sub(start)).div(duration);
    }
  }
}

TokenVesting也是鎖倉的一種方式,主要解決的是有斷崖時間和持續鎖倉時間的鎖倉場景

PausableToken.sol

pragma solidity ^0.4.23;

import "./StandardToken.sol";
import "../../lifecycle/Pausable.sol";


/**
 * @title Pausable token
 * @dev StandardToken modified with pausable transfers.
 **/
contract PausableToken is StandardToken, Pausable {

  function transfer(
    address _to,
    uint256 _value
  )
    public
    whenNotPaused
    returns (bool)
  {
    return super.transfer(_to, _value);
  }

  function transferFrom(
    address _from,
    address _to,
    uint256 _value
  )
    public
    whenNotPaused
    returns (bool)
  {
    return super.transferFrom(_from, _to, _value);
  }

  function approve(
    address _spender,
    uint256 _value
  )
    public
    whenNotPaused
    returns (bool)
  {
    return super.approve(_spender, _value);
  }

  function increaseApproval(
    address _spender,
    uint _addedValue
  )
    public
    whenNotPaused
    returns (bool success)
  {
    return super.increaseApproval(_spender, _addedValue);
  }

  function decreaseApproval(
    address _spender,
    uint _subtractedValue
  )
    public
    whenNotPaused
    returns (bool success)
  {
    return super.decreaseApproval(_spender, _subtractedValue);
  }
}

PausableToken繼承了StandardToken,可是在方法中都添加了whenNotPaused函數修改器,whenNotPaused繼承自Pausable合約,Pausable有個paused來標記暫停的狀態,從而控制合約的是否暫停。

OpenZeppelin ERC20源碼分析到這裏就結束了。

轉載請註明: 轉載自Ryan是菜鳥 | LNMP技術棧筆記

若是以爲本篇文章對您十分有益,何不 打賞一下

謝謝打賞

本文連接地址: OpenZeppelin ERC20源碼分析

相關文章
相關標籤/搜索