简介
NFT 最初被创建用于表示数字或物理资产(如收藏品)的所有权。然而,它们仅限于跟踪某个 ID 及其相关元数据的所有权;它们无法拥有其他 NFT 或 ERC-20 代币,也无法像普通的 Ethereum 账户那样与 DeFi 协议进行交互。ERC-6551 改变了这一点,它为每个 NFT 赋予了专属的智能合约账户,即 Token Bound Account (TBA)。TBA 可以:
- 持有 ERC-20 代币、ETH、ERC-721、ERC-1155 以及你的钱包所支持的任何其他代币等资产
- 在 NFT 所有者(EOA)发起时执行链上交易
- 像任何其他 Ethereum 账户一样维护交易历史记录
例如,在某款区块链游戏中,你可能需要购买一个角色 NFT 才能开始游玩。借助 ERC-6551,该角色 NFT 会获得其专属的 TBA(一个智能合约账户)。随着游戏进度的推进,你的角色 TBA 会持有你积累的所有资产。
如果你决定出售这个角色 NFT,买家不仅会获得该 NFT,还会获得其 TBA,其中包含你积累的所有资产——除非你提前将它们转出。这就好比购买了一个装备齐全、经验丰富的角色,而不是从零开始。
在 ERC-6551 标准下:
- 每个 NFT 都会被分配一个或多个确定性的 TBA 地址,由 registry 合约计算得出。
- TBA 充当由 NFT 所有者控制的智能合约账户。
- 所有操作均通过 TBA 执行。
- 交易历史记录和资产均与 TBA 绑定。
这些智能合约账户是为 NFT 创建的,不需要对现有的 NFT 合约进行任何更改。由于它们与 NFT 本身解耦,因此它们向前和向后兼容当前及未来的 NFT 标准。
在本文中,你将了解 ERC-6551 标准的核心流程与机制。
前置知识
本文假设读者熟悉以下内容:
- ERC-721 标准
- EIP-1167 minimal proxy standard
- EIP-1014 (
CREATE2) - 合约的 ERC-1271 签名验证方法
ERC-6551 标准的关键特性
ERC-6551 模式通过使用 CREATE2 的确定性地址机制,将 NFT 隐式映射到智能合约地址。
具体而言,NFT 通过元组 (chainId, tokenContract, tokenId) 进行唯一标识。仅仅使用这些参数的缺点在于,每个 NFT 将被限制为只能拥有一个账户。
在某些情况下,一个 NFT 可能需要多个 TBA,就像普通的 Ethereum 用户出于不同目的(热/冷存储)拥有独立的钱包一样。这就是 salt 参数发挥作用的地方。
每个唯一的 salt 值有助于为同一个 NFT 生成不同的 TBA,从而允许单个 NFT 管理多个 TBA。这使得该元组扩展为:(chainId, tokenContract, tokenId, salt)。
回顾一下,CREATE2 部署的地址计算方式如下:
predictedAddress = address(uint160(uint256(keccak256(abi.encodePacked(
bytes1(0xff),
deployer,
salt,
keccak256(_initCode)
)))));
由于部署者的地址会影响 CREATE2 地址的计算,因此所有的 TBA 必须来自一个公共的部署者 (factory)。否则,就必须去跟踪哪个 factory 部署了哪个 TBA。
然而,这一要求带来了一个挑战:为了实现可预测的寻址,从同一个 factory 部署的 TBA 需要具有完全相同的初始化字节码(_initCode)。将不同的自定义逻辑嵌入到每个 TBA 中会改变初始化字节码,从而破坏地址的可预测性。
为了解决这个问题,每个 TBA 都会被部署为一个自定义的 EIP-1167 minimal proxy clone,它扩展了自身的字节码以包含用于账户绑定的额外不可变数据(chainId、tokenContract、tokenId 和 salt),同时将执行委托给一个独立的 implementation 合约。这既保持了初始化代码的一致性以确保地址可预测,又允许来自同一 factory 的 TBA 使用不同的 implementation。
TBA 字节码结构
从 factory 部署的每个 TBA 都具有以下字节码结构:
[EIP-1167 Minimal Proxy]
├── Header (10 bytes) - 标准代理初始化
├── Implementation (20 bytes) - 目标合约地址
└── Footer (15 bytes) - 委托逻辑
[Immutable Account Data] (将 TBA 绑定到特定 NFT 的数据)
├── Salt (32 bytes) - 用于 create2 地址推导
├── ChainID (32 bytes) - NFT 所在的链标识符
├── TokenContract (32 bytes) - NFT 合约地址
└── TokenID (32 bytes) - NFT 标识符
例如,其字节码结构将如下所示:
// ERC-1167 Proxy Section
363d3d373d3d3d363d73 // Header - copy calldata
bebebebebebebebebebebebebebebebebebebebe //Implementation
5af43d82803e903d91602b57fd5bf3 // Footer - delegate call section
// Immutable Data Section
0000...0000 // Salt (32 bytes of zeros)
0000...0001 // ChainID (1 for Ethereum mainnet)
cfcf...cfcf // NFT contract address
0000...007b // TokenID (123 in hex)
当 NFT 所有者与其 TBA 进行交互时,TBA(代理克隆)会维护其自身的状态(例如积累的 NFT 或游戏内资产),同时将逻辑委托给它所使用的 implementation 合约。
鉴于这种代理架构,还需要一个额外的参数:implementation 地址。因此,用于标识 TBA 地址的完整元组变为了 (implementation, salt, chainId, tokenContract, tokenId)。
这种标识方法与 CREATE2 相结合,允许任何人在需要时按需计算 TBA 的地址,而无需存储任何映射。因此,它消除了存储成本,同时确保每个 TBA 都拥有唯一且可预测的地址。
Registry 合约
权威的 registry 充当部署这些 TBA 的 factory。由于 TBA 采用委托执行,因此必须首先部署它们的 implementation。
要创建 TBA,registry 的 createAccount() 函数会接收所有元组参数(implementation、salt、chainId、tokenContract、tokenId)并部署一个代理克隆。

下面高亮显示的 registry 的 createAccount() function 中的汇编代码,正是通过在内存中将 EIP-1167 模式与 NFT 绑定数据相结合,从而创建出这种专用的代理字节码:

通过这种设计,任何合约都可以通过读取其字节码数据,轻松验证一个账户绑定到了哪个 NFT。本文稍后的 TBA 创建演示环节将对此进行更详细的解释。
Registry 合约函数
该 registry 必须实现 IERC6551Registry,其中定义了两个主要函数:
createAccount()
为特定的 NFT 创建一个 Token Bound Account (TBA)。

如果账户已经存在,createAccount() 只需返回计算出的账户地址,而不会调用 CREATE2。它是通过使用 iszero(extcodesize(computed)) 检查计算出的地址处是否已存在代码来实现这一点的,如下方红色箭头高亮显示;如果没有代码(size = 0),这意味着尚未存在任何账户,因此 registry 会在那个确切的地址处创建一个新的 TBA,然后 createAccount() 在第 121 行返回新创建的 TBA 地址。然而,如果代码已经存在(size > 0),它将返回现有账户的地址,而不会部署新的账户。

虽然可以使用任何 salt 值,但大多数 TBA 都是使用默认的 salt 值 bytes32(0) 部署的。正如在*“ERC-6551 标准的关键特性”*一节中提到的,使用不同的 salt 值允许一个 NFT 拥有用于不同目的的多个 TBA。
account()
为给定的 NFT 计算并返回 TBA 的确定性地址,而无需实际创建它。

AccountCreated() 事件
registry 需要触发一个事件 ERC6551AccountCreated,该事件仅在 createAccount() 成功创建新 TBA 时才会触发:

