摘要:前提是拥有者必须要通过某些机制对这个请求进行确认,比如通过进行。事件,当被调用时,需要触发该事件。允许从中转出的数增加所有者允许花费代币的数量。已经归属合约,其余归还给所有者。计算已归属但尚未释放的金额。源码分析到这里就结束了。
ERC20:Ethereum Request for Comments 20,是一个基于以太坊代币的接口标准(协议)。所有符合ERC-20标准的代币都能立即兼容以太坊钱包,它能让用户和交易所,都能非常方便的管理多种代币,转账、存储、ICO等等。
OpenZeppelin的Token中实现了ERC20的一个安全的合约代码,本篇主要来分析一下源码,了解一下ERC20的实现,由于代码之间的调用可能略复杂,直接每个文件每个文件的来看会有点绕,我直接画了一个继承和调用关系的思维导图,可以帮助更容易地看源码。
ERC20Basic.solpragma 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的基本接口,定义了必须要实现的方法:
totalSupply 返回总共发行量
balanceOf 查询指定address的余额
transfer 发送指定数目的token到指定账户,同时发送后需要触发Transfer事件
Transfer事件,任何token发送发生时,必须触发该事件,即使是0额度。 当一个token合约创建时,应该触发一个Transfer事件,token的发送方是0x0,也就是说凭空而来的token,简称空气币。
ERC20.solpragma 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被调用时,需要触发该事件。
DetailedERC20.solpragma 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.solpragma 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.solpragma 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」
BurnableToken.solpragma 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减去对应销毁的数量
StandardBurnableToken.solpragma 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.solpragma 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.solpragma 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.solpragma 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.solpragma 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.solpragma 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.solpragma 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源码分析
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/24147.html
摘要:它和我写的上一篇源码分析介绍的有所不同,最小的单位为无法再分割,代表独一无二的,针对不可置换的的智能合约标准接口。源码分析到这里就结束了。 ERC721 官方简介是:A standard interface for non-fungible tokens, also known as deeds.也叫非同质代币,或者不可置换代币(NFTs)。提到ERC721,一个好理解的例子就是Cry...
摘要:从这节开始,我们将学习代币标准以及加密收集资产等知识。声明一个继承的新合约,命名为。注意目前是一个草稿,还没有正式商定的实现。所以把这一个可能的实现当作考虑,但不要把它作为代币的官方标准。 从这节开始,我们将学习代币, ERC721标准, 以及加密收集资产等知识。 一、代币 代币 让我们来聊聊以太坊上的代币。 如果你对以太坊的世界有一些了解,你很可能听过人们聊到代币——尤其是 ERC2...
摘要:合约安全增强溢出和下溢我们将来学习你在编写智能合约的时候需要注意的一个主要的安全特性防止溢出和下溢。实战演练给加上一些标签把这里变成标准的注释把一个管理转移僵尸所有权的合约符合对标准草案的实现 通过上一节的学习,我们完成了 ERC721 的实现。并不是很复杂,对吧?很多类似的以太坊概念,当你只听人们谈论它们的时候,会觉得很复杂。所以最简单的理解方式就是你自己来实现它。 一、预防溢出 不...
摘要:我们目前正处于一个新兴的区块链开发行业中。,一种在以太坊开发人员中流行的新的简单编程语言,因为它是用于开发以太坊智能合约的语言。它是全球至少万开发人员使用的世界上最流行的编程语言之一。以太坊,主要是针对工程师使用进行区块链以太坊开发的详解。 我们目前正处于一个新兴的区块链开发行业中。区块链技术处于初期阶段,然而这种颠覆性技术已经成功地风靡全球,并且最近经历了一场与众不同的繁荣。由于许多...
摘要:假设某一天,星巴克突然宣布为了拥抱区块链技术,不再接受法币买咖啡了,大家以后可以用以太币或者星巴克自己发行的星星币来买咖啡。用星星币买咖啡星巴克自己发行了,取名,遵循协议。 什么是ERC20 ERC20是以太坊上为token提供的一种协议,也可以理解成一种token的共同标准。遵循ERC20协议的token都可以兼容以太坊钱包,让用户在钱包中可以查看token余额以及操作token转账...
阅读 3659·2021-10-18 13:34
阅读 2373·2021-08-11 11:15
阅读 1122·2019-08-30 15:44
阅读 665·2019-08-26 10:32
阅读 965·2019-08-26 10:13
阅读 2046·2019-08-23 18:36
阅读 1756·2019-08-23 18:35
阅读 502·2019-08-23 17:10