OpenZeppelin分析一

Token源码解析

Posted by Emo on August 25, 2022

OpenZeppelin

EMO’s Blog

token

token根据其 同质化性质(fungibility) 分为两种,

  • 同质化token,如Ether,我们在意的是我们的balance的大小
  • 非同质化token,如NFTs,我们在意的是我们有什么

每一种token的开发,都是一个链上的合约,那么如果要让合约和合约之间能够相互协作,就需要有统一的标准,下面要介绍的的就是一些标准。

ERC20

使用最广泛的 同质化token 标准,但是因为其设计比较简单会带来一些限制。

contract ERC20 is Context, IERC20, IERC20Metadata {}

  • Context <<Abstract»
    • _msgSender() => return msg.sender;
    • _msgData() => return msg.data;
  • IERC20Metadata <<Interface»
    • name() external view => 查看token的名字
    • symbol() external view => 查看token的代号
    • decimals() external view => 查看token单位
  • IERC20 <<Interface»
    • totalSupply() external view => 查看token总量
    • balanceOf() external view => 查看某个人的token持有量
    • allowance() external view => 查看A用户能代理B用户消费多少token,其实就是一个委托记录
    • approve() external =>发起token委托
    • transfer() external => 转正给别人
    • transferFrom() external => A用B的钱转给别人
    • event
      • Transfer(from, to, value)
      • Approval(owner, spender, value)

上面是ERC20依赖的类,接下来看看ERC20实现了哪些功能

  • ERC20
    • state variable
      • _totalSupply uint256
      • _name string
      • _symbol string _ _balances mapping(address=>uint256)
      • _allowances mapping(address=>mapping(address=>uint256))
    • constructor => 设置name和symbol
    • IERC20Metadata实现
      • name() => return _name
      • symbol() => return symbol
      • decimals() => return 18 (跟以太坊的Ether一样,1 ether = 10**18 wei)
    • IERC20实现
      • totalSupply() external view => return _totalSupply()
      • balanceOf() external view => return _balances[account]
      • allowance() external view => return _allowances[owner][spender]
      • approve() public => 需要注意在ERC20里是public的,而在IERC20里是external的。发起token委托,实际功能调用_approve(owner, spender, amount)
      • transfer() external => 实际功能调用_transfer(owner, to, amount)
      • transferFrom() external => 实际先后调用了两个内部函数
        • _spendAllowance(from, spender(msg.sender), amount)
        • _transfer(from, to, amount)
    • 自己的internal函数
      • _transfer() => 实际交易逻辑发生的地方,包含了条件判断和Transfe事件抛出。调用了两个内部函数
        • _beforeTokenTransfer()
        • _afterTokenTransfer()
      • _mint() => 增加_totalSupply,也就是发币的机制,不对外暴露,但是可以用一个external的函数把它包起来对外提供服务,但是需要注意设计控制规则。
      • _burn() => 燃烧token函数,某个账户可以决定将自己的token销毁,_totolSupply会相应减少
      • _approve() => 代币委托行为实际发生的地方,其实就是添加或者覆盖_allowances里面的数据,会抛出Approval事件
      • _spendAllowance => 其实就是检查以下委托金额够不够,然后从_allowances里面相对应的地方去扣掉
      • _beforeTokenTransfer => 在交易逻辑执行前触发,由子类自己去实现
      • _afterTokenTransfer => 在交易逻辑执行后触发,由子类自己去实现
    • 自己的函数
      • increaseAllowance() public => 其实就是增加_allowance[owner][spender]
      • decreaseAllowance() public => 其实就是减少_allowance[owner][spender]

我们以可以看到上面代码里面,针对allowances的操作中,owner在委托amount数量的token给spender的时候,并不会对owner的balance是否充足进行确认,而是在实际被委托人spender消费owner的token的时候,如果金额不够,_transfer里面会失败。 这样设计的原因,我想是因为即使owner把token委托给了spender,他自己依旧是可以花token的。如果要在确保allowance里面的委托金额一定要小于owner自己的balance金额的话,很难设计当owner花钱的时候,要怎么对allowance进行相应的改动。代码复杂度会变得很高,得不偿失。

IERC20主要围绕state variables中的balance和allowances两个板块进行的函数规划。而token的基础信息板块是在IERC20metadata接口中规划的。对于msg中的sender和data信息的访问,则封装到了抽象的context中。

