一、库合约的运用

官网示例中给出了一个自界说的library,然后直接在合约中引证此library,这个自界说的Balances library完成的是转账功用,在转账之前对金额做校验,保证转出账户不会为负,转账金额不会为负。

1. 运用示例

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0  <0.9.0;
library Balances {
    function move(mapping(address => uint256) storage balances, address from, address to, uint amount) internal {
        require(balances[from] >= amount);
        require(balances[to] + amount >= balances[to]);
        balances[from] -= amount;
        balances[to] += amount;
    }
}
contract Token {
    mapping(address => uint256) balances;
    using Balances for *;   // 引入库
    mapping(address => mapping (address => uint256)) allowed;
    event Transfer(address from, address to, uint amount);
    event Approval(address owner, address spender, uint amount);
    function transfer(address to, uint amount) external returns (bool success) {
        balances.move(msg.sender, to, amount);
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function transferFrom(address from, address to, uint amount) external returns (bool success) {
        require(allowed[from][msg.sender] >= amount);
        allowed[from][msg.sender] -= amount;
        balances.move(from, to, amount);   // 运用了库办法
        emit Transfer(from, to, amount);
        return true;
    }
    function approve(address spender, uint tokens) external returns (bool success) {
        require(allowed[msg.sender][spender] == 0, "");
        allowed[msg.sender][spender] = tokens;
        emit Approval(msg.sender, spender, tokens);
        return true;
    }
    function balanceOf(address tokenOwner) external view returns (uint balance) {
        return balances[tokenOwner];
    }
}

仍然按照老的习惯,以测试的方式来调用合约。

2.用例规划

规划下面一组用例,用以调用上述合约,观察库合约中的代码是否生效

  1. A经过transfer给B转账3个以太,预期成果:转账成功,A的账户余额削减3个以太,B余额增加3个以太
  2. A经过transfer给B转账0个以太,预期成果:转账成功,A和B账户余额不变
  3. A经过transfer给B转账value为-2,预期成果:转账失败,A和B账户余额不变
  4. A给B授权3个以太,B经过transferFrom从A钱包中提现3个以太,预期成果:转账成功,A的账户余额削减3个以太,B余额增加3个以太
  5. A给B授权0个以太,B经过transferFrom从A钱包中提现0个以太,预期成果:转账成功,A和B账户余额不变
  6. A经过transferFrom给B转账value为-2,预期成果:转账失败,A和B账户余额不变

(因为transfer和transferFrom中界说的金额amount为uint类型,这里无法传入负数。所以上述用例里的第3,6个用例在不修正合约的情况下无法完成。)

3.完成说明

(1)因为在示例合约中,没有界说结构函数,无法给balances的任意成员赋值,因而增加了如下结构函数,便于运用:

uint256 totalSupply_ = 100 ether;
constructor() {
    balances[msg.sender] = totalSupply_;
    }

(2)因为library库中对转账金额判别后,没有给出详细的错误提示,不便于判别详细场景,因而添加revert的信息:

require(balances[from] >= amount, "balance not enough");
require(balances[to] + amount >= balances[to],"transfer amount is negative");

(3)因为transfer和transferFrom中界说的金额amount为uint类型,这里无法传入负数。所以上述用例里的第3,6个用例在不修正合约的情况下无法完成。

实战练习Solidity(5)——库合约的使用

二、合约继承

1.ERC-20协议

不难看出上述示例合约想要完成的功用就是同质化代币转账操作,在ERC-20规范合约协议中现已一致界说好了发行同质化代币需求遵循的开发约好。在实践开发过程中,开发者不需求每次都自己从头规划一套完好的发行代币的合约,而是可以经过引入OpenZeppelin中的ERC-20合约后再自界说开发扩展额外需求的功用。ERC-20规范大大削减了开发者的工作量,并且一致了代币发行规范。


interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

其间,有三个只读函数:

  • totalSupply()是一个外部只读函数,函数的调用不会改动合约的状况,它回来的是代币发行总量。
  • balanceOf()也是一个外部只读函数,函数的调用不会改动合约的状况,它回来的是传入参数地址account中的账户余额。
  • allowance()回来的是,owner允许spender代表自己可支配的代币数量

别的三个函数的调用将会改动合约的变量状况:

  • transfer():此函数会将amount金额的代币从调用者账户(msg.sender)搬运给接受者账户(recipient),搬运成功回来true,并且会发送一个Transfer事情
  • approve(): 此函数将必定金额amount的代币支配权由调用者(msg.sender)分配给spender,分配成功则回来true,并且会发送一个Approval事情
  • transferFrom():此函数使用授权机制,将amount金额的代币从sender账户搬运到recipient账户,搬运成功则回来true,并且会发送一个Transfer事情

事情的界说:

  • Transfer事情:当代币在账户之间进行搬运时,就会触发此事情。假如一种新代币在mint时,此事情的from address将是0地址0x00..0000,假如毁掉代币时,此事情的接纳地址也是0地址0x00..0000
  • Approval事情:当发生代币授权行为时,会触发此事情。

2.根据ERC-20的代币合约的示例

pragma solidity ^0.8.0;
interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function allowance(address owner, address spender) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract ERC20Basic is IERC20 {
    string public constant name = "ERC20Basic";
    string public constant symbol = "ERC";
    uint8 public constant decimals = 18;
    mapping(address => uint256) balances;
    mapping(address => mapping (address => uint256)) allowed;
    uint256 totalSupply_ = 10 ether;
   constructor() {
    balances[msg.sender] = totalSupply_;
    }
    function totalSupply() public override view returns (uint256) {
    return totalSupply_;
    }
    function balanceOf(address tokenOwner) public override view returns (uint256) {
        return balances[tokenOwner];
    }
    function transfer(address receiver, uint256 numTokens) public override returns (bool) {
        require(numTokens <= balances[msg.sender]);
        balances[msg.sender] = balances[msg.sender]-numTokens;
        balances[receiver] = balances[receiver]+numTokens;
        emit Transfer(msg.sender, receiver, numTokens);
        return true;
    }
    function approve(address delegate, uint256 numTokens) public override returns (bool) {
        allowed[msg.sender][delegate] = numTokens;
        emit Approval(msg.sender, delegate, numTokens);
        return true;
    }
    function allowance(address owner, address delegate) public override view returns (uint) {
        return allowed[owner][delegate];
    }
    function transferFrom(address owner, address buyer, uint256 numTokens) public override returns (bool) {
        require(numTokens <= balances[owner]);
        require(numTokens <= allowed[owner][msg.sender]);
        balances[owner] = balances[owner]-numTokens;
        allowed[owner][msg.sender] = allowed[owner][msg.sender]-numTokens;
        balances[buyer] = balances[buyer]+numTokens;
        emit Transfer(owner, buyer, numTokens);
        return true;
    }
}

另一个被广泛选用的是OZ(OpenZeppelin)对ERC-20协议的完成,其源码地址:github.com/OpenZeppeli…,当开发者自己要发一套代币时,可以选择直接引证OZ库中的合约,运用办法如下:

pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20, Ownable {
    constructor() ERC20("Token demo", "TESTER") {}
    function mint(address account, uint256 amount) public onlyOwner {
        _mint(account, amount);
    }
// add other functions
}

3.学习学习

在OZ完成的ERC20合约中,有一些值得学习学习的地方:

(1)继承性

以transfer函数为例,加了virtual关键字,这样便利子合约可以继承后重写transfer。真正完成token transfer功用的是_transfer函数,可以看到在token搬运前后,各有一个virtual函数,_beforeTokenTransfer和_afterTokenTransfer,这两个函数在OZ的合约中没有详细的完成,其实就是为了便利开发者引入库合约后,自界说开发额外的功用,在transfer通用逻辑处预设了hooks。

function transfer(address to, uint256 amount) public virtual override returns (bool) {
        address owner = _msgSender();
        _transfer(owner, to, amount);
        return true;
    }
function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");
        _beforeTokenTransfer(from, to, amount);
        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        unchecked {
            _balances[from] = fromBalance - amount;
            // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by
            // decrementing then incrementing.
            _balances[to] += amount;
        }
        emit Transfer(from, to, amount);
        _afterTokenTransfer(from, to, amount);
    }
function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}
function _afterTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {}

别的_msgSender()实践上是import “../../utils/Context.sol“中的函数,回来的值是msg.sender

abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }
    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

有一个疑问是:为什么用Context.sol中的_msgSender()办法,而不是直接用msg.sender呢?

在网上查找了一下答案,看到一种解释——这么做也是为了便利拓宽,完成自界说功用而选用了virtual的_msgSender(),假如不需求对_msgSender()进行自界说开发,那用msg.sender就可以了。

(2)Gas优化

在上面transfer代码中,在对from账户进行余额扣减,对to账户进行余额增加时,OZ的作法是,先判别from账户的余额是大于转出金额的,然后用一段unchecked代码进行余额变更。这样的写法会越过整数溢出的检查从而节约必定的gas费用,但需求注意的是必定要先判别余额然后再运用unchecked代码段。

(3)安全规划

在ERC-20协议中有提示approve函数存在安全隐患,进犯者可以经过规划好的进犯序列,获得比授权额度更多的代币金额。要了解这个规划缝隙概况可以检查:docs.google.com/document/d/…

OZ在遵循ERC20协议的前提下,在安全方面做了一些改善,增加了两个在ERC20规范协议里没有界说的函数:

function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        address owner = _msgSender();
        _approve(owner, spender, allowance(owner, spender) + addedValue);
        return true;
    }
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        address owner = _msgSender();
        uint256 currentAllowance = allowance(owner, spender);
        require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
        unchecked {
            _approve(owner, spender, currentAllowance - subtractedValue);
        }
        return true;
    }

