接着上一篇“区块链系列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
定义错误描述的代码部分见下: