本文详细介绍了ERC-777代币标准的实现,包括其核心概念、创建和部署的步骤,以及测试ERC-777合约的功能。通过配合使用Foundry工具,读者可以学习如何有效地管理和使用新特性如操作员和钩子。本文不仅提供了代码示例,还覆盖了项目设置和部署到Sepolia测试网的过程,适合希望深入理解加密货币领域先进代币标准的开发者。
截至目前,最受欢迎的代币标准是 ERC-20(这也是有原因的)。但是,开发者们不断在寻找创新的方法。ERC-777的作者提出了一种新的代币标准(与ERC-20向后兼容),引入了“操作员”,允许经过批准的地址代表持有者管理代币,并在代币转移时实现发送/接收钩子。
在本指南中,我们将详细介绍ERC-777的实现,涵盖核心概念,然后学习如何使用Foundry自己创建、部署和测试ERC-777合约。如果你还不知道Foundry,可以查看本指南:Introducing to Foundry。
让我们开始吧!
希望通过可视化的指导来学习吗?跟随Sahil一起学习如何创建和部署ERC-777代币,并探索ERC-777代币标准的独特功能。
如何创建和部署ERC-777代币 - YouTube
QuickNode
131K订阅者
QuickNode
搜索
信息
购物
点击静音
如果播放未能及时开始,请尝试重新启动设备。
你已退出登录
你观看的视频可能会加入智能电视的观看历史并影响推荐。为避免这种情况,请在电脑上取消并登录YouTube。
取消确认
分享
包含播放列表
检索共享信息时发生错误。请稍后重试。
稍后观看
分享
复制链接
在
0:00
/ •直播
•
订阅我们的YouTube频道以获取更多视频! 订阅
依赖项 | 版本 |
---|---|
node | 20.17 |
foundry | 0.8.10 |
ERC-777是一个先进的代币标准,基于ERC-20构建。它的主要特点包括:
send
函数替代transfer
,可能消除了单独approve
和transferFrom
调用的需要。要实现ERC-777的钩子功能:
0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24
ERC777TokensSender
和ERC777TokensRecipient
接口。这些钩子启用在转账时拒绝不必要的代币或实现复杂的转账逻辑等功能。
注意
值得注意的是,虽然ERC-777提供了高级功能,但也引入了潜在的安全风险,特别是与重入攻击相关的风险(了解更多这里)。OpenZeppelin,作为领先的智能合约库,因感到这个原因而停止支持ERC-777。开发者在实现ERC-777代币时应谨慎行事。
钩子允许在代币发送或接收之前执行自定义逻辑,具体包括:
tokensToSend
函数,在代币转移之前调用tokensReceived
函数,在代币转移后调用操作员是被授权代表持有者管理代币的地址,具体包括:
operatorSend
和operatorBurn
函数isOperatorFor
检查某个地址是否为操作员在接下来的章节中,我们将深入探讨ERC-777,通过自己创建、测试和部署一个来说明。
打开终端并运行以下命令以创建新项目:
mkdir erc777-token
cd erc777-token
forge init
接下来,安装OpenZeppelin库:
forge install OpenZeppelin/openzeppelin-contracts@v4.9.6
然后通过在项目根目录中创建一个remappings.txt
文件来设置映射(这将自动检索项目的推断映射),并包含以下行:
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
接下来,我们将通过在QuickNode上创建一个端点来为生产测试网提供部署。
要与以太坊网络进行交互并为本指南部署智能合约,我们需要一个RPC端点。按照以下步骤使用QuickNode创建一个:
在本项目中配置RPC端点的一种方法是也在foundry.toml
文件中包含它,但如果你打算开源此代码,建议不要这样做。相反,我们创建环境变量以包括我们的RPC端点和私钥(我们将用来部署智能合约),这种方式将在命令行窗口通过以下命令创建:
export RPC_URL=<Your RPC endpoint>
export PRIVATE_KEY=<Your wallets private key>
为了将ERC-777合约部署到Sepolia测试网,你需要Sepolia测试网的ETH来支付手续费。Multi-Chain QuickNode Faucet使获取测试ETH变得简单!
导航到Multi-Chain QuickNode Faucet并连接你的钱包(例如,MetaMask、Coinbase Wallet)或粘贴你的钱包地址以检索测试ETH。请注意使用EVM水龙头需要在以太坊主网上有0.001 ETH的主网余额。你也可以通过推特或用QuickNode帐户登录以获得额外的测试网代币!
创建一个新文件src/TestERC777.sol
并添加以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC777/ERC777.sol";
contract TestERC777 is ERC777 {
constructor(string memory name, string memory symbol, address[] memory defaultOperators)
ERC777(name, symbol, defaultOperators)
{}
function mint(address account, uint256 amount, bytes memory userData, bytes memory operatorData) public {
_mint(account, amount, userData, operatorData);
}
}
此合约扩展了OpenZeppelin提供的ERC-777实现。它将代币名称设置为“Gold”,符号为“GLD”,并包含一个发布新代币的功能,允许向特定地址铸造代币。它还包含一个defaultOperators
参数,你在合约部署时将使用它传递我们希望设置的操作员。
接下来,为了展示ERC-777的钩子功能,我们将创建另一个合约src/TestERC777Recipient.sol
,它将包含在接收到代币时的逻辑。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
import "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol";
contract TestERC777Recipient is IERC777Recipient {
IERC1820Registry private constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
uint256 public receivedTokens;
uint256 public lastReceivedAmount;
address public lastOperator;
address public lastSender;
event TokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes userData,
bytes operatorData
);
constructor() {
_ERC1820_REGISTRY.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, address(this));
}
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external override {
receivedTokens += amount;
lastReceivedAmount = amount;
lastOperator = operator;
lastSender = from;
emit TokensReceived(operator, from, to, amount, userData, operatorData);
}
function getReceivedTokens() public view returns (uint256) {
return receivedTokens;
}
function getLastReceivedInfo() public view returns (uint256, address, address) {
return (lastReceivedAmount, lastOperator, lastSender);
}
}
我们回顾一下上面的合约。它实现了IERC777Recipient
接口,以处理传入的ERC777代币转移。它在ERC1820注册表中注册自己,以被识别为ERC777代币接收者。合约跟踪接收到的代币总量,以及最新转移的详细信息(amount
、operator
和sender
)。当代币被接收时,tokensReceived
函数更新合约的状态并发出带有转移详细信息的事件。此外,两个公共函数getReceivedTokens
和getLastReceivedInfo
允许外部查询所接收的代币和最近的转移信息。
请注意,tokensReceived
函数是在此合约接收到ERC-777代币时实际被调用的钩子。它在接收代币的情况下更新合约状态并发出事件。
现在,保存文件,我们将继续创建测试,以确保一切正常工作,然后再部署到测试网上。
创建一个测试文件test/TestERC777.t.sol
并输入以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC777/ERC777.sol";
import "@openzeppelin/contracts/utils/introspection/IERC1820Registry.sol";
import "../src/TestERC777Recipient.sol";
import "../src/TestERC777.sol";
contract ERC777SetupTest is Test {
TestERC777 public token;
TestERC777Recipient public recipient;
IERC1820Registry public erc1820Registry;
address public deployer;
address public user1;
address public user2;
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
}
在上面,我们导入了TestERC777
合约和Foundry测试所需的Test
合约。然后我们声明合约ERC777SetupTest
并继承Test
合约。在合约内部,我们声明了TestERC777
和TestERC777Recipient
合约。
添加setUp
函数以初始化我们的测试环境:
function setUp() public {
// 部署模拟ERC1820注册表
vm.etch(
address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24),
bytes(
hex"608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"
)
);
erc1820Registry = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
deployer = address(this);
user1 = address(0x1);
user2 = address(0x2);
// 部署自定义ERC777代币
address[] memory defaultOperators = new address[](0);
token = new TestERC777("Gold", "GLD", defaultOperators);
// 部署ERC777接收者
recipient = new TestERC777Recipient();
vm.label(address(token), "GoldToken");
vm.label(address(recipient), "TokenRecipient");
vm.label(user1, "User1");
vm.label(user2, "User2");
}
该函数在每个测试之前运行,设置一个新的状态。它初始化所有者和操作员地址,创建代币并设置初始供应,又部署ERC777Recipient
合约。
让我们添加第一个测试以检查设置是否正确:
function testERC777Setup() public {
// 验证ERC1820注册表已部署
assertTrue(address(erc1820Registry).code.length > 0, "ERC1820 registry应该被部署");
// 验证ERC777代币已部署
assertTrue(address(token).code.length > 0, "ERC777 token应该被部署");
// 检查代币详情
assertEq(token.name(), "Gold", "代币名称应该是Gold");
assertEq(token.symbol(), "GLD", "代币符号应该是GLD");
assertEq(token.totalSupply(), 0, "初始供应应该是0");
// 向user1铸造一些代币
token.mint(user1, 1000 * 10**18, "", "");
// 检查余额
assertEq(token.balanceOf(user1), 1000 * 10**18, "User1的余额应该是1000 GLD");
}
现在,让我们测试代币转移功能:
function testERC777TransferToAddress() public {
// 向user1铸造代币
token.mint(user1, 1000 * 10**18, "", "");
// User1将代币发送到user2(常规地址)
vm.prank(user1);
token.send(user2, 100 * 10**18, "");
// 检查代币余额
assertEq(token.balanceOf(user1), 900 * 10**18, "User1的余额应该在发送后为900 GLD");
assertEq(token.balanceOf(user2), 100 * 10**18, "User2的余额应该为100 GLD");
}
这个代码片段模拟user1
将代币发送到user2
,然后检查结果余额。
让我们测试ERC777发送钩子功能:
function testERC777TransferToRegisteredRecipient() public {
// 向user1铸造代币
token.mint(user1, 1000 * 10**18, "", "");
vm.prank(address(recipient)); // 接收者必须调用此函数,因为按默认情况下它是自己的管理者
erc1820Registry.setManager(address(recipient), address(this));
// 注册接收者合约
vm.prank(address(this));
erc1820Registry.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, address(this));
// User1将代币发送给接收者合约
vm.prank(user1);
token.send(address(recipient), 150 * 10**18, "");
// 检查接收者合约状态
assertEq(recipient.getReceivedTokens(), 150 * 10**18, "接收者应该接收到150 GLD");
(uint256 lastAmount, address lastOperator, address lastSender) = recipient.getLastReceivedInfo();
assertEq(lastAmount, 150 * 10**18, "最后接收金额应该为150 GLD");
assertEq(lastOperator, user1, "最后操作员应该是user1");
assertEq(lastSender, user1, "最后发送者应该是user1");
// 检查代币余额
assertEq(token.balanceOf(user1), 850 * 10**18, "User1的余额在发送后应该为850 GLD");
assertEq(token.balanceOf(address(recipient)), 150 * 10**18, "接收者的余额应该在接收后为150 GLD");
}
这个测试验证了TestERC777Recipient合约是否正确接收到代币,更新了其状态,并记录了交易的必要细节。
现在,让我们测试操作员代表另一个用户发送代币的功能:
function testERC777OperatorSend() public {
// 向user1铸造代币
token.mint(user1, 1000 * 10**18, "", "");
// 将user2设为user1的操作员
vm.prank(user1);
token.authorizeOperator(user2);
// 确保接收者合约被正确设置
vm.prank(address(recipient));
erc1820Registry.setInterfaceImplementer(address(recipient), _TOKENS_RECIPIENT_INTERFACE_HASH, address(recipient));
// User2从user1发送代币到接收者合约
vm.prank(user2);
token.operatorSend(user1, address(recipient), 200 * 10**18, "", "");
// 检查接收者合约状态
assertEq(recipient.getReceivedTokens(), 200 * 10**18, "接收者应该接收到200 GLD");
(uint256 lastAmount, address lastOperator, address lastSender) = recipient.getLastReceivedInfo();
assertEq(lastAmount, 200 * 10**18, "最后接收金额应该为200 GLD");
assertEq(lastOperator, user2, "最后操作员应该是user2");
assertEq(lastSender, user1, "最后发送者应该是user1");
// 检查代币余额
assertEq(token.balanceOf(user1), 800 * 10**18, "User1的余额在发送后应该为800 GLD");
assertEq(token.balanceOf(address(recipient)), 200 * 10**18, "接收者的余额在接收后应该为200 GLD");
}
这个测试检查操作员user2
是否能够从其他用户的帐户user1
发送代币,并验证接收者合约是否正确处理接收到的代币。
要运行这些测试,在终端中使用以下命令:
forge test
你将看到类似于下面的输出:
ERC777-Foundry % forge test -vvv
[⠊] 编译中...
[⠘] 编译 1 个文件,使用Solc 0.8.22
[⠃] Solc 0.8.22完成,耗时1.76s
编译器运行成功!
为test/GLDToken.t.sol:ERC777SetupTest运行了4个测试
[通过] testERC777OperatorSend() (gas: 252056)
[通过] testERC777Setup() (gas: 86202)
[通过] testERC777TransferToAddress() (gas: 110695)
[通过] testERC777TransferToRegisteredRecipient() (gas: 268863)
测试结果:好。4个通过;0个失败;0个跳过;完成时间6.20ms(1.13ms CPU时间)
经过测试,我们已成功!接下来,让我们将其部署到实时测试网上。
创建一个名为script/DeployERC777Contracts.s.sol
的文件,并包含以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Script.sol";
import "../src/TestERC777.sol";
import "../src/TestERC777Recipient.sol";
contract DeployERC777Contracts is Script {
IERC1820Registry constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
bytes32 constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
function deployTestERC777() external {
vm.startBroadcast();
// 部署TestERC777代币
address[] memory defaultOperators = new address[](0);
uint256 initialSupply = 1000000 * 10**18; // 100万个代币,带18位小数
TestERC777 token = new TestERC777("Gold", "GLD", defaultOperators);
token.mint(msg.sender, initialSupply, "", "");
vm.stopBroadcast();
console.log("TestERC777代币部署在:", address(token));
console.log("代币已铸造给:", msg.sender);
console.log("初始供应:", initialSupply);
}
function deployTestERC777Recipient() external {
vm.startBroadcast();
// 部署TestERC777接收者
TestERC777Recipient recipient = new TestERC777Recipient();
vm.stopBroadcast();
console.log("TestERC777Recipient部署在:", address(recipient));
console.log("ERC1820注册表地址:", address(_ERC1820_REGISTRY));
}
}
我们回顾一下这个脚本。
该脚本包含两个单独的部署函数,每个函数设置ERC-777代币和接收者合约钩子的特定组件。请注意,这些是独立注册的合约,与ERC-1820注册表的关系独立。注册表允许动态接口检测,这就是代币合约如何知道某个地址(例如你的接收者合约)实现了接收代币所需的接口。
现在,执行以下每个命令以部署TestERC777
和ERC777Recipient
合约。
forge script script/DeployERC777Contracts.s.sol:DeployERC777Contracts --sig "deployTestERC777()" --rpc-url $RPC_URL --broadcast -vvv --private-key $PRIVATE_KEY
forge script script/DeployERC777Contracts.s.sol:DeployERC777Contracts --sig "deployTestERC777Recipient()" --rpc-url $RPC_URL --broadcast -vvv --private-key $PRIVATE_KEY
输出将显示跟踪并显示合约地址:
...
链11155111
估计gas价格:0.269606234 gwei
回归脚本估计总gas的使用总量:2035187
估计需要的金额:0.000548699102555758 ETH
==========================
发送交易 [0 - 1]。
⠉ [00:00:00] [##################################################################################################################################################################################] 2/2 txes (0.0s)##
等待收据。
⠙ [00:00:07] [##############################################################################################################################################################################] 2/2 receipts (0.0s)
##### sepolia
✅ [成功]Hash: 0x12451b5a7e8cb5fe13389d01c9c86a2ac18610dd5d35932a810e39d263e8c935
合约地址:0x7258c917eBE13c54a71a81D50e9652a847dceA21
区块:6572863
支付:0.000235367593825194 ETH (1481877 gas * 0.158830722 gwei
就这样!如果你看完这个过程,你已经走了很远。ERC-777是一个复杂的代币标准,但你应该始终参考原始EIP。
在本指南中,我们涵盖了如何使用Foundry创建、测试和部署ERC-777代币和ERC-777接收者合约。你现在对ERC-777代币如何工作有了基础的理解,并且你已经实现了代币和接收者合约以展示钩子和操作员的功能。
通过在Twitter(@QuickNode)上关注我们或加入我们的Discord社区,保持对最新区块链开发工具和见解的了解。我们迫不及待想看到你接下来会构建出什么!
让我们知道如果你有任何反馈或对新主题的请求。我们很想听到你的看法。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!