以太坊智能合约[ERC20]发币记录
以太坊被称为区块链2.0,就是因为以太坊在应用层提供了虚拟机,使得开发者可以基于它自定义逻辑,通常被称为智能合约,合约中的公共接口可以作为区块链中的普通交易执行。本文就智能合约发代币流程作一完整介绍(当然智能合约不局限于发代币)。内容如下:
- Solidity
- ERC20
- 合约编写与发布
- 合约源码上传
- 其它
Solidity
Solidity是用于在以太坊编写智能合约的语言,目前最新版本0.5.11。这里对几个关键概念作一介绍。
library
library常用于提供可复用方法,可以随合约[作为合约的一部分]发布,不过最终它是单独部署[到链上]的,有自己的地址。外部可以使用delegatecall方法调用库函数。
我们使用library关键字来创建一个library,这和创建contract十分类似。但不像contract,在library中我们不能定义任何storage类型的变量。因为library只是意味着代码的重用而不是进行state的状态管理。
internal的库函数对所有合约可见。
using for:指令using A for B;用来附着库A里定义的函数到任意类型B,函数的第一个参数应是B的实例。可以使用using A for *,将库函数赋予任意类型。库函数可以重载的,你可以定义好几个同名函数,但是第一个参数的类型不同,调用的时候自动的根据调用类型选择某一种方法。
address indexed
The indexed parameters for logged events will allow you to search for these events using the indexed parameters as filters.
The indexed keyword is only relevant to logged events.
几个预定义字段
address.balance:地址余额,指该地址相关的以太币数额。
msg.sender:交易发起人。
this:合约地址。
msg.value:发起这笔交易支付的以太币数额,和payable搭配使用。
似乎还有msg.sig、msg.data、msg.gas。
payable
修饰函数,表示在调用函数时,可以给这个合约充以太币(合约也是一种账户,也有自己的地址)。合约本身持有以太币,可使得用户基于此合约进行跨币交易更方便。
constant、view、pure
constant修饰常量或函数,修饰函数时表示该函数不修改合约状态,即不产生需广播的交易,也就不会消耗gas,一般用于读状态或状态无关操作。0.4.17开始,改为view和pure修饰函数,前者表示读状态,后者表示状态无关。
存储区
storage:状态变量,将存储在链上;
memory:临时变量
interface:搭配传入的不同address,实现多态。
event,应用层(web3)可通过watch监听,应该是用轮询实现。
modifier,简单的aop,编译时会将代码替换合并。
ERC20
我们可以在智能合约里随意开发各种功能,相当一部分以代币合约的形式存在。既然是代币,自然有转账、余额查询等功能,大家苦于对不同的代币合约开发不同的钱包,于是有了一些规范的出现,其中最普遍的是ERC20。
ERC20定义了若干方法,网上有很多资料,这里不再赘述。稍微不好理解的是approve、transferFrom及allowance三个方法,这里举例说明——账户A有1000个ETH,想授权B账户随意调用A账户的100个ETH,则需调用approve(B,100)。当B账户想用这100个ETH中的10个ETH给C账户时,则调用transferFrom(A, C, 10)。这时调用allowance(A, B)可以查看B账户还能够调用A账户多少个token。
本人试用了若干本地钱包和在线钱包,都支持转账,但鲜有支持授权的。
有人在github上汇总了众多ERC20合约代码,see 基于以太坊发行的ERC20代币合约代码大集合 。
合约编写与发布
一个简单的合约demo如下:
1 pragma solidity ^0.5.7; 2 3 import \'./safemath.sol\'; 4 5 contract ErbCoin { 6 using SafeMath for uint256; 7 8 string constant public name = "Mask Coin"; // token name 9 string constant public symbol = "MASK"; // token symbol 10 uint256 public decimals = 18; // token digit 11 12 //mapping (address => uint256) public balanceOf; 13 mapping (address => mapping (address => uint256)) public allowance; //a授权给b表示b可转账给其他人的代币数 14 mapping (address => uint256) public frozenBalances; //冻结余额 15 mapping (address => uint256) public balances; //可操作余额 16 17 uint256 public totalSupply = 0; 18 bool public stopped = false; 19 20 //uint256 constant valueFounder = 1000000; 21 address constant zeroaddr = address(0); 22 address owner = zeroaddr; //合约所有者 23 address founder = zeroaddr; //初始代币持有者 24 25 modifier isOwner { 26 assert(owner == msg.sender); 27 _; 28 } 29 30 modifier isFounder { 31 assert(founder == msg.sender); 32 _; 33 } 34 35 modifier isAdmin { 36 assert(owner == msg.sender || founder == msg.sender); 37 _; 38 } 39 40 modifier isRunning { 41 assert (!stopped); 42 _; 43 } 44 45 modifier validAddress { 46 assert(zeroaddr != msg.sender); 47 _; 48 } 49 50 constructor(address _addressFounder,uint256 _valueFounder) public { 51 owner = msg.sender; 52 founder = _addressFounder; 53 totalSupply = _valueFounder*10**decimals; 54 balances[founder] = totalSupply; 55 emit Transfer(zeroaddr, founder, totalSupply); 56 } 57 58 function balanceOf(address _owner) public view returns (uint256) { 59 //账户余额 = 可操作余额 + 被冻结余额 60 return balances[_owner] + frozenBalances[_owner]; 61 } 62 63 function transfer(address _to, uint256 _value) public isRunning validAddress returns (bool success) { 64 balances[msg.sender] = balances[msg.sender].sub(_value); 65 balances[_to] = balances[_to].add(_value); 66 emit Transfer(msg.sender, _to, _value); 67 return true; 68 } 69 70 //msg.sender 将 _from 授权给他(msg.sender)的代币转给 _to 71 function transferFrom(address _from, address _to, uint256 _value) public isRunning validAddress returns (bool success) { 72 balances[_from] = balances[_from].sub(_value); 73 //balances[_to] = balances[_to].add(_value); 74 frozenBalances[_to] = frozenBalances[_to].add(_value); //代币为冻结状态 75 allowance[_from][msg.sender] = allowance[_from][msg.sender].sub(_value); 76 emit TransferFrozen(_to, _value); 77 return true; 78 } 79 80 //msg.sender 授权 _spender 可操作代币数 81 function approve(address _spender, uint256 _value) public isRunning isFounder returns (bool success) { 82 require(_value == 0 || allowance[msg.sender][_spender] == 0,"illegal operation"); 83 allowance[msg.sender][_spender] = _value; 84 emit Approval(msg.sender, _spender, _value); 85 return true; 86 } 87 88 //冻结部分释放 89 function release(address _target, uint256 _value) public isRunning isAdmin returns(bool){ 90 frozenBalances[_target] = frozenBalances[_target].sub(_value); 91 balances[_target] = balances[_target].add(_value); 92 emit Release(_target, _value); 93 return true; 94 } 95 96 function stop() public isAdmin { 97 stopped = true; 98 } 99 100 function start() public isAdmin { 101 stopped = false; 102 } 103 104 event Transfer(address indexed _from, address indexed _to, uint256 _value); 105 event Approval(address indexed _owner, address indexed _spender, uint256 _value); 106 event TransferFrozen(address _target, uint256 _value); 107 event Release(address _target, uint256 _value); 108 }
View Code
细心的朋友会发现,ERC20规范定义的都是函数,而这里的代码并没有name()、symbol()、decimals(),而只有同名字段,这是因为编译器会自动将公共字段编译为函数。
代码里还import了一个库文件,是为了避免数值溢出导致的bug,具体可看关于ERC20 Token智能合约的SafeMath安全。
发布合约我们可以直接在浏览器端操作,需要用到:
MetaMask:一款以浏览器插件形式存在的以太坊钱包,能接入主链和多个测试链。
Remix:https://remix.ethereum.org,编写编译发布合约的一个站点,能和MetaMask无缝合作。
具体如何操作就不作介绍了,网上已有较多资料。
本人踩过一个坑,发币多次在imToken(一个较流行的钱包App)上显示的代币名称都是XXX_UNKOWN,后面多了_UNKOWN后缀;后有一次将contract名称和代币name、symbol保持一致,才显示正常,但不知是否是名称不一致导致的问题。
另合约部署成功后,可以在Remix上调用合约接口。如果你知道其它人的合约代码和合约地址,也可以在Remix上编译后,输入地址调用,如下图:
如果你知道合约的ABI,那么使用https://www.myetherwallet.com调用合约接口则更方便,如下图:
别人的合约代码和ABI从哪里来?请看下节。
合约源码上传
为什么要上传智能合约的代码呢?
- 公开token的源码,增加透明度和投资人的信任度;
- 上传源码后,人们可以在Etherscan查看当前token的源码,同时也可以很方便的看到token的相关信息;
- 上述所说,如果不是标准接口,市面上的各类钱包不支持,那么可以通过Remix或myetherwallet直接调用接口,而不用另外开发定制化钱包。
代码上传是在https://etherscan.io上完成的,目前上传很简单,基本上上传代码文件(有几个传几个)后next即可(不像网上有些资料说的需要另外输入byteCode和ABI)。不过需要注意的是国内网络问题(你懂的),在提交验证合约时,总是提示 “Sorry! We encountered an unexpected error. Please try back again shortly ”,这是因为验证码被墙了,如何操作不需要我教了吧。
其它
在智能合约中,只能等待外部的调用,而无法执行定时任务。同时也无法主动发起外部请求。
默认情况下,geth创建的账户是被锁住的,可以转入不能转出,除非解锁(geth –unlock)。尽量避免在对外的节点服务器上做解锁操作,否则将有被盗币的风险。具体可看金钱难寐,大盗独行——以太坊 JSON-RPC 接口多种盗币手法大揭秘,本人亦有教训,所幸损失不大。实际对外提供服务的全节点,做到以下一点或几点,就能显著降低风险:
不使用默认端口;
RPC只对内开放,对外须网关协议转换;
服务器上不存储账户,或不直接在服务器上使用账户。
助记词(BIP32/BIP39/BIP44)的原理可参看 比特币源码研读(7)—— 钱包的原理
智能合约的同一个接口,gas used 未必相同,要看实际执行到的步骤和对存储的操作,规则如下:
EVM智能合约的实现原理:solidity虽然是编译后上链的,但执行时依旧是由EVM(其实也是以太坊内的若干函数)解释执行的,EVM通过字节码确定操作符和操作数,然后其自身执行相应逻辑。从这个角度说,EVM就是solidity的解释器。若将EVM看作操作系统,solidity编译后即能被EVM运行,那么solidity也可认为是编译型语言。可看EVM原理及其功能扩展。
将数据存储在event产生的log中,比存在storage变量中划算的多,可参看在Java中监听以太坊智能合约事件。
区块链一般都应该是达成共识之后再执行交易。
其它参考资料: