在手把手教你实现BigBank文章中,我们实现了一个稍微复杂点的存款、取款业务。但是聪明的你可能发现了,我们的BigBank虽然名字中带有big,但是有一个明显的缺点:它只能存入和取出ETH原生代币,面对广大的符合ERC20标准的Token却无能为力。
在手把手教你实现BigBank文章中,我们实现了一个稍微复杂点的存款、取款业务。但是聪明的你可能发现了,我们的BigBank
虽然名字中带有big
,但是有一个明显的缺点:
它只能存入和取出ETH原生代币,面对广大的符合ERC20标准的Token却无能为力。
这就好比你去一家银行,只能存取人民币,你想去换点港币去香港旅游,但是银行告诉你不支持港币业务,这就很让人头疼。
这篇文章,我们就从ERC20代币开始分析,逐步实现我们的TokenBank
合约。
ERC-20标准是用于在以太坊区块链上创建智能合约的代币的技术规范。它定义了一系列规则,所有ERC-20代币都必须遵守这些规则,以便它们能够与以太坊网络上的其他代币和应用程序兼容。
ERC-20标准的主要目的是确保所有ERC-20代币都具有相同的基本功能,使其易于使用和集成。这使得开发人员可以轻松地创建新的代币,并确信它们将能够与现有的以太坊基础设施和工具一起工作。
还记的我们介绍过的接口吗?这里可以把标准认为是接口,我们自己铸造的ERC20 Token必须继承这个接口。并实现它其中的方法。
ERC-20标准定义了代币合约必须实现的一些基本功能,包括:
而可选功能则包括:
在真实的生产环境中,我们一般不会亲自去手动实现,一方面社区已经比较成熟的方案和标准库供我们调用,不需要我们重复造轮子,一方面,我们自己写的如果测试不充分,可能会有很多的漏洞。
但是为了学习,这里提供一份符合ERC20
标准的Token代码。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract BaseERC20 {
// Basic ERC20 token information
string public name;
string public symbol;
uint8 public decimals;
// Total supply of tokens
uint256 public totalSupply;
// Mapping of owner addresses to their token balances
mapping(address => uint256) balances;
// Mapping of owner addresses to spender addresses and their allowances
mapping(address => mapping(address => uint256)) allowances;
// Event emitted when tokens are transferred
event Transfer(address indexed from, address indexed to, uint256 value);
// Event emitted when an allowance for spending tokens is set
event Approval(address indexed owner, address indexed spender, uint256 value);
// Constructor to initialize token information and mint total supply to deployer
constructor(
string memory tokenName,
string memory tokenSymbol,
uint8 TokenDecimals,
uint256 tokenTotalSupply
) {
name = tokenName;
symbol = tokenSymbol;
decimals = TokenDecimals;
totalSupply = tokenTotalSupply * (10 ** decimals);
balances[msg.sender] = totalSupply;
}
// Returns the token balance of a given address
function balanceOf(address _owner) public view returns (uint256 balance) {
require(
_owner != address(0),
"ERC20: balance query for the zero address"
);
return balances[_owner];
}
// Transfers tokens from the message sender to a recipient
function transfer(address _to, uint256 _value) public returns (bool success) {
require(_to != address(0), "ERC20: transfer to the zero address");
require(_value > 0, "ERC20: transfer amount must be greater than zero");
uint256 senderBalance = balances[msg.sender];
require(
senderBalance >= _value,
"ERC20: transfer amount exceeds balance"
);
// SafeMath not required for internal transfers in Solidity 0.8+ due to overflow checking
if (_to != msg.sender) {
balances[msg.sender] = senderBalance - _value;
balances[_to] += _value;
}
emit Transfer(msg.sender, _to, _value);
return true;
}
// Transfers tokens on behalf of another address using an allowance
function transferFrom(
address _from,
address _to,
uint256 _value
) public returns (bool success) {
require(_to != address(0), "ERC20: transferFrom to the zero address");
require(
_value > 0,
"ERC20: transferFrom amount must be greater than zero"
);
require(
balances[_from] >= _value,
"ERC20: transfer amount exceeds balance"
);
require(
allowances[_from][msg.sender] >= _value,
"ERC20: transfer amount exceeds allowance"
);
balances[_from] -= _value;
balances[_to] += _value;
allowances[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
}
// Approves an address to spend a certain amount of tokens on your behalf
function approve(address _spender, uint256 _value) public returns (bool success) {
address owner = msg.sender;
require(owner != address(0), "ERC20: approve from the zero address");
require(_spender != address(0), "ERC20: approve to the zero address");
allowances[owner][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
// Returns the remaining allowance for a spender on a specific owner's tokens
function allowance(address _owner, address _spender)
public
view
returns (uint256 remaining)
{
require(_owner != address(0), "ERC20: owner is the zero address");
require(_spender != address(0), "ERC20: spender is the zero address");
return allowances[_owner][_spender];
}
从上面代码中可以看出,我们将allowances
设置为了嵌套映射。这么设计是经过慎重思考的。
如果你觉得还是不够直观,这里我们可以用一个图示来说明一下:
让我解释一下这个嵌套映射和可视化图表:
基本结构:
mapping(address => mapping(address => uint256)) private allowances;
这个结构可以理解为一个两层的映射:
具体例子: 假设我们有以下情况:
在代码中的表示:
allowances[0x1234...][0x5678...] = 100;
allowances[0x1234...][0x9ABC...] = 50;
allowances[0x5678...][0x9ABC...] = 30;
可视化解释:
如何使用:
allowances[Alice的地址][Bob的地址]
allowances[Bob的地址][Charlie的地址]
这种结构允许在 ERC20 代币中实现复杂的授权机制,使得用户可以安全地允许其他地址(如智能合约)代表自己使用一定数量的代币,而无需转移代币的所有权。
approve
函数。是非常重要的,它在 ERC20 代币的授权机制中扮演着关键角色。我们上面解释的allowances
在这个函数中就有比较重要的应用。
让我们逐步深入理解 approve
函数:
基本定义:
function approve(address spender, uint256 amount) public returns (bool)
目的:
approve
函数允许代币持有者(调用者)授权另一个地址(通常是一个合约)代表自己使用一定数量的代币。(这个点非常重要,后续的TokenBank有个坑就是因为这个)
参数:
spender
:被授权的地址,可以是另一个用户的地址或一个合约地址。amount
:授权使用的代币数量。返回值:
工作原理:
msg.sender
(调用者)允许 spender
使用的代币数量。Approval
事件,记录这次授权操作。使用场景:
重要注意事项:
approve
不会检查用户的余额。用户可以授权超过自己拥有的代币数量。approve
都会覆盖之前的授权值,而不是增加或减少。与其他函数的关系:
transferFrom
:使用 approve
设置的授权额度。allowance
:查询当前的授权额度。实际应用示例: 假设 Alice 想要使用一个去中心化交易所(DEX):
// Alice 授权 DEX 合约使用最多 100 个代币
tokenContract.approve(dexContractAddress, 100);
// 之后,DEX 合约可以调用 transferFrom 来使用这些代币
tokenContract.transferFrom(aliceAddress, bobAddress, 50);
理解 approve
函数对于掌握 ERC20 代币的工作机制非常重要。它为代币持有者提供了一种安全的方式来允许其他地址或合约管理他们的部分代币,而无需完全转移所有权。这种机制极大地增加了 ERC20 代币的灵活性和实用性,尤其是在复杂的 DeFi 应用中。
transfer
和 transferFrom
是 ERC20 标准中两个核心函数,它们有着不同的用途和工作方式。
基本定义:
transfer
:
function transfer(address recipient, uint256 amount) public returns (bool)
transferFrom
:
function transferFrom(address sender, address recipient, uint256 amount) public returns (bool)
主要区别:
a) 调用者:
transfer
: 直接由代币持有者调用。transferFrom
: 可以由第三方调用,前提是该第三方已被授权。b) 资金来源:
transfer
: 资金总是从调用者(msg.sender
)的账户转出。transferFrom
: 资金从 sender
参数指定的账户转出,而不一定是调用者的账户。c) 授权机制:
transfer
: 不需要预先授权。transferFrom
: 需要 sender
事先通过 approve
函数授权给调用者足够的额度。用途:
transfer
:
transferFrom
:
工作流程:
transfer
:
transfer
。transferFrom
:
approve
,授权某地址使用一定数量的代币。transferFrom
。使用场景举例:
transfer
: 用户A直接向用户B发送代币。transferFrom
: 去中心化交易所代表用户执行交易,或智能合约自动从用户账户扣除订阅费用。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title TokenBank
* @dev A smart contract that allows users to deposit and withdraw ERC20 tokens.
*/
contract TokenBank {
/// @dev A mapping to store balances for each token address and user address.
mapping(address => mapping(address => uint256)) private balances;
/**
* @dev Deposits a specified amount of ERC20 tokens from the caller's address to the contract.
* @param token The address of the ERC20 token to deposit.
* @param amount The amount of tokens to deposit.
*
* Emits an event if the deposit is successful.
*
* Requirements:
* - `amount` must be greater than zero.
* - The transfer from the caller to the contract must be successful.
*/
function deposit(address token, uint256 amount) public {
require(amount > 0, "Amount must be greater than zero");
require(
IERC20(token).transferFrom(msg.sender, address(this), amount),
"Transfer failed"
);
balances[token][msg.sender] += amount;
// Emit an event for successful deposits (optional)
// emit Deposit(msg.sender, token, amount);
}
/**
* @dev Withdraws a specified amount of ERC20 tokens from the contract to the caller's address.
* @param token The address of the ERC20 token to withdraw.
* @param amount The amount of tokens to withdraw.
*
* Emits an event if the withdrawal is successful.
*
* Requirements:
* - `amount` must be greater than zero.
* - The caller must have sufficient balance of the specified token.
* - The transfer from the contract to the caller must be successful.
*/
function withdraw(address token, uint256 amount) public {
require(amount > 0, "Amount must be greater than zero");
require(balances[token][msg.sender] >= amount, "Insufficient balance");
balances[token][msg.sender] -= amount;
require(IERC20(token).transfer(msg.sender, amount), "Transfer failed");
// Emit an event for successful withdrawals (optional)
// emit Withdrawal(msg.sender, token, amount);
}
/**
* @dev Returns the balance of a specific ERC20 token for a given user.
* @param token The address of the ERC20 token.
* @param account The address of the user.
* @return The balance of the specified token for the user.
*/
function balanceOf(address token, address account) public view returns (uint256) {
return balances[token][account];
}
}
细心的朋友可能发现了,我们直接引用了openzeppelin
接口,这也是上文中提到的,真实的生产环境中,我们不需要自己实现这些能力。
我们首先部署ERC20合约,这里为了方便起见,我们还是使用remix, 因为需要钱包授权交互,我们使用真实的测试网络。
我这里直接贴出来我自己部署的合约地址:
0x2F91329978dBb23fEa7FE89Ab98c24BE808537B0
部署成功后,可以看到这个样子:
为了交互方便,我们需要将这个token添加到自己的钱包中:
添加完代币之后,我们可以代币列表中看到我们刚刚添加的代币。
我这里直接贴出来我自己部署的合约地址:
0xc4c85b3aadced03c018e7dba42718104f33d7166
为了顺利实现TokenBank
的功能,我们需要用户授权TokenBank
合约调用ERC20
一部分的份额。
授权一定要在存款和取款行为之前,否则交易会失败。
我们调用deposit方法,入参填入ERC20合约地址和需要存入的数量
点击拉起钱包交互,点击确认。交易成功。
调用withdraw方法,入参填入自己的钱包地址和取款存入的数量;
整个流程跑通。
如果你期望我们的TokenBank
合约可以接收原生代币,可以继承手把手教你实现BigBank这篇文章中的合约,一切都显的自然了。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!