它可以帮助 dApps 和索引器跟踪每个 TBA 使用了哪个 implementation,并监控 TBA 的创建。
Registry 合约部署
在所有兼容 EVM 的链上,ERC-6551 registry 合约都被部署在一个固定地址(0x000000006551c19487814612e58FE06813775758)。
值得注意的是,地址中的“6551”部分是有意选择的靓号,这是通过不断迭代不同的 salt 值直到获得所需的地址才实现的。
可以在此处找到各个链上已部署的 registry 地址列表。
跨链 Registry 部署机制
为了确保 registry 合约在所有链上都部署在相同的地址,registry 使用了 Nick’s factory:Nick’s factory 是一个 CREATE2 factory 合约,在所有链上都部署在同一地址(0x4e59b44847b379578588920cA78FbF26c0B4956C)。它使得跨链的确定性合约部署成为可能。
Nick’s factory 使用 CREATE2 在可预测的地址部署其他合约,部署的地址是由以下内容计算得出的:factory 的地址、一个 salt 值以及合约的字节码(在本例中为 registry)。
因此,为了使 registry 合约在所有链上都部署在一个一致的地址,Nick’s factory 必须在所有链上都存在于 0x4e59b44847b379578588920cA78FbF26c0B4956C 地址。
若要在尚未部署 registry 的链上部署它,请提交以下部署交易:
{
"to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
"value": "0x0",
"data": "0x0000000000000000000000000000000000000000fd8eb4e1dca713016c518e31608060405234801561001057600080fd5b5061023b806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063246a00211461003b5780638a54c52f1461006a575b600080fd5b61004e6100493660046101b7565b61007d565b6040516001600160a01b03909116815260200160405180910390f35b61004e6100783660046101b7565b6100e1565b600060806024608c376e5af43d82803e903d91602b57fd5bf3606c5285605d52733d60ad80600a3d3981f3363d3d373d3d3d363d7360495260ff60005360b76055206035523060601b60015284601552605560002060601b60601c60005260206000f35b600060806024608c376e5af43d82803e903d91602b57fd5bf3606c5285605d52733d60ad80600a3d3981f3363d3d373d3d3d363d7360495260ff60005360b76055206035523060601b600152846015526055600020803b61018b578560b760556000f580610157576320188a596000526004601cfd5b80606c52508284887f79f19b3655ee38b1ce526556b7731a20c8f218fbda4a3990b6cc4172fdf887226060606ca46020606cf35b8060601b60601c60005260206000f35b80356001600160a01b03811681146101b257600080fd5b919050565b600080600080600060a086880312156101cf57600080fd5b6101d88661019b565b945060208601359350604086013592506101f46060870161019b565b94979396509194608001359291505056fea2646970667358221220ea2fe53af507453c64dd7c1db05549fa47a298dfb825d6d11e1689856135f16764736f6c63430008110033",
}
理解 registry 的部署交易:
to:0x4e59b44847b379578588920ca78fbf26c0b4956c(Nick’s factory 地址)value:0x0(部署时不发送任何 ETH)data: 包含代表 salt 参数的前 32 个字节:0x0000000000000000000000000000000000000000fd8eb4e1dca713016c518e31,其余字节则为 registry 合约的字节码。
这一部署交易将数据发送到 Nick’s factory,随后它提取 salt 和合约字节码以在预定义的地址部署 registry。这种方法确保了只有一个权威的 registry,并且任何人都可以进行部署。
在 registry 处理完 Token Bound Accounts 的创建之后,接下来的重点是 TBA 的 implementation。
每个 TBA 必须遵循特定的接口,这些接口定义了兼容 ERC-6551 标准所需的核心函数。这些接口可确保与钱包、市场和应用程序的正确集成。
TBA 接口
为了符合 ERC-6551 标准,每个 Token Bound Account (TBA) 的 implementation 必须实现用于账户标识和授权的 IERC6551Account、用于操作执行的 IERC6551Executable、用于接口检测的 IERC165,以及用于签名验证的 IERC1271。
每个接口(
IERC6551Account、IERC6551Executable、IERC165以及IERC1271)中的函数将分别进行解释。它们的集成将在“这些接口如何协同工作”小节中进行探讨。
账户接口 (IERC6551Account)
当 TBA 与其他合约或协议进行交互时,这些合约和协议需要一种标准化的方式来验证一些重要信息,例如:该 TBA 绑定到了哪个 NFT、交易签名者是否获得授权,以及 TBA 当前的执行状态(nonce)是什么。
IERC6551Account 接口定义了一组核心函数,以满足上述验证需求:

以下三项与我们讨论 TBA 接口相关:
token()- 从调用账户的字节码中读取嵌入的数据,并返回一个包含与该账户绑定的 NFT(chainId、合约地址、tokenId)的元组。isValidSigner()- 验证某地址是否被授权为该账户进行签名。state()- 返回一个跟踪该账户操作 nonce 的 uint256 值。
该接口的目的是定义每个 TBA implementation 必须具备的基本函数,同时为 implementation 预留添加扩展功能的空间。
每个 TBA implementation 必须也支持:
ERC-165 接口检测

允许其他合约在运行时且在与它们交互之前,验证它们是否实现了所需的 IERC6551Account 和 IERC6551Executable 接口。
ERC-1271 签名验证接口

来自 EIP-6551 官方页面 的参考 implementation 使用 ERC-1271 来解决协议如何验证 TBA 是否已授权交易的问题。它实现了 isValidSignature() 函数,该函数检查该签名对于 NFT 所有者是否有效,如果有效则返回 0x1626ba7e,如果无效则返回 bytes4(0)。
0x1626ba7e 派生自 isValidSignature(bytes32,bytes) 的函数选择器,其计算方式为:
keccak256("isValidSignature(bytes32,bytes)")
然后,获取该哈希值的前 4 个字节(bytes4)作为返回值。

执行接口 (IERC6551Executable)

IERC6551Executable 包含一个必选函数 execute(),它使得 TBA implementations 可以在被有效签名者调用时执行底层操作。
该接口没有要求必须触发的事件。
通过 execute(),TBA implementations 定义了它们支持哪些操作。operation 参数是一个 uint8 值,用于指示应执行哪种底层操作:
0 = CALL // Regular calls (sending ETH, interacting with contracts)
1 = DELEGATECALL // Execute code from another contract in TBA's context
2 = CREATE // Deploy new contracts
3 = CREATE2 // Deploy contracts with deterministic addresses
考虑这样一种场景:NFT 所有者希望其 TBA 将 ERC-20 代币存入某个协议中。这需要 NFT 所有者针对每个操作在 TBA 上调用 execute(),随后委托给 implementation 合约去执行这些交易。
授权代币:
execute(
to: tokenContract, // ERC20 token address
value: 0, // No ETH sent
data: abi.encodeWithSignature(
"approve(address,uint256)",
spender, // Protocol address to approve
amount // Amount to approve
),
operation: 0 // CALL operation
);
存入协议:
execute(
to: protocol address,
value: 0, // no ETH sent
data: depositData,// encoded deposit() call
operation: 0 // operation: CALL
);
或者仅仅发送 ETH:
execute(
to: recipient,
value: 2 ether,
data: "", // empty for ETH transfer
operation: 0 // operation: CALL
);
每一个操作都必须由有效的签名者发起调用,并且必须包含适当的错误处理机制。
IERC6551Executable 接口的灵活性在于:它并不强制要求以特定的方式来处理交易,而是仅仅要求 TBA implementations 清晰地标明(通过 ERC-165)它们支持哪种执行接口,无论是标准的 IERC6551Executable 接口还是它们自定义的执行机制。
这些接口如何协同工作
参考 implementation 强调了 IERC6551Account、IERC6551Executable、IERC165 和 ERC-1271 是如何协同工作,使得 TBA 能够执行交易并验证某条消息是否由其 NFT 所有者(EOA)签名的。正如参考 implementation 所示,这些接口构成了 TBA 运作方式的核心:

当 TBA 需要执行一笔交易时,以下验证函数就会发挥作用:
isValidSignature()(来自 ERC-1271)验证签名对所绑定 NFT 的所有者是否有效

- 它检查该 TBA 的
owner()(EOA)是否创建了该签名。参考 implementation 使用 OpenZeppelin 的 SignatureChecker 进行此验证。
isValidSigner()(来自 IERC6551Account)检查给定地址(调用者)是否被授权代表 TBA 执行交易

- 它通过核对 NFT 的所有者来验证地址。这主要在
execute()函数(来自 IERC6551Executable)中用于验证调用者。

请注意,参考 implementation 使用了一个内部函数 _isValidSigner(),该函数仅检查给定地址是否是 NFT 的所有者,但也可以包含额外的验证逻辑以授权除所有者之外的其他地址。
对于想要与其 TBA 交互的 NFT 所有者,或是对于正在与 TBA 集成的去中心化交易所(DEX)和市场,其工作流通常如下所示:
-
直接交易 (Direct Transaction):
- 用户(NFT 所有者)向其 TBA 发送交易调用
execute() execute()函数通过_isValidSigner(msg.sender)验证调用者是否获得授权- 如果已授权,TBA 将执行请求的操作,例如转移 ERC-20 代币
- 在每次执行后,存储中的
state都会递增,从而防止重放攻击。
- 用户(NFT 所有者)向其 TBA 发送交易调用
-
基于签名 (Signature-Based):
请注意,这需要协议实现 ERC-1271 支持。许多 DEX 在设计时并未广泛采用该标准,因此可能原生不支持该合约签名验证方法。-
用户使用其钱包(拥有该 NFT)对包含交易细节(例如:“从我的 TBA 中用 100 USDC 交易 0.05 ETH”)的消息进行签名
-
DEX 在 TBA 上调用
isValidSignature(),使用消息哈希和来自 NFT 所有者的签名作为参数,以验证两件事:- 签名的有效性
- 以及该签名是由绑定到 TBA 的 NFT 的当前所有者生成的,从而确认他们具有对 TBA 账户的控制权
-
如果验证通过,DEX 就知道该交易已获授权,但为了实际执行该交易,TBA 必须通过调用
execute()将代币授权给 DEX
-
execute(
to: tokenContract, // USDC token address
value: 0, // No ETH sent
data: abi.encodeWithSignature(
"approve(address,uint256)",
spender, // Protocol address to approve
100 // Amount to approve
),
operation: 0 // CALL operation
);
IERC6551Account、IERC6551Executable 和 ERC-1271 接口共同为 TBA 提供了一种标准化的方式来验证签名和执行授权交易,使得它们既兼容直接的函数调用,也兼容使用 ERC-1271 签名验证的协议。
如何为 NFT 创建 TBA
要创建 TBA,请遵循以下简单步骤:
- 创建并部署一个 implementation 合约。
- 通过调用
createAccount()并传入必要的参数:(implementation, salt, chainId, tokenContract, tokenId),与地址为0x000000006551c19487814612e58FE06813775758的 registry 进行交互。- 如果你已经有一个合适且已部署的 implementation 合约,你可以直接将其地址作为
implementation参数传入,而无需创建新的。
- 如果你已经有一个合适且已部署的 implementation 合约,你可以直接将其地址作为
在本次演示中,参考 implementation 扩展了一个权限函数,该函数将允许 NFT 所有者授予其他地址代表其 TBA 执行交易的访问权限。
定制后的参考 implementation 合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
interface IERC6551Account {
receive() external payable;
function token() external view returns (uint256 chainId, address tokenContract, uint256 tokenId);
function state() external view returns (uint256);
function isValidSigner(address signer, bytes calldata context) external view returns (bytes4 magicValue);
}
interface IERC6551Executable {
function execute(address to, uint256 value, bytes calldata data, uint8 operation)
external payable returns (bytes memory);
}
contract ERC6551Account is IERC165, IERC1271, IERC6551Account, IERC6551Executable {
uint256 public state;
/// @notice Mapping to store addresses permitted to act on behalf of the account
mapping(address => bool) public isPermitted;
receive() external payable {}
function execute(address to, uint256 value, bytes calldata data, uint8 operation)
external payable virtual returns (bytes memory result) {
require(_isValidSigner(msg.sender), "Invalid signer");
require(operation == 0, "Only call operations are supported");
++state;
bool success;
(success, result) = to.call{value: value}(data);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}
/// @notice Allows the NFT owner to grant or revoke permission for another address
function setPermission(address user, bool permitted) external {
require(msg.sender == owner(), "Only owner can set permissions");
require(user != msg.sender, "Cannot set permission for yourself");
isPermitted[user] = permitted;
}
function isValidSigner(address signer, bytes calldata) external view virtual returns (bytes4) {
return _isValidSigner(signer) ? IERC6551Account.isValidSigner.selector : bytes4(0);
}
function isValidSignature(bytes32 hash, bytes memory signature)
external view virtual returns (bytes4 magicValue) {
return SignatureChecker.isValidSignatureNow(owner(), hash, signature)
? IERC1271.isValidSignature.selector
: bytes4(0);
}
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId
|| interfaceId == type(IERC6551Account).interfaceId
|| interfaceId == type(IERC6551Executable).interfaceId;
}
function token() public view virtual returns (uint256, address, uint256) {
bytes memory footer = new bytes(0x60);
assembly {
extcodecopy(address(), add(footer, 0x20), 0x4d, 0x60)
}
return abi.decode(footer, (uint256, address, uint256));
}
function owner() public view virtual returns (address) {
(uint256 chainId, address tokenContract, uint256 tokenId) = token();
return (chainId == block.chainid) ? IERC721(tokenContract).ownerOf(tokenId) : address(0);
}
function _isValidSigner(address signer) internal view virtual returns (bool) {
return signer == owner() || isPermitted[signer];
}
}
以下是对参考 implementation 合约的新增内容:
mapping(address => bool) isPermitted:跟踪哪些地址具有代表该账户执行操作的权限setPermission(address user, bool permitted):允许 NFT 所有者授予或撤销权限的 External 函数- 更新后的
_isValidSigner()函数,现在该函数会检查签名者是否为 NFT 所有者或受权地址。
要为 NFT 创建 TBA,请遵循以下步骤:
- 克隆此 repository:
git clone https://github.com/Sayrarh/ERC-6551-Reference-Implementation.git - 安装依赖项:
npm install - 复制
.env.example的内容并填写你的 API 密钥,以此创建你的.env文件 - 打开
scripts/interaction.ts文件,将nftContractAddress替换为你的实际 NFT 地址,同时替换nfttokenId和chainId参数

然后运行该脚本:npx hardhat run scripts/interaction.ts --network sepolia 以与合约交互:

你可以在 Sepolia 网络上找到此已部署的 TBA 地址,并可在此处的浏览器上查看其交易详情。
对其字节码进行分析可以揭示出之前所描述的代理结构:

EIP-1167 Proxy 结构部分
Header (10 bytes) -> 0x363d3d373d3d3d363d
Implementation Address (20 bytes) -> 311e822a099fae1ef8fc961ddf61fafd5392e7a9 (Implementation Address)
Footer (15 bytes) -> 5af43d82803e903d91602b57fd5bf3
不可变数据部分(账户绑定)
salt (32 bytes) -> 0000000000000000000000000000000000000000000000000000000000000000
chainID (32 bytes) -> 00000000000000000000000000000000000000000000000000000000aa36a7 // chainID -> 11155111 (sepolia)
tokenContract (32 bytes) -> 0000000000000000000000006b57b7edf751829dfb2aeccf578d6d24c33a45a2 (NFT contract address)
tokenID (32 bytes) -> 0000000000000000000000000000000000000000000000000000000000000001 //token ID -> 1
该 TBA 的完整字节码显示,在 0x97212622cbdb6f1aa96c4abceaebb2b1b47d2bbe 处新创建的 TBA 会将调用转发到 0x311e822a099fae1ef8fc961ddf61fafd5392e7a9 处的 implementation 合约,并且该 TBA 与以下 NFT 绑定:
- NFT 合约地址:
0x6b57b7edf751829dfb2aeccf578d6d24c33a45a2 - tokenID:1
- chainID:11155111(Sepolia 测试网)
- salt:0(默认 salt)
因此,当调用 token() 函数时,它会读取 TBA 的字节码:

然后返回如下所示的元组:

这些值(chainId、tokenContract、tokenId)唯一地标识了该账户所绑定的 NFT,从而使得合约或应用程序能够轻松确认其关联的 NFT。
处理 NFT 转移
当拥有 TBA 的 NFT 被转移时,TBA 的所有权也会自动转移。这是因为 TBA 的所有权是通过 owner() 函数由当前的 NFT 所有者动态确定的。

- TBA 会保留其所有资产(ETH、代币、NFT)
- TBA 的控制权将转移给新的 NFT 所有者
- 一旦 NFT 发生转移,前任所有者就会失去对 TBA 拥有资产的所有访问权限,除非这些资产被提前转出,正如游戏示例中所说明的那样。
本节中使用的 implementation 是一个简单版本。EIP-6551 的作者在他们的 GitHub repository 中还提供了一个可升级的参考 implementation,允许你在需要时通过将其指向一个新的 implementation 合约来升级 TBA 的逻辑。
防止 TBA 中的所有权循环(Ownership Cycles)
可升级的 implementation 包含了防止所有权循环(ownership cycles)的保护措施,当 TBA 所有权链形成一个闭环时就会出现这种情况。
所有权循环会导致没有任何账户有权发起交易的情况,因为每个 TBA 都需要得到困在循环中的所有者(EOA)的许可。这可以通过下图说明:

为什么这不安全:
- 每个 TBA 都需要其 NFT 所有者(EOA)的许可才能进行操作
- 当 EOA 将 NFT 转移到循环中的某个 TBA 内部时,它会失去所有的控制权,因为它实际上交出了所有权。
- 在一个循环中,没有任何其他方可以发起交易,因为 EOA 不再拥有该链上的任何 NFT
- 所有的资产都将被永久锁定,没有办法打破这个循环
由于复杂的循环(深度 > 1)代表可能涉及多个嵌套 TBA 的深层所有权闭环,检测它们可能需要无限的递归检查——这使得完全的链上检测在计算上不可行。因此,应用程序有必要实施针对这些所有权循环的保护措施。
TBA 欺诈缓解
NFT 市场将需要实施保护措施,以防止恶意的 TBA 所有者进行欺诈行为。
想象这样一个场景:Bob 拥有一个角色 NFT,其关联的 TBA 中持有 0.5 ETH、2 个龙 NFT 以及游戏奖励获得的 50 USDC。Bob 在市场上将该角色 NFT 挂单出售,标价为 1 ETH,并且将 TBA 中的所有资产都作为交易的一部分包含在内。
Alice 不想从零开始玩这款游戏,于是同意购买该 NFT。然而在交易完成之前,Bob 迅速将 50 USDC 从 TBA 中提取了出来。
当交易完成时,Alice 收到的角色 NFT 其 TBA 里面只有 0.5 ETH 和 2 个龙 NFT,而 Bob 不仅获得了出售所得的 1 ETH,还保留了他撤出的 50 USDC。
为了解决此类欺诈行为,NFT 市场需要在市场层面实施保护机制,而实现该标准的合约也应包含保护措施。一些缓解策略包括:
- 在市场订单上附加一份资产承诺清单(例如特定的 ERC-20 余额、NFT 等),如果在订单履行之前承诺的资产被移除,则该报价无效。
- 让市场在挂单期间暂时获取 NFT 的所有权(而不仅仅是授权),这能防止卖家操控 TBA 的资产。
总结
ERC-6551 允许将 NFT 链接到智能合约账户(TBA),从而使它们能够拥有资产并与协议进行交互。它通过确定性地址(create2)、在字节码中存储 NFT 绑定数据的自定义修改过的 minimal proxies,以及一个权威的 registry 来实现这一点,同时完全不需要更改现有的 NFT 合约。
一些项目已经开始利用 TBA 来管理游戏内库存(持有物品、服装和装备)、社区忠诚度系统、声誉跟踪等。
随着采用率的增长,ERC-6551 将进一步将 NFT 整合进 DeFi 中,扩大其应用场景。尽管目前已被一些项目采用,但在撰写本文时,ERC-6551 仍处于审查阶段,尚未最终定稿。
如需 implementation 示例,请参阅 Tokenbound 文档或探索如 frontend SDK 和 iframe 等工具。同时,也可以使用 explorer 来查看并与 ERC-6551 账户进行交互。
参考资料
EIP-6551 标准页面
ERC-721 标准
EIP-1167 Minimal Proxy
EIP-1967 storage slot
ERC-6551 源代码
已采用 ERC-6551 的项目列表
TBA Explorer