1.ERC20简介ERC20是以太坊区块链创建的可替代的技术标准,可替代代币是可以与另一种代币进行交换的代币,故此ERC20代币是一种同质化代币。ERC20协议更像是一种规范,规范了在智能合约中实施代币的标准API,使得代币具有基本的转账功能,以便其他链上第三方可以使用。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()
,由于没有授权导致的。
还有一个值得注意的点是,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);
}
本地复现。
本地部署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;
}
当然还有其他的解决方式,也是最常用一种解决方式,那就是使用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函数也是如此。
不遵循标准的ERC20协议,需要操作该代币时,建议使用SafeERC20.sol。
遵循标准的ERC20协议。源码链接:link。
这两种稳定币的比较:
来自 OpenZeppelin,链接。ERC4626单独xue'xi
这个拓展协议实现的功能是,用户在执行transfer、transferFrom和approve操作的时候,可以传入calldata,完成一些函数调用,或者是参数的传递,实现逻辑类似ERC721的
checkOnERC721Received()
。它的
_checkOnTransferReceived()
和_checkOnApprovalReceived()
函数会分别去调用IERC1363Receiver(to).onTransferReceived
,IERC1363Spender(spender).onApprovalReceived
,这里会埋下被重入的安全隐患,在实用这个协议的时候需要注意这点。
该合约提供了销币功能。
- burn(uint256 value):销毁msg.sender的value个代币。
- burnFrom(address account, uint256 value) :msg.sender销毁account的value个代币,前提是account给予msg.sender权限。
我的理解是,给ERC20代币的的totalsupply盖帽子,也就是设置上限,设置某个ERC20代币的发行量不能超过cap。限制的逻辑在这里(from==0,则被检测为铸币操作):
if (from == address(0)) { uint256 maxSupply = cap(); uint256 supply = totalSupply(); if (supply > maxSupply) { revert ERC20ExceededCap(supply, maxSupply); } }
该合约提供了一个借贷功能,只能借该合约生成的代币,且最大接待额为:
token == address(this) ? type(uint256).max - totalSupply() : 0
;还需要支付fee,这个借贷函数不需要主动还款,因为ta采用的是burn操作,直接将你手中借来的token全部销毁。但是这不影响执行某些重入攻击,比如可以借钱去执行套利操作这类的,这是借贷函数的“通病”吧。
该合约提供了一个紧急停止功能,在_update()函数加上whenNotPaused修饰符,当所有者暂停合约时,该合约生成的代币将不能执行一系列操作,如transfer、mint、transferFrom等。
该合约提供了一个新的授权操作,permit()函数的作用便是完成 owner对spender的授权,个人理解是,因为原ERC20中的approve函数必须是owner亲自去调用才能完成授权,这比较麻烦owner,而permit则是可以通过owner提供的签名来验证,并执行owner对spender的授权操作。
举个例子来对比,以开门为例:
- 原ERC20的授权方式:owner想让spender进屋拿东西,而owner需要亲自开门让他进去
- ERC20Permit的授权方式:同样的示例,owner可以直接把家门口的钥匙给spender,让spender直接去开门拿东西就好了。
该合约支持类似Compound的投票和授权。
该合约支持代币的包装,用户可以存入和取出
_underlying
代币,存入多少_underlying代币,就可以铸造多少ERC20Wrapper代币,同理取出多少 _underlying代币,便会销毁多少ERC20Wrapper代币代币。还提供了一个 _recover函数,该函数的作用是将错误转入该合约的 _underlying代币数量全部铸造为ERC20Wrapper代币,我对“错误转入”的理解是,没有通过
depositFor()
函数转入 _underlying代币。
最后,码字不易,点个赞呗~🤪
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!