以太坊购买代币(一文读懂以太坊代币合约)

  以太坊购买代币(一文读懂以太坊代币合约)

工欲善其事,必先利其器。要想挖掘和分析智能合约的漏洞,你必须要先学会看懂智能合约。而目前智能合约中有很大一部分是发行代币的,那什么是代币,他们有什么标准呢?本文就是带领你入门,教会你看懂一个代币的智能合约。

  

以太坊代币

  

在以太坊系统中,存在作为基础货币的 Ether(以太),以及同样可以作为货币使用的 Token(代币)。

  

以太坊与其他加密货币的主要不同在于,以太坊不是单纯的货币,而是一个环境/平台。在这个平台上,任何人都可以利用区块链的技术,通过智能合约来构建自己的项目和DAPPS(去中心化应用)。

  

如果把以太坊理解成互联网,DAPPS则是在上面运行的网页。DAPPS是去中心化的,意味着它不属于某个人,而是属于一群人。DAPPS发布的方式通常是采用被称为 ICO 的众筹方式。简单来说,你需要用你的以太来购买相应DAPP的一些tokens。

  

一般有两种Token:

  

  1. Usage Tokens! 就是对应 DAPP 的原生货币。Golem 就是一个很好的例子,如果你需要使用 Golem 的服务,你就需要为其支付 Golem Network Token(GNT)。由于这种 Tokens 有货币价值,所以通常不会有其他的权益。
  2. Work Tokens! 此类 Tokens 可以标识你对于 DAPP 的某种股东权益。以 DAO tokens 为例,如果你拥有 DAO tokens,那么你有权就DAO是否资助某款 DAPP 来进行投票。

类比到股权,可以把 Usage Tokens 简单理解为普通流通股,可以与真实货币兑换,本身具有价值。而 Work Token,则大致相当于投票权。

  

为何需要Token!

  

不是有以太基础货币了,那为什么还需要 token 呢?可以想下现实生活的真实场景,在游乐场里,我们需要用现金兑换代币,然后用代币支付各种服务。 类比到以太坊,现金就是以太,代币就是 token,用 token 来执行合约中的各项功能。

  

以太坊Token标准

  

这个是本文学习的重点,所有遵循 ERC20 标准的函数,都要事先它定义的标准接口。搞懂这些,你也就能很快看懂一些智能合约代币的逻辑。

  

ERC-20 标准是在2015年11月份推出的,使用这种规则的代币,表现出一种通用的和可预测的方式。任何 ERC-20 代币都能立即兼容以太坊钱包(几乎所有支持以太币的钱包,包括Jaxx、MEW、imToken等),由于交易所已经知道这些代币是如何操作的,它们可以很容易地整合这些代币。这就意味着,在很多情况下,这些代币都是可以立即进行交易的。简单理解就是,ERC20是开发者在自己的tokens中必须采用的一套具体的公式/方法,从而确保不同DAPP的token与ERC20标准兼容。

  

ERC-20 标准规定了各个代币的基本功能,非常方便第三方使用,在开发人员的编程下,5 分钟就可以发行一个 ERC-20 代币。因为它可以快速发币,而且使用又方便,因此空投币和空气币基本上就是利用 ERC-20 标准开发的。基于 ERC-20 标准开发的同种代币价值都是相同的,它们可以进行互换。ERC-20 代币就类似于人民币,你的 100 元和我的 100 元是没有区别的,价值都是 100 元,并且这两张 100 元可以进行互换。有了这套标准,相当于全世界都使用人民币,而不用去别的国家还要计算汇率换成别的货币。想象下,每个Dapp都有不同格式的币,那对于这些应用的交互简直是种灾难。

  

etherscan上开源的 ERC20 标准的智能合约:https!//etherscan。io/tokens

  

ERC20 Token标准接口:

  

contract ERC20 {

  

uint256 public totalSupply;

  

function balanceOf(address who) constant public returns (uint256);

  

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

  

function allowance(address owner, address spender) constant public returns (uint256);

  

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

  

function approve(address spender, uint256 value) public returns (bool);

  

event Transfer(address indexed from, address indexed to, uint256 value);

  

event Approval(address indexed owner, address indexed spender, uint256 value);

  

}

  

函数:

  

注意:非常重要的一点是调用者应该处理函数返回的错误,而不是假设错误永远不会发生。

  

  1. totalSupply! 返回token的总供应量
  2. balanceOf! 用于查询某个账户的账户余额
  3. tansfer! 发送 _value 个 token 到地址 _to
  4. transferFrom: 从地址 _from 发送 _value 个 token 到地址 _to
  5. approve: 允许 _spender 多次取回您的帐户,最高达 _value 金额; 如果再次调用此函数,它将用 _value 的当前值覆盖的 allowance 值。
  6. allowance: 返回 _spender 仍然被允许从 _owner 提取的金额。

事件 !

  

  1. event Transfer: 当 tokens 被转移时触发。
  2. event Approval: 当任何成功调用 approve(address _spender, uint256 _value) 后,必须被触发。

代币合约实例分析

  

talk is cheap show me the code 。前面给的函数说明是简单的概括,大家可能还似懂非懂,下面就将用实例说明。《AMR智能合约漏洞分析》这篇文章用实例讲解了智能合约的一种漏洞,合约代码在 https!//etherscan。io/address/0x96c833e43488c986676e9f6b3b8781812629bbb5#code ,我们就以这个代码做个详细的分析。

  

开始一行行分析这个智能合约:

  

library SafeMath {

  

function mul(uint256 a, uint256 b) internal pure returns (uint256){

  

uint256 c = a * b;

  

assert(a == 0 c / a == b);

  

return c;

  

}

  

function div(uint256 a, uint256 b) internal pure returns (uint256){

  

assert(b >; 0);

  

uint256 c = a / b;

  

return c;

  

}

  

function sub(uint256 a, uint256 b) internal pure returns (uint256){

  

assert(b <;= a);

  

return a - b;

  

}

  

function add(uint256 a, uint256 b) internal pure returns (uint256){

  

uint256 c = a + b;

  

assert(c >;= a);

  

return c;

  

}

  

}

  

这个比较简单,定义安全函数的库,用来防止整数溢出漏洞。

  

contract ERC20 {

  

uint256 public totalSupply;

  

function balanceOf(address who) constant public returns (uint256);

  

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

  

function allowance(address owner, address spender) constant public returns (uint256);

  

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

  

function approve(address spender, uint256 value) public returns (bool);

  

event Transfer(address indexed from, address indexed to, uint256 value);

  

event Approval(address indexed owner, address indexed spender, uint256 value);

  

}

  

ERC20 标准接口,上一节说过了。

  

contract Ownable {

  

address owner;

  

// 把当前合约的调用者赋值给owner

  

function Ownable() public{

  

owner = msg。sender;

  

}

  

// 只有智能合约的所有者才能调用的方法

  

  

modifier onlyOwner(){

  

require(msg。sender == owner);

  

_;

  

}

  

// 合约的所有者可以把权限转移给其他用户

  

function transferOwnership(address newOwner) onlyOwner public{

  

require(newOwner != address(0));

  

owner = newOwner;

  

}

  

}

  

这个合约接口的功能是判断和修改该合约的所有者。其中函数 onlyOwner 用到了 modifiers(函数修改器) 关键字。函数修改器可以用来改变一个函数的行为,比如用于在函数执行前检查某种前置条件。如果你了解 python 的装饰器,这个就很容易理解了。还不理解?没关系,我们再详细说明下这个接口。首先你需要理解下这边的几个概念:

  

1。 msg。sender 内置变量,区块链平台代表当前调用该合约的账户地址。

  

2。 Ownable() 函数,和合约接口同名,这是个构造函数,只能在创建合约期间运行,不能在事后调用。所以这个owner是创建该合约人的地址,无法被篡改,除非合约创始人授权。

  

3。 特殊字符串 _; 用来替换使用修改符的函数体。比如上述代码就是把 _; 替换成 transferOwnership ,也就是执行 transferOwnership 函数时候会先判断 require(msg。sender == owner);

  

下面把这个合约加一个打印owner的函数,然后放到 remix 调试,这样更直观理解。

  

pragma solidity ^0。4。24;

  

contract Ownable {

  

address owner;

  

// 把当前合约的调用者赋值给owner

  

function Ownable() public{

  

owner = msg。sender;

  

}

  

function CurrentOwner() public returns (address){

  

return owner;

  

}

  

// 只有智能合约的所有者才能调用的方法

  

modifier onlyOwner(){

  

require(msg。sender == owner);

  

_;

  

}

  

// 合约的所有者可以把权限转移给其他用户

  

function transferOwnership(address newOwner) onlyOwner public{

  

require(newOwner != address(0));

  

owner = newOwner;

  

}

  

}

  

a) 使用账户 A 创建合约,则 owner 则是 A 的地址,切换到用户 B 点击 onlyOwner 函数,看到owner的值是账户 A 的地址。这时候如果点击 transferOwnership 会报错,因为这个函数被 onlyOwner 修饰了,会先判断当前调用合约的是否是合约所有者。当前合约所有者是账户 A,合约调用者账户 B 是没权限转移权限的。

  

