前言
我们在第一次使用Uniswap这一类的DEX的时候,在swap之前,会需要进行approve的操作。这让人很疑惑,心里发问,我直接转账不就完了吗?授权啥?
今天,我们来研究下,为什么swap之前需要approve
协议
首先,我们以Ethereum为例,转账的都是ERC-20代币,那么我们就来直接看ERC-20的协议标准
相关的方法事件有:
function balanceOf(address _owner) : //查询余额
function transfer(address _to, uint256 _value) //转账
function transferFrom(address _from, address _to, uint256 _value) // 转账
function approve(address _spender, uint256 _value) // 授权
function allowance(address _owner, address _spender) // 查询授权额度
event Transfer(address indexed _from, address indexed _to, uint256 _value) // Transfer事件
event Approval(address indexed _owner, address indexed _spender, uint256 _value) //Approval事件
代码
然后,我门直接上代码,看 openzeppelin的实现
首先是全局变量和allowance和balanceOf方法
// 首先是两个全局变量map
mapping (address => uint256) private _balances; // 存储地址address的余额
mapping (address => mapping (address => uint256)) private _allowed;// 存储地址address1对address2的授权额度
// allowance方法
function allowance(
address owner,
address spender
)
public
view
returns (uint256)
{
return _allowed[owner][spender]; // 返回_allowed 这个map中存储的:ownder对spender的授权额度
}
// balanceOf方法
function balanceOf(address owner) public view returns (uint256) {
return _balances[owner]; //返回_balances 这个map中存储的:ownder的余额
}
balanceOf方法,就是接收一个账户地址,返回该地址中这个ERC-20 token的余额
allowance方法,有两个参数,owner和spender,返回owner对spender的针对这个ERC-20 token的授权额度
然后是涉及到转账的方法,分别是transfer和transferFrom
// transfer方法
function transfer(address to, uint256 value) public returns (bool) {
require(value <= _balances[msg.sender]); // transfer的数量value要 <= msg.sender的余额
require(to != address(0)); // 接收方to地址不能为空
_balances[msg.sender] = _balances[msg.sender].sub(value); // msg.sender的余额扣除transfer的数量value
_balances[to] = _balances[to].add(value); // 接收方to地址的余额增加transfer的数量value
emit Transfer(msg.sender, to, value); // 触发Transfer事件
return true;
}
// transferFrom方法
function transferFrom(
address from,
address to,
uint256 value
)
public
returns (bool)
{
require(value <= _balances[from]); // transfer的数量value要 <= 发送方from的余额
require(value <= _allowed[from][msg.sender]); // transfer的数量value要 <= 发送方from对msg.sender的授权额度
require(to != address(0)); //接收方to地址不为空
_balances[from] = _balances[from].sub(value); //发送方from的余额扣除transfer的数量value
_balances[to] = _balances[to].add(value); //接收方to的余额增加transfer的数量value
_allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value); // 发送方from对msg.sender的授权额度扣除transfer的数量value
emit Transfer(from, to, value); // 触发Transfer事件
return true;
}
transfer方法,就是向to地址转账value个token,前提是发送方的余额要大于转账个数value
transferFrom方法,就是从from地址向to地址转账value个token,前提是发送方from的余额要大于转账个数value,同时转账的个数value要小于发送方from对msg.sender的授权额度。
大家可能会有疑问,msg.sender不就是from吗?其实不是,钱是从from这里出去到to这里的,但是发送这个动作并不是from做出的,而是一个第三方中间人(通常是合约)来做的。可以这么理解,from事先给了msg.sender自己金库的钥匙,允许msg.sender可以去from的金库里取钱,transferFrom这个动作其实就是msg.sender从from的金库里取钱发送给了to。
from把自己的金库钥匙给msg.sender这个动作,就是授权
function approve(address spender, uint256 value) public returns (bool) { //接受两个参数,spender(被授权者)、value(授权额度)
require(spender != address(0)); // 被授权者的地址不为空
_allowed[msg.sender][spender] = value; //msg.sender对spender授权value额度
emit Approval(msg.sender, spender, value); //触发Approval事件
return true;
}
上面的msg.sender就是当前调用ERC-20合约方法的人,这个人可以是一个普通账户,也可以是一个合约地址。
总结
一般来说,transfer方法是给普通账户用的。假设当a要向一个目标地址b转账某个ERC-20代币的时候,只需要调用这个ERC-20代币的transfer方法即可,此时msg.sender,也就是方法的调用者,就是这个普通账户a。
与之对应的,transferFrom方法一般是给合约用的。首先a授权给合约b转走token的权利,a授权b的时候,msg.sender是方法approve的调用者,也就是a。
然后合约b会调用transferFrom给c转账,这个时候msg.sender就是transferFrom的调用者,也就是合约b。
授权和转账这两步,都是需要收取gas fee的
为什么合约需要这么做呢?一般情况下,a调用transfer方法给b转账1个usdt,然后就结束了。
但是如果在DEX的场景下,a想要把1个usdt换成1个usdc。在这种情况下,a在向swap合约b转账1个usdt之后,b并不会得到任何的通知(因为ERC-20标准并没有规定一笔转账会通知到任何人),所以b并不会及时地给a转账1个usdc。
这个时候就需要transferFrom登场了,a先授权给合约b转走自己usdt的权利。然后a再去调用合约b的转账方法,合约b的转账方法会调用usdt的transferFrom方法直接从a那里拿到1个usdt,只要transferFrom方法成功,合约b就知道自己一定从a那里收到了1个usdt,所以合约b就可以给a转账1个usdc
前端实现
合约逻辑清楚了,其实前端的实现就很简单了,这里贴一些伪代码吧
首先声明一些变量
// 连接钱包地址 from
// 转账到的地址 to
// 调用的swap合约 SWAP
// 转账的token USDT,它的合约地址 U
// 转账USDT的数量 amount
// 调用U的合约方法allowance,判断allowance的返回值是否为0;如果不为0则直接调用SWAP合约的swap方法转账;如果allowance为0,则调用U的approve方法给SWAP合约授权,再调用SWAP合约的转账方法
const allowed = allowance(from,SWAP)
if(allowed< amount){ //这里一般是写allowed === 0,因为一般授权的都是最大数量,即时这个数量会随着每次transfer而减少,但是也几乎不可能出现allowed<amount的情况
approve(SWAP,最大数量)
}else{
transfer(from,to,amount)
}
// 当然,SWAP合约的转账方法,本质上还是调用U的transferFrom方法,并在成功后会让to给from转账,其实是两笔转账(如果有流动性池,那可能是多笔)
引申问题
既然我们可以给一个合约授权,让他可以有转走我们的token的权利。那么我们可以取消授权吗?
答案是可以,我们只需要再次调用approve方法,把approve的数量设为0即可。一些插件钱包已经实现了这个功能。
ETH虽然是ethereum的原生代币,但是ETH并不是ERC-20代币,那么我们该怎么在ethereum上转账ETH呢?
答案是直接转即可,不需要授权。
假如想要实现像DEX这样的使用场景,那么只有两种办法,一种就是用WETH(ERC-20),另一种就是合约做兼容,专门处理ETH的SWAP,普遍来说就是再写一个合约方法,SWAP、SWAPETH。
references
what-is-the-difference-between-transfer-and-trasnferfrom