深入剖析ERC20
1. ERC20简介
ERC20是以太坊区块链创建的可替代的技术标准,可替代代币是可以与另一种代币进行交换的代币,故此ERC20代币是一种同质化代币。ERC20协议更像是一种规范,规范了在智能合约中实施代币的标准API,使得代币具有基本的转账功能,以便其他链上第三方可以使用。
ERC20接口:
pragma solidity ^0.8.20;
interface IERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
这是ERC20最基本的也是最重要的功能,凡是遵循ERC20标准的都需要实现该接口。这几个方法很简单,transferFrom()
函数还是要值得注意,该函数的使用方式是,from
需要提前为msg.sender
授权,即需要from
去亲自调用approve()
函数。只有 from
为msg.sender
授权之后,transferFrom()
函数才能够成功执行,这是在平时打CTF的时候经常容易忽视的操作,写完攻击逻辑之后,最后报错。。。才发现是在某些合约里的某些函数中的转账逻辑是transferFrom()
,由于没有授权导致的。
2. USDT的坑
2.1 USDT的问题所在
还有一个值得注意的点是,transfer()
和transferFrom()
都是有返回值的!!!!为什么主要,就是因为全球使用最广的稳定币的源码中transfer()
和transferFrom()
是没有返回值,这是一个坑!!!大坑!!!
可以到 浏览器看到Tether Token(USDT)的源码,可以看到这两个函数:
TetherToknen.sol:
function transfer(address _to, uint _value) whenNotPaused {
if (deprecated) {
return UpgradedStandardToken(upgradedAddress).transferByLegacy(msg.sender, _to, _value);
} else {
return super.transfer(_to, _value);
}
}
function transferFrom(address _from, address _to, uint _value) whenNotPaused {
if (deprecated) {
return UpgradedStandardToken(upgradedAddress).transferFromByLegacy(msg.sender, _from, _to, _value);
} else {
return super.transferFrom(_from, _to, _value);
}
}
这里两个函数是没有返回值,即使它调用的是父类的函数,在没有看到父类的具体函数实现,姑且说它的父类是有返回值的,但是子类中的函数是没有返回值,这是在Remix中编译不过去的,举例:
可以看到编译通过了。下面是子合约函数中没有返回值的:
所以不用去父类中找函数都可以知道父类的函数也是没有返回值的,不信就去验证一下:
BasicToken.sol::transfer()
function transfer(address _to, uint _value) onlyPayloadSize(2 * 32) {
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
uint sendAmount = _value.sub(fee);
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(sendAmount);
balances[owner] = balances[owner].add(fee);
Transfer(msg.sender, _to, sendAmount);
Transfer(msg.sender, owner, fee);
}
StandardToken.sol::transferFrom()
function transferFrom(address _from, address _to, uint _value) onlyPayloadSize(3 * 32) {
var _allowance = allowed[_from][msg.sender];
uint fee = (_value.mul(basisPointsRate)).div(10000);
if (fee > maximumFee) {
fee = maximumFee;
}
uint sendAmount = _value.sub(fee);
balances[_to] = balances[_to].add(sendAmount);
balances[owner] = balances[owner].add(fee);
balances[_from] = balances[_from].sub(_value);
if (_allowance < MAX_UINT) {
allowed[_from][msg.sender] = _allowance.sub(_value);
}
Transfer(_from, _to, sendAmount);
Transfer(_from, owner, fee);
}
2.2 复现DOS异常
本地复现。
本地部署USDT,地址为:0x5FbDB2315678afecb367f032d93F642f64180aa3
。
再部署一个转移代币的 TokenTransfer.sol
,地址为:0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
interface IERC20_USDT {
function transfer(address, uint256) external;
function transferFrom(address, address, uint256) external;
}
interface IERC20_stad {
function transfer(address, uint256) external returns(bool);
function transferFrom(address, address, uint256) external returns(bool);
}
// USDT: 0x5FbDB2315678afecb367f032d93F642f64180aa3
contract TokenTransfer {
IERC20_USDT USDT;
IERC20_stad TOKEN;
constructor(address _usdt) {
USDT = IERC20_USDT(_usdt);
TOKEN = IERC20_stad(_usdt);
}
function test_TransferWithOutReturnValue() external {
USDT.transfer(msg.sender, 10);
}
function test_TransferWithReturnValue() external {
TOKEN.transfer(msg.sender, 10);
}
function test_TransferFromWithOutReturnValue() external {
USDT.transferFrom(msg.sender, 0x71bE63f3384f5fb98995898A86B02Fb2426c5788, 1);
}
function test_TransferFromWithReturnValue() external {
TOKEN.transferFrom(msg.sender, 0x71bE63f3384f5fb98995898A86B02Fb2426c5788, 1);
}
}
该合约主要是测试transfer
和transferFrom
函数,定义两个接口,一个接口中有返回值,一个没有,模拟将USDT转入一个遵循ERC20标准的合约,看看是否能将转入的ERC20 Token转出。
可以看到调用test_TransferWithReturnValue
,交易会被revert,而调用test_TransferWithOutReturnValue
,交易则正常运行。
同理test_TransferFromWithReturnValue
操作也是会被revert。这也就说明了,用标准的ERC20接口转换USDT,就会造成资金永久封锁的情况。当然,为了解决这个问题,可以将合约中的ERC20接口中的那两个函数的返回值移除,即:
interface IERC20_USDT {
function transfer(address, uint256) external;
function transferFrom(address, address, uint256) external;
}
3.SafeERC20
3.1 兼容USDT
当然还有其他的解决方式,也是最常用一种解决方式,那就是使用SafeERC20
库。
using SafeERC20 for IERC20;
SafeERC20做了兼容严格遵守与不严格遵守ERC20协议标准的代币,兼容的原理如下:
function _callOptionalReturn(IERC20 token, bytes memory data) private {
bytes memory returndata = address(token).functionCall(data);
if (returndata.length != 0 && !abi.decode(returndata, (bool))) {
revert SafeERC20FailedOperation(address(token));
}
}
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
简单来说就是,通过Address.sol的低级调用方式,可以检测调用是否成功,且检测是否有返回值。当调用USDT的transfer函数时,如果执行成功,且return data = 0x,那么函数便可以执行,即跳过if的检测。
同理 transferFrom函数也是如此。
4. ERC20系列数字货币
4.1 (Tether USD)USDT
不遵循标准的ERC20协议,需要操作该代币时,建议使用SafeERC20.sol。
- 发行公司: 由 Tether Limited 发行,成立于 2014 年的香港公司。
- 市值: USDT 是市值最大的稳定币之一,截至 2022 年 7 月,市值超过 650 亿美元,占稳定币市场 50% 以上的份额。
- 挂钩: USDT 与美元 1:1 挂钩,据称有大量抵押品储备支持,包括现金、商业票据和商品。
- 历史: 最早作为 RealCoin 推出,后来于 2014 年 11 月更名为 Tether。然而,Tether 并不是没有争议的,曾因多次争议而备受关注,包括被指控误导投资者和缺乏对储备的透明度。
4.2 (USD Coin)USDC
遵循标准的ERC20协议。源码链接:link。
- 发行公司: 由 Circle、Coinbase 和其他金融科技公司共同创立的财团 Center 是 USDC 的发行人。
- 市值: USDC 是按市值计算的第二大稳定币,截至 2022 年 7 月,市值超过 540 亿美元。
- 挂钩: 每个 USDC 与美元 1:1 挂钩,并由现金和美元等值资产支持。
- 安全性: USDC 被认为是一种更安全的价值储存手段,因为它有现金和现金等价物支持,而且受到美国监管。
这两种稳定币的比较:
- 流动性: USDT 的交易量更大,更广泛可用,但USDC 的交易量较低。
- 透明度: USDC 在透明度和监管方面表现较好,而 USDT 面临一些争议。
- 用途: USDT 在期货交易中很受欢迎,提供了更高的收益,而 USDC 是去中心化金融 (DeFi) 领域的首选,因为它被认为更安全。
4.3 其他
- (Shiba Inu)SHIB:SHIB是一种基于以太坊的山寨币,算是狗狗币的一种替代品,遵循标准的ERC20协议。
- Binance USD(BUSD):遵循标准的ERC20协议。
- DAI Stablecoin (DAI):遵循标准的ERC20协议。
- HEX (HEX):遵循标准的ERC20协议。
5. ERC20 extensions
来自 OpenZeppelin,链接。ERC4626单独xue'xi
5.1 ERC1363.sol
这个拓展协议实现的功能是,用户在执行transfer、transferFrom和approve操作的时候,可以传入calldata,完成一些函数调用,或者是参数的传递,实现逻辑类似ERC721的
checkOnERC721Received()
。它的
_checkOnTransferReceived()
和_checkOnApprovalReceived()
函数会分别去调用IERC1363Receiver(to).onTransferReceived
,IERC1363Spender(spender).onApprovalReceived
,这里会埋下被重入的安全隐患,在实用这个协议的时候需要注意这点。
5.2 ERC20Burnable.sol
该合约提供了销币功能。
- burn(uint256 value):销毁msg.sender的value个代币。
- burnFrom(address account, uint256 value) :msg.sender销毁account的value个代币,前提是account给予msg.sender权限。
5.3 ERC20Capped.sol
我的理解是,给ERC20代币的的totalsupply盖帽子,也就是设置上限,设置某个ERC20代币的发行量不能超过cap。限制的逻辑在这里(from==0,则被检测为铸币操作):
if (from == address(0)) { uint256 maxSupply = cap(); uint256 supply = totalSupply(); if (supply > maxSupply) { revert ERC20ExceededCap(supply, maxSupply); } }
5.4 ERC20FlashMint.sol
该合约提供了一个借贷功能,只能借该合约生成的代币,且最大接待额为:
token == address(this) ? type(uint256).max - totalSupply() : 0
;还需要支付fee,这个借贷函数不需要主动还款,因为ta采用的是burn操作,直接将你手中借来的token全部销毁。但是这不影响执行某些重入攻击,比如可以借钱去执行套利操作这类的,这是借贷函数的“通病”吧。
5.5 ERC20Pausable.sol
该合约提供了一个紧急停止功能,在_update()函数加上whenNotPaused修饰符,当所有者暂停合约时,该合约生成的代币将不能执行一系列操作,如transfer、mint、transferFrom等。
5.6 ERC20Permit.sol
该合约提供了一个新的授权操作,permit()函数的作用便是完成 owner对spender的授权,个人理解是,因为原ERC20中的approve函数必须是owner亲自去调用才能完成授权,这比较麻烦owner,而permit则是可以通过owner提供的签名来验证,并执行owner对spender的授权操作。
举个例子来对比,以开门为例:
- 原ERC20的授权方式:owner想让spender进屋拿东西,而owner需要亲自开门让他进去
- ERC20Permit的授权方式:同样的示例,owner可以直接把家门口的钥匙给spender,让spender直接去开门拿东西就好了。
5.7 ERC20Votes.sol
该合约支持类似Compound的投票和授权。
5.8 ERC20Wrapper.sol
该合约支持代币的包装,用户可以存入和取出
_underlying
代币,存入多少_underlying代币,就可以铸造多少ERC20Wrapper代币,同理取出多少 _underlying代币,便会销毁多少ERC20Wrapper代币代币。还提供了一个 _recover函数,该函数的作用是将错误转入该合约的 _underlying代币数量全部铸造为ERC20Wrapper代币,我对“错误转入”的理解是,没有通过
depositFor()
函数转入 _underlying代币。
最后,码字不易,点个赞呗~🤪
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。