b) 把账户切换到 A,transferOwnership 地址填账户 B,这时候你就可以把合约所有者权限转移给账户 B 了。而再一次执行,发现提示错误了,因为此时合约所有者已经是账户 B,账户 A 没权限。

  

contract StandardToken is ERC20 {

  

using SafeMath for uint256;// 使用 SafeMath 函数库

  

mapping (address =>; mapping (address =>; uint256)) allowed;// 类比二维数组

  

mapping(address =>; uint256) balances;// 类比一维数组

  

// 把合约调用者的余额转移 _value 个tokens给用户 _to

  

function transfer(address _to, uint256 _value) public returns (bool){

  

assert(0 <; _value);

  

assert(balances[msg。sender] >;= _value);

  

balances[msg。sender] = balances[msg。sender]。sub(_value);

  

balances[_to] = balances[_to]。add(_value);

  

emit Transfer(msg。sender, _to, _value);

  

return true;

  

}

  

// 查询 _owner 账户的余额

  

function balanceOf(address _owner) constant public returns (uint256 balance){

  

return balances[_owner];

  

}

  

// 从地址 _from 转移 _value 个 tokens 给地址 _to

  

function transferFrom(address _from, address _to, uint256 _value) public returns (bool){

  

uint256 _allowance = allowed[_from][msg。sender];

  

assert (balances[_from] >;= _value);

  

assert (_allowance >;= _value);

  

assert (_value >; 0);

  

balances[_to] = balances[_to]。add(_value);

  

balances[_from] = balances[_from]。sub(_value);

  

allowed[_from][msg。sender] = _allowance。sub(_value);

  

emit Transfer(_from, _to, _value);

  

return true;

  

}

  

// 允许 _spender 多次取回您的帐户,最高达 _value 金额; 如果再次调用此函数,它将用 _value 的当前值覆盖的 allowance 值

  

function approve(address _spender, uint256 _value) public returns (bool){

  

require((_value == 0) (allowed[msg。sender][_spender] == 0));

  

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

  

emit Approval(msg。sender, _spender, _value);

  

return true;

  

}

  

// 返回 _spender 仍然被允许从 _owner 提取的金额

  

function allowance(address _owner, address _spender) constant public returns (uint256 remaining){

  

return allowed[_owner][_spender];

  

}

  

}

  

这边逻辑不复杂,有个概念可能不太好理解,这里详细说明下。allowed 这个变量(类比成二维数组),是用来存取授信的额度,在 approve 函数中定义。

  

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

  

这个 msg。sender 是当前合约调用者,_spender 是被授权人,额度是 _value 。可以通俗的理解成,银行(msg。sender)给用户( _spender) 授权了 _value 额度的 tokens 。在银行转账,相应的额度也会减少,而用户在此银行最多可以转被授权的 _value 个 tokens,不同的银行(msg。sender)可以给用户(_spender)授信不同的额度(_value)。

  

把 allowd 的概念理解了,allowance 函数也就很好理解了,第一个参数 _owner 类比成银行,第二个参数 _spender 类比成用户,这个函数就用来查询用户(_spender)在银行(_owner)剩余的额度(tokens)。通过上述的讲解,可以知道 transfer 和 transferfrom 函数区别如下:

  

1。 transfer 是把当前合约调用者的 tokens 转移给其他人

  

2。 transferFrom 则是可以把 ”银行“ 授信额度的钱(tokens)转给自己或者他人,转移的是 “银行” 的 tokens

  

contract Ammbr is StandardToken, Ownable {

  

string public name = ;

  

string public symbol = ;

  

uint8 public decimals = 0;

  

uint256 public maxMintBlock = 0;

  

event Mint(address indexed to, uint256 amount);

  

// 给地址 _to 初始化数量 _amount 数量的 tokens,注意 onlyOwner 修饰,只有合约创建者才有权限分配

  

function mint(address _to, uint256 _amount) onlyOwner public returns (bool){

  

assert(maxMintBlock == 0);

  

totalSupply = totalSupply。add(_amount);

  

balances[_to] = balances[_to]。add(_amount);

  

emit Mint(_to, _amount);

  

maxMintBlock = 1;

  

return true;

  

}

  

// 转帐操作,可以同时转给多个人

  

function multiTransfer(address[] destinations, uint[] tokens) public returns (bool success){

  

assert(destinations。length >; 0);

  

assert(destinations。length <; 128);

  

assert(destinations。length == tokens。length);

  

uint8 i = 0;

  

uint totalTokensToTransfer = 0;

  

for (i = 0; i <; destinations。length; i++){

  

assert(tokens[i] >; 0);

  

totalTokensToTransfer += tokens[i]; // 存在溢出

  

}

  

assert (balances[msg。sender] >; totalTokensToTransfer);

  

balances[msg。sender] = balances[msg。sender]。sub(totalTokensToTransfer);

  

for (i = 0; i <; destinations。length; i++){

  

balances[destinations[i]] = balances[destinations[i]]。add(tokens[i]);

  

emit Transfer(msg。sender, destinations[i], tokens[i]);

  

}

  

return true;

  

}

  

// 构造函数,可选

  

function Ammbr(string _name , string _symbol , uint8 _decimals) public{

  

name = _name; // 设定代币的名字,比如: MyToken

  

symbol = _symbol; // 返回代币的符号,比如: ARM

  

decimals = _decimals; // 设置 token 的精度

  

}

  

}

  

现在来详细说明下 decimals 这个参数。首先我们来理解下常说的以太(ether)到底是怎么换算的。在以太坊交易中,最小的单位是 wei ,1 ether = 10^18 wei 。单位换算在线地址: https!//converter。murkin。me/

  

ether单位对照表:

  

调用合约转发 Token 的时候,传入的值是要转发的 Token 数乘上精度(默认decimals=18),比如转1个Token,传入合约的值是1000000000000000000 wei

  

代币(Token)参数对照表:

  

前面把我认为的难点、疑惑点都说完了,想必大家看懂这个合约也没什么难度。看懂合约后,如果看过以太坊智能合约安全漏洞入门之类的文章,应该一看就能看出 multiTransfer 存在溢出漏洞。原理就是 totalTokensToTransfer 没有使用安全函数,可以导致整数上溢出。详情可参考 这篇文章 。下面我们就手动调试下这个漏洞

  

1。 首先运行 mint 函数,给账户 A 初始化 100000 个tokens

  

2。 向账户 B,C 分别充值 57896044618658097711785492504343953926634992332820282019728792003956564819968, 相加得 115792089237316195423570985008687907853269984665640564039457584007913129639936,而账户 unit256 最大值15792089237316195423570985008687907853269984665640564039457584007913129639935,导致溢出 totalTokensToTransfer 的值为0

  

注意: 在 remix 调试时候,传入的数组可以用 [。。。。。。, 。。。。。。] 表示,但是地址必须用双引号包裹,传入的数字如果较大也必须用双引号包裹,否则会报错

  

总结:

  

其实智能合约的代码比平常分析逆向的程序代码都简单多了,只要你掌握 ERC20 的标准几个接口,了解函数修改器的概念以及一些以太坊基本的概念,相信看懂一个 ERC20 标准的合约并不难。也希望大家能把上述分析自己动手实践一下,之前我很多地方也有疑问,通过动手实践很快就明白问题的所在了。如果学习有捷径的话就是多动手,多调试。Solidity 没有打印的函数,有时候会给调试带来不便,下面补个 Solidity 调试的代码,方便大家打印变量值。

  

pragma solidity ^0。4。21;

  

//通过log函数重载,对不同类型的变量trigger不同的event,实现solidity打印效果,使用方法为:log(string name, var value)

  

contract Console {

  

event LogUint(string, uint);

  

function log(string s , uint x) internal {

  

emit LogUint(s, x);

  

}

  

event LogInt(string, int);

  

function log(string s , int x) internal {

  

emit LogInt(s, x);

  

}

  

event LogBytes(string, bytes);

  

function log(string s , bytes x) internal {

  

emit LogBytes(s, x);

  

}

  

event LogBytes32(string, bytes32);

  

function log(string s , bytes32 x) internal {

  

emit LogBytes32(s, x);

  

}

  

event LogAddress(string, address);

  

function log(string s , address x) internal {

  

emit LogAddress(s, x);

  

}

  

event LogBool(string, bool);

  

function log(string s , bool x) internal {

  

emit LogBool(s, x);

  

}

  

}

  

把文件保存成 Console。sol ,其他程序中引用这个文件即可。具体用法如下

  

以上是全文内容,如果文中说的有误,或者大家有什么更好的想法,欢迎大家和我交流。

  

参考:

  

http!//yinxiangblog。com/?id=10

  

https!//github。com/ethereum/EIPs/issues/20

  

https!//blog。csdn。net/diandianxiyu_geek/article/details/78082551

  

https!//theethereum。wiki/w/index。php/ERC20_Token_Standard

  

http!//www。cnblogs。com/huahuayu/p/8593774。html

  

https!//docs。google。com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit#heading=h。wqhvh2y0obwt

  

脉搏地址:https!//www。secpulse。com/

  

微博地址:https!//weibo。com/311057789/home?wvr=5

  
","content_hash"!"4450f221

版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。

评论