分类: Web3
7 条动态区块链系列3 – 如何发布支持预授权的ERC20代币
接着上一篇“区块链系列2 – 如何发布一个支持黑名单管理的ERC20代币” ,对于发币来说,默认的只支持主动转账的ERC20代币有时会有限制,在传统银行业务中,还有一个场景是授权交易,比如A账户可以发起一个授权,允许B账户在一定时间内向自己的账户发起扣款X元,这里的B账户、时间范围、累计扣款金额就是三个参数,那是否可以基于区块链开发一个这样的智能合约?即允许一个钱包用户授权另一个钱包客户在一定时间范围内一定金额范围内向自己的钱包发起扣款。经过我尝试,是可以的。
智能合约
预授权功能实现
重点内容:自托管钱包比如MetaMask它只有基础的转账和收款功能,如果是一个授权,是需要调用智能合约的一个方法,MetaMask是做不到的,所以就需要一个独立的DApp才可以,在自己开发的DApp里主动让钱包的私钥签名后调用特定的授权方法。
DApp开发
现在都有很成熟的DApp开发框架,很多语言也都可以实现,为了实现方式,我才用了php。
开发好的网址是: https://token.liangdong.me ,如下图所示
选择链
管理员权限
MetaMask选择测试链,并选择发布合约相同的钱包,即可看到管理员的功能,可以看到有100万代币,精读是2。
创建预授权
现在准备验证发起预授权,首先使用另外一个钱包, 0x207C3fF50e568a6DfEFB34FdDAd36faB69648Aa6 ,先使用管理员钱包0xDb5206169c8c491BC8B17d294DbaFF6644df889B 向这个钱包铸币1000
切换到0x207C3fF50e568a6DfEFB34FdDAd36faB69648Aa6钱包后即可看到余额
现在就可以发起预授权,授权0xDb5206169c8c491BC8B17d294DbaFF6644df889B钱包可以在1小时内授权累计500,不区分次数,总额不超过500即可。
点击创建预授权时需要通过Meta Mask签名,签名时也会看到信息,
Authorize 0xDb5206169c8c491BC8B17d294DbaFF6644df889B to spend 50000 wei of IOCT4CM. Valid for 1 hours.
这笔签名需要Gas Fee
预授权成功
可查询到该笔预授权,关键信息时预授权ID,后续可以通过这个预授权ID发起扣款交易,当然前提是只有允许被授权的钱包0xDb5206169c8c491BC8B17d294DbaFF6644df889B才可以通过这个ID发起授权扣款,其他钱包不可以。
发起预授权
现在把MetaMask切回到可以发起预授权的钱包0xDb5206169c8c491BC8B17d294DbaFF6644df889B ,输入预授权ID,输入金额100,点击执行扣款。
授权完成后各钱包查询
授权钱包余额900
区块链系列2 – 如何发布一个支持黑名单管理的ERC20代币
接着上一篇“区块链系列1 – 如何在以太坊发布一个带图标带官网的ERC20代币” ,对于发币来说,肯定希望增加黑名单管理的功能,比如动态增加黑名单、删除黑名单,这又分为付款黑名单 和 收款黑名单,这就会有几个问题。
- 黑名单保存在哪里?
- 可以通过一个URL来配置黑名单吗?
- MetaMask这样的Web3 Wallet可以使用这种ERC20代币吗?
- 在智能合约里配置因黑名单造成转账失败的错误提示,可以被查看吗?
一、Talk is cheap. Show me the code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract ERC20BlackList is ERC20, Ownable {
// 黑名单结构体,包含状态和时间信息
struct BlackListInfo {
bool isBlacklisted;
uint256 addedTime;
uint256 removedTime;
}
mapping(address => BlackListInfo) public blackPayerList;
mapping(address => BlackListInfo) public blackPayeeList;
event BlackPayerAdded(address indexed account, uint256 addedTime);
event BlackPayerRemoved(address indexed account, uint256 removedTime);
event BlackPayeeAdded(address indexed account, uint256 addedTime);
event BlackPayeeRemoved(address indexed account, uint256 removedTime);
constructor() ERC20("IOC Team Token V2 BlackList", "IOCV2BL") Ownable(msg.sender) {
_mint(msg.sender, 10000 * 10**decimals());
}
function _update(address from, address to, uint256 value) internal override {
// 检查付款人黑名单
if (from != address(0)) {
require(!blackPayerList[from].isBlacklisted, "Transfer failed: sender address is blacklisted");
}
// 检查收款人黑名单
if (to != address(0)) {
require(!blackPayeeList[to].isBlacklisted, "Transfer failed: recipient address is blacklisted");
}
super._update(from, to, value);
}
// =============================================================
// 付款人黑名单管理功能
// =============================================================
// 添加单个地址到付款人黑名单
function addBlackPayer(address account) external onlyOwner {
require(account != address(0), "Cannot add zero address");
require(!blackPayerList[account].isBlacklisted, "Address already in payer blacklist");
blackPayerList[account].isBlacklisted = true;
blackPayerList[account].addedTime = block.timestamp;
emit BlackPayerAdded(account, block.timestamp);
}
// 移除单个地址从付款人黑名单
function removeBlackPayer(address account) external onlyOwner {
require(blackPayerList[account].isBlacklisted, "Address not in payer blacklist");
blackPayerList[account].isBlacklisted = false;
blackPayerList[account].removedTime = block.timestamp;
emit BlackPayerRemoved(account, block.timestamp);
}
// 批量添加付款人黑名单
function batchAddBlackPayers(address[] memory accounts) external onlyOwner {
require(accounts.length > 0, "Address list cannot be empty");
uint256 currentTime = block.timestamp;
for (uint256 i = 0; i < accounts.length; i++) {
address account = accounts[i];
require(account != address(0), "Cannot add zero address");
if (!blackPayerList[account].isBlacklisted) {
blackPayerList[account].isBlacklisted = true;
blackPayerList[account].addedTime = currentTime;
emit BlackPayerAdded(account, currentTime);
}
}
}
// 批量移除付款人黑名单
function batchRemoveBlackPayers(address[] memory accounts) external onlyOwner {
require(accounts.length > 0, "Address list cannot be empty");
uint256 currentTime = block.timestamp;
for (uint256 i = 0; i < accounts.length; i++) {
address account = accounts[i];
if (blackPayerList[account].isBlacklisted) {
blackPayerList[account].isBlacklisted = false;
blackPayerList[account].removedTime = currentTime;
emit BlackPayerRemoved(account, currentTime);
}
}
}
// =============================================================
// 收款人黑名单管理功能
// =============================================================
// 添加单个地址到收款人黑名单
function addBlackPayee(address account) external onlyOwner {
require(account != address(0), "Cannot add zero address");
require(!blackPayeeList[account].isBlacklisted, "Address already in payee blacklist");
blackPayeeList[account].isBlacklisted = true;
blackPayeeList[account].addedTime = block.timestamp;
emit BlackPayeeAdded(account, block.timestamp);
}
// 移除单个地址从收款人黑名单
function removeBlackPayee(address account) external onlyOwner {
require(blackPayeeList[account].isBlacklisted, "Address not in payee blacklist");
blackPayeeList[account].isBlacklisted = false;
blackPayeeList[account].removedTime = block.timestamp;
emit BlackPayeeRemoved(account, block.timestamp);
}
// 批量添加收款人黑名单
function batchAddBlackPayees(address[] memory accounts) external onlyOwner {
require(accounts.length > 0, "Address list cannot be empty");
uint256 currentTime = block.timestamp;
for (uint256 i = 0; i < accounts.length; i++) {
address account = accounts[i];
require(account != address(0), "Cannot add zero address");
if (!blackPayeeList[account].isBlacklisted) {
blackPayeeList[account].isBlacklisted = true;
blackPayeeList[account].addedTime = currentTime;
emit BlackPayeeAdded(account, currentTime);
}
}
}
// 批量移除收款人黑名单
function batchRemoveBlackPayees(address[] memory accounts) external onlyOwner {
require(accounts.length > 0, "Address list cannot be empty");
uint256 currentTime = block.timestamp;
for (uint256 i = 0; i < accounts.length; i++) {
address account = accounts[i];
if (blackPayeeList[account].isBlacklisted) {
blackPayeeList[account].isBlacklisted = false;
blackPayeeList[account].removedTime = currentTime;
emit BlackPayeeRemoved(account, currentTime);
}
}
}
// =============================================================
// 黑名单检查功能(带时间信息)
// =============================================================
// 检查地址是否在付款人黑名单中
function isBlackPayer(address account) external view returns (bool) {
return blackPayerList[account].isBlacklisted;
}
// 检查地址是否在收款人黑名单中
function isBlackPayee(address account) external view returns (bool) {
return blackPayeeList[account].isBlacklisted;
}
// 获取付款人黑名单详细信息
function getBlackPayerInfo(address account) external view returns (
bool isBlacklisted,
uint256 addedTime,
uint256 removedTime
) {
BlackListInfo memory info = blackPayerList[account];
return (info.isBlacklisted, info.addedTime, info.removedTime);
}
// 获取收款人黑名单详细信息
function getBlackPayeeInfo(address account) external view returns (
bool isBlacklisted,
uint256 addedTime,
uint256 removedTime
) {
BlackListInfo memory info = blackPayeeList[account];
return (info.isBlacklisted, info.addedTime, info.removedTime);
}
// 检查地址在两个黑名单中的状态
function getBlackListStatus(address account) external view returns (
bool isPayerBlacklisted,
bool isPayeeBlacklisted
) {
return (
blackPayerList[account].isBlacklisted,
blackPayeeList[account].isBlacklisted
);
}
// 获取地址的完整黑名单信息
function getFullBlackListInfo(address account) external view returns (
bool isPayerBlacklisted,
uint256 payerAddedTime,
uint256 payerRemovedTime,
bool isPayeeBlacklisted,
uint256 payeeAddedTime,
uint256 payeeRemovedTime
) {
BlackListInfo memory payerInfo = blackPayerList[account];
BlackListInfo memory payeeInfo = blackPayeeList[account];
return (
payerInfo.isBlacklisted,
payerInfo.addedTime,
payerInfo.removedTime,
payeeInfo.isBlacklisted,
payeeInfo.addedTime,
payeeInfo.removedTime
);
}
// 批量检查多个地址的付款人黑名单状态
function batchCheckBlackPayers(address[] memory accounts) external view returns (bool[] memory) {
bool[] memory results = new bool[](accounts.length);
for (uint256 i = 0; i < accounts.length; i++) {
results[i] = blackPayerList[accounts[i]].isBlacklisted;
}
return results;
}
// 批量检查多个地址的收款人黑名单状态
function batchCheckBlackPayees(address[] memory accounts) external view returns (bool[] memory) {
bool[] memory results = new bool[](accounts.length);
for (uint256 i = 0; i < accounts.length; i++) {
results[i] = blackPayeeList[accounts[i]].isBlacklisted;
}
return results;
}
// 批量获取付款人黑名单详细信息
function batchGetBlackPayerInfo(address[] memory accounts) external view returns (
bool[] memory isBlacklistedList,
uint256[] memory addedTimeList,
uint256[] memory removedTimeList
) {
uint256 length = accounts.length;
isBlacklistedList = new bool[](length);
addedTimeList = new uint256[](length);
removedTimeList = new uint256[](length);
for (uint256 i = 0; i < length; i++) {
BlackListInfo memory info = blackPayerList[accounts[i]];
isBlacklistedList[i] = info.isBlacklisted;
addedTimeList[i] = info.addedTime;
removedTimeList[i] = info.removedTime;
}
return (isBlacklistedList, addedTimeList, removedTimeList);
}
// =============================================================
// 通用批量更新功能(兼容您原来的需求)
// =============================================================
// 批量更新付款人黑名单(支持添加和移除)
function updateBlackPayerList(address[] memory addresses, bool[] memory statuses) external onlyOwner {
require(addresses.length == statuses.length, "Address and status arrays length mismatch");
require(addresses.length > 0, "Address list cannot be empty");
uint256 currentTime = block.timestamp;
for (uint256 i = 0; i < addresses.length; i++) {
address account = addresses[i];
bool status = statuses[i];
require(account != address(0), "Cannot update zero address");
if (blackPayerList[account].isBlacklisted != status) {
blackPayerList[account].isBlacklisted = status;
if (status) {
blackPayerList[account].addedTime = currentTime;
emit BlackPayerAdded(account, currentTime);
} else {
blackPayerList[account].removedTime = currentTime;
emit BlackPayerRemoved(account, currentTime);
}
}
}
}
// 批量更新收款人黑名单(支持添加和移除)
function updateBlackPayeeList(address[] memory addresses, bool[] memory statuses) external onlyOwner {
require(addresses.length == statuses.length, "Address and status arrays length mismatch");
require(addresses.length > 0, "Address list cannot be empty");
uint256 currentTime = block.timestamp;
for (uint256 i = 0; i < addresses.length; i++) {
address account = addresses[i];
bool status = statuses[i];
require(account != address(0), "Cannot update zero address");
if (blackPayeeList[account].isBlacklisted != status) {
blackPayeeList[account].isBlacklisted = status;
if (status) {
blackPayeeList[account].addedTime = currentTime;
emit BlackPayeeAdded(account, currentTime);
} else {
blackPayeeList[account].removedTime = currentTime;
emit BlackPayeeRemoved(account, currentTime);
}
}
}
}
// =============================================================
// 时间相关的查询功能
// =============================================================
// 查询在指定时间段内被添加到付款人黑名单的地址数量
// 注意:这个函数需要遍历所有地址,实际使用中可能需要使用事件日志来实现
// 这里只是示例,实际实现可能需要离链查询事件
function getBlackPayersAddedInTimeRange(uint256 /*startTime*/, uint256 /*endTime*/) external pure returns (uint256) {
// 占位返回 - 实际实现需要事件日志查询
return 0;
}
// 检查地址是否在指定时间被加入黑名单
function wasBlacklistedAt(address account, uint256 timestamp, bool isPayer) external view returns (bool) {
BlackListInfo memory info = isPayer ? blackPayerList[account] : blackPayeeList[account];
if (info.addedTime == 0) {
return false; // 从未被加入黑名单
}
if (info.addedTime > timestamp) {
return false; // 在指定时间后才被加入
}
if (info.removedTime > 0 && info.removedTime <= timestamp) {
return false; // 在指定时间前已被移除
}
return true; // 在指定时间确实在黑名单中
}
// =============================================================
// 实用工具函数
// =============================================================
// 获取当前区块时间戳
function getCurrentTimestamp() external view returns (uint256) {
return block.timestamp;
}
// 将时间戳转换为可读日期(需要前端处理)
function getReadableTime(uint256 timestamp) external pure returns (string memory) {
if (timestamp == 0) {
return "Never set";
}
// 返回时间戳,前端可以转换为可读格式
return string(abi.encodePacked("Timestamp: ", uint2str(timestamp)));
}
// 数字转字符串的辅助函数
function uint2str(uint256 _i) internal pure returns (string memory) {
if (_i == 0) {
return "0";
}
uint256 j = _i;
uint256 len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint256 k = len;
while (_i != 0) {
k = k - 1;
uint8 temp = (48 + uint8(_i - _i / 10 * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
// 获取合约基本信息
function getContractInfo() external view returns (
string memory tokenName,
string memory tokenSymbol,
uint8 tokenDecimals,
uint256 tokenTotalSupply,
address contractOwner
) {
return (
name(),
symbol(),
decimals(),
totalSupply(),
owner()
);
}
}看了代码后,就可以解决前两个问题了
- 黑名单保存在哪里?
- 保存在区块链上。
- 可以通过一个URL来配置黑名单吗?
- 不能,智能合约无法直接发起HTTP请求来检查外部URL,因为区块链是封闭的环境。
关于黑名单在区块链保存在哪里?可以参考下图会清晰一些:
二、发布合约到测试链Sepolia
还是使用Remix来部署合约,操作可以参考上一篇。
部署这个合约需要0.0062个ETH,主链的话就需要15美金,上一篇介绍的一个最简单的ERC20合约只需要3美金,毕竟这个代码很复杂,15美金也是正常的。
合约地址是: https://sepolia.etherscan.io/address/0x244af7a9faf2e494dfee1717e08e0a32fdbc9934
代币名称:IOCV2BL ,发行总量10000个。
三、需要公开代码,方便在线调用函数
把合约代码公开后,就可以在合约地址 https://sepolia.etherscan.io/token/0x244af7a9faf2e494dfee1717e08e0a32fdbc9934#code 直接读取合约或者写合约了,读合约的主要操作就是查询某个地址是否是黑明单,写合约操作就是单次或者批量的增加收付款黑名单地址及移除收付款黑名单地址。
四、未增加黑名单时转账
这时解答了第三个问题
- MetaMask这样的Web3 Wallet可以使用这种ERC20代币吗?
- 可以
五、对收款人地址增加收款黑名单
检查一下是否维护了黑名单,确实已经是黑名单了。
六、给增加了收款黑名单的地址发起转账
可以发现交易失败,并且可以在区块链浏览器查看到这笔错误交易,错误信息也是在智能合约里定义的”Transfer failed: recipient address is blacklisted”
这时也解答了第四个问题
- 在智能合约里配置因黑名单造成转账失败的错误提示,可以被查看吗?
- 可以,都在记录在区块链上,使用区块链浏览器查看。
区块链地址: https://sepolia.etherscan.io/tx/0x85d370c9c9a0feba43930b28a507426728b40bf56adea127d776952050603e78
定义错误描述的代码部分见下:
区块链系列1 – 如何在以太坊发布一个带图标带官网的ERC20代币
在以太坊上创建并发布一个带有图标和官网链接的ERC20代币,不仅可以提升代币的专业形象,还能增强用户的信任感。
从USDT说起
USDT(Tether)作为最早发行的稳定币之一,USDT 由 Tether Limited Inc. 于 2014 年推出,旨在提供与美元等法定货币挂钩的数字资产。USDT 的价值与美元保持 1:1 的锚定关系,旨在减少加密货币市场的波动性。
USDT 合约地址:https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7
ERC20代币的开发与部署
使用Remix开发环境,结合OpenZeppelin的ERC20标准合约,快速创建了一个名为IOC的代币:
// SPDX-License-Identifier: MIT
// 指定合约的许可证类型为 MIT,表明该合约遵循 MIT 开源许可证。
pragma solidity ^0.8.20;
// 声明 Solidity 编译器的版本要求,确保合约在 0.8.20 版本或更高版本的编译器上编译。
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// 导入 OpenZeppelin 提供的 ERC20 合约实现,利用其安全且经过审计的标准功能,避免重复造轮子。
contract TeamToken is ERC20 {
// 定义一个名为 TeamToken 的合约,并继承自 OpenZeppelin 的 ERC20 合约,实现标准的 ERC20 功能。
string public teamName = "IOC Team";
// 声明一个公共字符串变量 teamName,用于存储团队名称,任何人都可以读取该值。
string public teamWebsite = "https://ioc.liangdong.me";
// 声明一个公共字符串变量 teamWebsite,用于存储团队官网地址,便于用户了解更多信息。
constructor() ERC20("IOC Team Token", "IOC") {
// 构造函数,在部署合约时执行。调用父合约 ERC20 的构造函数,设置代币名称为 "IOC Team Token",代币符号为 "IOC"。
_mint(msg.sender, 238800 * 10 ** 18);
// 调用 _mint 函数,向部署合约的地址(msg.sender)铸造 238,800 个代币。
// 由于 ERC20 默认使用 18 位小数,因此实际铸造的单位为 238800 × 10^18。
}
}
在Environment通过MetaMask关联自己的钱包,同时需要在MetaMask上先切换到主网
通过Remix将合约部署到以太坊主网,通过我的以太坊钱包(MetaMask)liangdong.eth ,部署到主网。
发布后的合约地址:https://etherscan.io/token/0x5541cba0eeed53ae8335b93b7f20ccc453ca03bb
- 代币基本信息
IOC Team Token(IOC)是一种ERC-20代币,最大总供应量为238,800个IOC。目前只有1个持有者,且仅有1次转账记录。 - 市场信息
文本中未明确给出链上市值和流通供应市值的具体数值。 - 代币合约地址
代币合约地址为0x5541cba0eeed53ae8335b93b7f20ccc453ca03bb。
部署到主网的交易见下图,交易地址: https://etherscan.io/tx/0x9ec6974ee204e9e241ed198950a19544d07eaa3abb4bcfe6cd53f409a5412f47
- 交易费用大概3美元。
- Transaction Fee:0.0011718135 ETH($3.00)
- Gas Fee:1.1 Gwei(0.0000000011 ETH)
添加图标和官网链接
为了让IOC代币在Etherscan上展示图标和官网链接,我进行了以下准备:
- 官网建设:使用Vibe Coding快速搭建了一个简洁的代币官网:https://ioc.liangdong.me。
- 图标设计:利用ChatGPT生成了一个SVG格式的图标:https://ioc.liangdong.me/ioc.svg。
需要注意的是,ERC20标准本身仅包含代币的名称(name)、符号(symbol)和小数位数(decimals)等基本信息,并不包括图标(icon)和官网链接等元数据。这些额外的信息是由区块链浏览器(如Etherscan)提供的展示功能。因此,代币开发者需要主动向这些浏览器提交相关信息,经过审核后才能在代币页面上展示图标和官网链接。
完成以上准备工作后,我在Etherscan的“Update Token Info”页面提交了官网链接和图标,希望能够在区块浏览器上展示。
Etherscan的审核过程
5月18日 10:23提交
5月20日 14:45分收到Etherscan的拒绝电邮
邮件明确表示,Etherscan团队对提交的信息进行审核,并非任何网页和图标都可以随意上传。他们要求代币项目提供足够的信息和详细说明,包括项目介绍、团队信息、社交媒体链接、使用场景、白皮书或文档等。
总结与建议
- 准备详实的代币信息:Etherscan对项目有明确的要求,建议在提交信息时提供全面、详细且清晰的项目介绍。
- 官网建设:设计一个清晰、信息全面的代币官网,提升项目的可信度。
- 图标设计:提供清晰、美观且符合ERC20标准的图标。
通过以上步骤,可以提高代币在Etherscan上展示图标和官网链接的成功率,增强代币的专业形象和用户信任度。
最后,把这个代币展示在自己的钱包里
以MetaMask APP为例,默认打开显示的代币只有ETH和NFT
点击右上角的+号,选择“自定义代币“,输入合约地址:0x5541cba0eeed53ae8335b93b7f20ccc453ca03bb ,就可以在钱包里看到这个代币了,可以看到是238800枚,当然因为这个代币没有流动性,也就没有价值显示,也就是俗称的“空气币”了。
通过一个简单的角度来理解为何数字货币转账很慢
通过一个简单的角度来理解一下为何比特币、以太坊的转账都很慢,不像微信、支付宝可以做到实时到账。
中心化的模式 的假设前提是“信任”。需要各个使用者相信中央节点的可靠与权威(例如相信支付宝系统的诚实可靠、权威),换来的优势就是近乎瞬间的转账效率。
分布式系统 的假设前提是“互不信任”。则需要在协调各节点达成共识后才能完成转账,这是需要付出昂贵的时间代价的,换来的优势就是账本的可靠性较高且没有中心化风险。
