这两个办法不能彻底躲避掉ERC20协议自带的“抢跑”问题,可是能从必定程度上缓解此问题。比方下面的场景: 这两个办法不能彻底躲避掉ERC20协议自带的“抢跑”问题,可是能从必定程度上缓解此问题。比方下面的场景:

  • A授权给B 100个token
  • A改动想法,只想给B 30个token

在原ERC20协议里,A需求发两次买卖:

  • approve(b.address, 100)
  • approve(b.address,30)

假如B在A的两次买卖之间进行一次提现100的操作:transferFrom(a.address,b.address,100),等候A的第二个买卖恳求执行成功后再提现30个token,那么实践上B从A那里提走的是130个token,而这明显不是A的志愿。

假如选用decreaseAllowance办法会怎样呢?A在这种情况下也需求发送两个买卖恳求:

  • approve(b.address, 100)
  • decreaseAllowance(b.address,70)

B还是在A的两次买卖之间进行提现100的操作,可以提现成功。此时轮到A的第二个买卖进行处理,会发现currentAllowance现已变成0,无法再为B更改可提现金额,这种情况下,B最多只能从A那里提走100个token,对A来说削减了一些损失。

(4)拓宽规划

OZ除了上述对ERC20规范进行了完成和拓宽之外,还增加了一些有代表性的拓宽功用,比方可毁掉、可暂停等功用,详细可参阅官方文档(docs.openzeppelin.com/contracts/4…)

三、小结

在开发智能合约的时候,善用合约库,以及学习和使用像OpenZeppelin这样的经过充分验证的开源智能合约库,可以协助咱们更好地进行功用的完成,高效完成需求的开发,同时开源智能合约库的规划也有很多奇妙之处,值得学习学习,协助提高本身的合约规划开发能力。