总结下来ERC20中对同质化代币的功能实现在于:

  1. 对发行的token的基础信息_name,_symbol进行设定。如果要对默认的decimals进行设定则是通过覆写decimals()函数去进行的
  2. 发行币和销毁币,主要对_totalSupply进行操作
  3. 查账和转账,都是对_balance的操作
  4. token委托相关的操作,都是对_allowances的操作

ERC721

非同质化token 的解决方案,通常用于藏品和游戏领域。 ERC721涉及的合约有

  • Context
  • ERC165
  • IERC721
  • IERC721Metadata

上面的Context合约在ERC20中讲过,就是封装了msg.sender和msg.data。这里主要将后面几个

  • ERC165
    • supportInterface() public view
  • IERC721Metadata <<Interface»
    • name() external view => 返回这个token的名字
    • symbol() external view => 返回这个token的代号
    • tokenURI(tokenId) external view => 返回该token的URI,也就是所谓的资源定位符
  • IERC721 <<Interface»
    • event
      • Transfer(from, to, tokenId)
      • Approval(owner, approved, tokenId)
      • ApprovalForAll(owner, operator, approved)
    • balanceOf(owner) external view => 返回owner的所有tokens
    • ownerOf(tokenId) external view => 返回tokenowner的地址
    • safeTransferFrom(from, to, tokenId, data) => from转账给to,需要满足以下条件
      1. from不能是address0
      2. to不能是address0
      3. tokenfrom所有的,或者from所代理的
      4. to如果是合约,必须要实现{IERC721Receiver-onERC721Received}
    • safeTransferFrom(from, to, tokenId)
    • transferFrom(from, to, tokenId) => 和safe的区别就是,当to是一个智能合约时,这个智能合约不用实现 {IERC721Receiver-onERC721Received}
    • approve(to, tokenId) => 委托token
    • setApprovalForAll(operator, _approved) => 设置是否将所有的token都委托出去
    • getApproved(tokenId) => 返回这个token的代理人地址
    • isApprovedForAll(owner, operator) => 返回是否owner被operator全权代理

然后是ERC721的具体实现

  • ERC721
    • state variables
      • _name
      • _symbol
      • _owner mapping(uint256 => address) => tokenowner的映射,记录了token的所有权
      • _balances mapping(address => uint256) ownertoken count的映射,记录了owner拥有的token的数量
      • _tokenApprovals mapping(uint256 => address) tokenapproved的映射,记录了token委托
      • _operatorApprovals mapping(address => mapping(address => bool)) => 记录owneroperator的是否设置了全权委托
    • constructor => 初始化_name和_symbol
    • ERC165
      • supportsInterface(interfaceId) public view => 判断传入的interfaceId是否符合IERC721,IERC721Metadata以及ERC165的标准
    • IERC721Metadata
      • name()
      • symbol()
      • tokenURI() public view => return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "" 其中的 baseURI 由内部函数 _baseURI() 提供
    • IERC721
      • balanceOf(owner) public view => 访问_balance[owner]
      • ownerOf(tokenId) public view => 访问_owners[tokenId]
      • approve(to, tokenId) public => 进行单个token的委托授权,实际的执行逻辑在_approve()中
      • getApproved(tokenId) public view => 访问_tokenApprovals[tokenId]
      • setApprovalForAll(operator, approved) public => 设置全权委托
      • isApprovedForAll(owner, operator) public view => 查看全权委托设定
      • transferFrom(from, to, tokenId) public => token转让,如果接收方是合约,不用在意其是不是符合标准的合约
      • safeTransferFrom(from, to, tokenId, data) public => token转让,如果接收方是合约,需要验证在意其是不是符合标准的合约
      • 内部函数
        • _transfer(from, to, tokenId) =>
          1. _beforeTokenTransfer(from, to, tokenId)
          2. _approve(address(0), tokenId) => 把授权取消
          3. 修改_balance和_owner
          4. _afterTokenTransfer(from, to, tokenId)
        • _approve(to, tokenId) => 将token的权柄授权给某给人,主要是对_tokenApprovals变量进行修改,也会抛出Approval事件
        • _setApprovalForAll(owner, operator, approved) => _operatorApprovals[owner][operator] = approved 对记录全权委托的变量进行修改
        • _beforeTokenTransfer() => 空函数,ERC20的子类可以自己去实现
        • _afterTokenTransfer() => 空函数,ERC20的子类可以自己去实现
        • _safeTransfer(from, to, tokenId, data) => 实际就是用的_transfer(),只是多了一个安全检查 _checkOnERC721Received(from, to, tokenId, data)
        • _checkOnERC721Received(from, to, tokenId, data) => 主要是两个检查,先检查是不是合约地址,在检查合约是不是符合要求的 IERC721Receiver
        • _mint(to, tokenId) => 给地址 to 发放token,主要也是对 _balance_owners 进行修改
        • _safeMint(to, tokenId, data) => 和 safeTransfer 一样,也是加了一个 _checkOnERC721Received(address(0), to, tokenId, data) 检查
        • _burn(tokenId) => 将token销毁掉,也就是修改_balance和去_owners里面删掉对应的记录,也会抛出一个发向address(0)的Transfer事件。
        • _exists(tokenId) => 很简单的一个函数,就是去判断token在_owner里是否存在
        • _requireMinted(tokenId) => 其实就是调用的_exist(tokenId),只是如果返回是false的话,会抛出异常
        • _isApprovedOrOwner(spender, tokenId) => 其实就是判断spender是不是tokenId的主人,或者是不是tokenId的主人委托的人
        • _baseURI() => 返回空字符串,可以在子类中去重写这个函数以此来修改 baseURI 的值

