liujie
liujie
Published on 2024-09-02 / 9 Visits
0
0

以太坊-智能合约-常用合约

https://liaoxuefeng.com/books/blockchain/ethereum/smart-contract/contract-list/index.html

本节我们介绍以太坊上常见的几种合约:

  • ERC-20:以太坊标准代币合约;

  • Wrapped Ether:将以太坊封装为ERC20的合约;

  • ERC-721:以太坊NFT标准合约;

  • ERC-1155:相同NFT允许多个持有者的合约。

ERC-20

ERC-20是以太坊定义的一个合约接口规范,符合该规范的合约被称为以太坊代币。

一个ERC-20合约通过mapping(address => uint256)存储一个地址对应的余额:

contract MyERC20 {
    mapping(address => uint256) public balanceOf;
}

如果要在两个地址间转账,实际上就是对balanceOf这个mapping的对应的kv进行加减操作:

contract MyERC20 {
    mapping(address => uint256) public balanceOf;

    function transfer(address recipient, uint256 amount) public returns (bool) {
        // 不允许转账给0地址:
        require(recipient != address(0), "ERC20: transfer to the zero address");
        // sender的余额必须大于或等于转账额度:
        require(balanceOf[msg.sender] >= amount, "ERC20: transfer amount exceeds balance");
        // 更新sender转账后的额度:
        balanceOf[msg.sender] -= amount;
        // 更新recipient转账后的额度:
        balanceOf[recipient] += amount;
        // 写入日志:
        emit Transfer(sender, recipient, amount);
        return true;
    }
}

安全性

早期ERC20转账最容易出现的安全漏洞是加减导致的溢出,即两个超大数相加溢出,或者减法得到了负数导致结果错误。从Solidity 0.8版本开始,编译器默认就会检查运算溢出,因此,不要使用早期的Solidity编译即可避免溢出问题。

没有正确实现transfer()函数会导致交易成功,却没有任何转账发生,此时外部程序容易误认为已成功,导致假充值:

function transfer(address recipient, uint256 amount) public returns (bool) {
    if (balanceOf[msg.sender] >= amount) {
        balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(sender, recipient, amount);
        return true;
    } else {
        return false;
    }
}

实际上transfer()函数返回bool毫无意义,因为条件不满足必须抛出异常回滚交易,这是ERC20接口定义冗余导致部分开发者未能遵守规范导致的。

ERC-20另一个严重的安全性问题来源于重入攻击:

function transfer(address recipient, uint256 amount) public returns (bool) {
    require(recipient != address(0), "ERC20: transfer to the zero address");
    uint256 senderBalance = balanceOf[msg.sender];
    require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
    // 此处调用另一个回调:
    callback(msg.sender);
    // 更新转账后的额度:
    balanceOf[msg.sender] = senderBalance - amount;
    balanceOf[recipient] += amount;
    emit Transfer(sender, recipient, amount);
    return true;
}

先回调再更新的方式会导致重入攻击,即如果callback()调用了外部合约,外部合约回调transfer(),会导致重复转账。防止重入攻击的方法是一定要在校验通过后立刻更新数据,不要在校验-更新中插入任何可能执行外部代码的逻辑。

Wrapped Ether

如果一个合约既支持ETH付款,也支持ERC-20代币付款,我们会发现这两种付款方式处理逻辑是不一样的:

contract Shop {
    function pay(uint productId) public payable {
        // pay ETH...
    }
    function pay(uint productId, address erc, uint256 amount) public {
        // pay ERC...
    }
}

两种逻辑混在一起用,代码就会复杂,就容易出问题。

一个简单的解决方法是将ETH也变成一种代币,可以用一个简单的WETH合约实现:

contract WETH {
    string public name     = "Wrapped Ether";
    string public symbol   = "WETH";
    uint8  public decimals = 18;

    mapping (address => uint) public  balanceOf;

    function deposit() public payable {
        balanceOf[msg.sender] += msg.value;
    }

    function withdraw(uint wad) public {
        require(balanceOf[msg.sender] >= wad);
        balanceOf[msg.sender] -= wad;
        msg.sender.transfer(wad);
    }

    ...
}

这样,处理ETH付款就可以简单地将它变成WETH,然后走同一种逻辑:

contract Shop {
    function pay(uint productId) public payable {
        // ETH -> WETH:
        WETH.deposit.value(msg.value)();
        _pay(productId, address(this), WETH.address, msg.value);
    }

    function pay(uint productId, address erc, uint256 amount) public {
        _pay(productId, msg.sender, erc, amount);
    }

    function _pay(uint productId, address sender, address erc, uint256 amount) private {
        // pay ERC...
    }
}

不需要自己实现WETH,因为以太坊主网已经有一个通用的WETH

小结

通过WETH,将ETH变成ERC20代币,可以简化处理代币的逻辑。


Comment