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代币,可以简化处理代币的逻辑。