ERC1155

一个 多token 的标准,允许一个合约包含多个 同质化token非同质化token。并且通过批量操作提高 gas 的效率。 除了提供 batch 的操作来提升 gas 的利用效率以外,本质上和ERC721差不太多,因为其是一个 多同质化代币 合约规范,如果我们把每一种同质化代币的数量都设置为1,那么这些各种类型的数量唯一的 同质化代币 也就构成了一个 非同质化代币 了。

contact ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI {}

和上面的一样来分析合约的组成

  • Context
  • ERC165
  • IERC1155MetadataURI
    • uri(id) external view => 返回token的uri
  • IERC1155
    • event
      • TransferBatch(operator, from, to, ids, values)
      • ApprovalForAll(account, operator, approved)
      • URI(value, id)
    • function
      • balanceOf(account, id) => 因为是多token合约,所以再查看账户的时候需要指定 id 查看的是哪一种token
      • balanceOfBatch(accounts, ids) => accounts数值和ids数组的长度必须一致,因为每一个 (account, id) 都是一次 balanceOf(account, id)
      • setApprovalForAll(operator, approved) => 和ERC721里面的 setApprovalForAll 差不多
      • isApprovedForAll(account, operator) => 和 setApprovalForAll 配套的,一个 一个
      • safeTransferFrom(from, to, id, amount, data)
      • safeBatchTransferFrom(from, to, ids, amounts, data) => 上面的batch版本,ids和amounts的长度要对应

最后是ERC1155

  • ERC1155
    • state variables
      • _balances mapping(uint256 => mapping(address => uint256))
      • _operatorApprovals mapping(address => mapping(address => bool))
      • _uri string
    • function
      • constructor(uri_) => 调用 _setURI 来设置 _uri
      • ERC165
        • supportsInterface(interfaceId) public view
      • IERC1155MetadataURI -uri(uint256) public view => 返回_uri
      • IERC1155
        • balanceOf(account, id) public view => 去_balances中查询
        • balanceOfBatch(accounts, ids) public view => 针对每一个 (account, id) 去调用 balanceOf
        • setApprovalForAll(operator, approved) public => 调用的 _setApprovalForAll(_msgSender(), operator, approved)
        • isApprovedForAll(account, operator) public view => 去_operatorApprovals里面查就行了
        • safeTransferFrom public => 实际的执行者是内部函数 _safeTransferFrom,只是在调用这个内部函数前先用require进行了资格验证
        • safeBatchTransferFrom public => 实际是 _safeBatchTransferFrom,同样在调用之前要对 _msgSender() 做资格验证
        • 内部函数
          • _safeTransferFrom(from, to, id, amount, data)
          • _safeBatchTransferFrom(from, to, ids, amounts, data)

          • _setURI(newuri)

          • _mint(to, id, amount, data)
          • _mintBatch(to, ids, amounts, data)

          • _burn(from, id, amount)
          • _burnBatch(from, ids, amounts)

          • _setApprovalForAll(owner, operator, approved)

          • _asSingletonArray(element)
          • _beforeTokenTransfer(operator, from, to, ids, amounts, data)
          • _afterTokenTransfer(operator, from, to, ids, amounts)
          • _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data)
          • _doSafeTransferAcceptanceCheck(operator, from, to, ids, amounts)

{ % if page.mermaid % } { % endif % }