作者:三襟(ERC6551 Space)
ERC-6551提案赋予了NFT钱包的功能,允许NFT拥有资产并可与Dapp交互。此提案无需更改现有智能合约机制和基础设施。
该提案旨在赋予每个 NFT 与以太坊账户相同的权利。这包括自我托管资产、执行任意操作、控制多个独立账户以及跨多个链使用账户的能力。通过这样做,该提案允许使用反映 Etherem 现有所有权模型的通用模式将复杂的现实世界资产表示为 NFT。
实现方式:通过定义一个单例注册表,该注册表为所有现有和未来的 NFT 分配唯一的、确定性的智能合约帐户地址。每个账户都永久绑定到一个 NFT,并将该账户的控制权授予该 NFT 持有者。此账户兼容所有现有的链上资产标准,并且可以扩展以支持未来创建的新资产标准。
核心实现:
①NFT绑定账户的单例注册表
②NFT绑定账户实现的通用接口 关于核心实现,我们先来看看ERC-6551的功能关系图:
此图说明了NFT、NFT 持有者、代币绑定账户和注册表之间的关系:假设张三拥有一个BAYC的NFT,张三通过注册表Registry(稍后我们会在技术层面详细介绍注册表)去生成了一个抽象账户,此抽象账户与张三的BAYC唯一绑定,另外此账户可以存储所有的EVM链资产,包括NFT、ERC20代币、ETH等。ERC-6551协议将该账户的控制权授予张三,若张三哪一天将他的BAYC卖给了李四,那么该账户的控制权就转移给了李四,此账户下的所有资产也转给了李四。
注册表主要有两个功能:
①createAccount:给NFT创建绑定的抽象账户 ②计算 NFT 的代币绑定账户地址 注册表必须用Nick’s Factory(0x4e59b44847b379578588920cA78FbF26c0B4956C)并附带加盐字节码0x0000000000000000000000000000000000000000fd8eb4e1dca713016c518e31部署在合约0x000000006551c19487814612e58FE06813775758 上
交易详细信息:
{
"to": "0x4e59b44847b379578588920ca78fbf26c0b4956c",
"value": "0x0",
"data": "0x0000000000000000000000000000000000000000fd8eb4e1dca713016c518e31608060405234801561001057600080fd5b5061023b806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c8063246a00211461003b5780638a54c52f1461006a575b600080fd5b61004e6100493660046101b7565b61007d565b6040516001600160a01b03909116815260200160405180910390f35b61004e6100783660046101b7565b6100e1565b600060806024608c376e5af43d82803e903d91602b57fd5bf3606c5285605d52733d60ad80600a3d3981f3363d3d373d3d3d363d7360495260ff60005360b76055206035523060601b60015284601552605560002060601b60601c60005260206000f35b600060806024608c376e5af43d82803e903d91602b57fd5bf3606c5285605d52733d60ad80600a3d3981f3363d3d373d3d3d363d7360495260ff60005360b76055206035523060601b600152846015526055600020803b61018b578560b760556000f580610157576320188a596000526004601cfd5b80606c52508284887f79f19b3655ee38b1ce526556b7731a20c8f218fbda4a3990b6cc4172fdf887226060606ca46020606cf35b8060601b60601c60005260206000f35b80356001600160a01b03811681146101b257600080fd5b919050565b600080600080600060a086880312156101cf57600080fd5b6101d88661019b565b945060208601359350604086013592506101f46060870161019b565b94979396509194608001359291505056fea2646970667358221220ea2fe53af507453c64dd7c1db05549fa47a298dfb825d6d11e1689856135f16764736f6c63430008110033",
}
对于上述交易: to:0x4e59b44847b379578588920ca78fbf26c0b4956c to即为交互合约,此合约实现了creat2功能,可快速加盐部署可确定地址的合约。 Value:给to合约发送的以太币数量 data:发送给to合约的字节码,data的前32字节即为需要传入的盐,0x6080604052即为创建新合约的字节码,后面一大串是针对于新创建的合约的字节码(此处不再详细介绍,若有兴趣请自学evm的opcode操作码)
注册表源码:
import "@openzeppelin/contracts/utils/Create2.sol";
interface IERC6551Registry {
/// @dev The registry SHALL emit the AccountCreated event upon successful account creation
event AccountCreated(
address account,
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt
);
/// @dev Creates a token bound account for an ERC-721 token.
///
/// @dev If account has already been created, returns the account address without calling create2.
///
/// @dev If initData is not empty and account has not yet been created, calls account with provided initData after creation.
///
/// @dev Emits AccountCreated event.
///
/// @return the address of the account
function createAccount(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt,
bytes calldata initData
) external returns (address);/// @dev Returns the computed address of a token bound account
///
/// @return The computed address of the account
function account(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt
) external view returns (address);
}contract SampleAccountRegistry is IERC6551Registry {
error InitializationFailed();
/*
ERC-1167 Header (10 bytes)
<implementation (address)> (20 bytes)
ERC-1167 Footer (15 bytes)
<salt (uint256)> (32 bytes)
<chainId (uint256)> (32 bytes)
<tokenContract (address)> (32 bytes)
<tokenId (uint256)> (32 bytes)
*/
function createAccount(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt,
bytes calldata initData
) external returns (address) {
bytes memory code = _creationCode(implementation, chainId, tokenContract, tokenId, salt);address _account = Create2.computeAddress(
bytes32(salt),
keccak256(code)
);if (_account.code.length != 0) return _account;_account = Create2.deploy(0, bytes32(salt), code);if (initData.length != 0) {
(bool success, ) = _account.call(initData);
if (!success) revert InitializationFailed();
}emit AccountCreated(
_account,
implementation,
chainId,
tokenContract,
tokenId,
salt
);return _account;
}function account(
address implementation,
uint256 chainId,
address tokenContract,
uint256 tokenId,
uint256 salt
) external view returns (address) {
bytes32 bytecodeHash = keccak256(
_creationCode(implementation, chainId, tokenContract, tokenId, salt)
);return Create2.computeAddress(bytes32(salt), bytecodeHash);
}function *creationCode(
address implementation*,
uint256 chainId_,
address tokenContract_,
uint256 tokenId_,
uint256 salt_
) internal pure returns (bytes memory) {
return
abi.encodePacked(
hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73",
implementation_,
hex"5af43d82803e903d91602b57fd5bf3",
abi.encode(salt_, chainId_, tokenContract_, tokenId_)
);
}
}
在上诉代码中,注册表必须将每个NFT绑定帐户部署为 ERC-1167(若您不了解ERC-1167,请自行参阅eip官方文档) 最小代理,并将不可变的常量数据附加到字节码,每个NFT绑定帐户必须具有以下结构:
ERC-1167 Header (10 bytes) <implementation (address)> (20 bytes) ERC-1167 Footer (15 bytes) <salt (uint256)> (32 bytes) <chainId (uint256)> (32 bytes) <tokenContract (address)> (32 bytes) <tokenId (uint256)> (32 bytes) 例:实现地址为 0xbebebebebebebebebebebebebebebebebebebebebebe、salt 为0、链 ID 为1、代币合约为 0xcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf 和代币 ID 为123 的代币绑定账户将具有以下部署字节码:(173字节) 363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000cfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcfcf000000000000000000000000000000000000000000000000000000000000007b
在createAccount函数里,有个内部函数_creationCode,此函数负责打包部署字节码,此处再次给出本函数:
function *creationCode(
address implementation*,
uint256 chainId_,
address tokenContract_,
uint256 tokenId_,
uint256 salt_
) internal pure returns (bytes memory) {
return
abi.encodePacked(
hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73",
implementation_,
hex"5af43d82803e903d91602b57fd5bf3",
abi.encode(salt_, chainId_, tokenContract_, tokenId_)
);
}
其中3d60ad80600a3d3981f3363d3d373d3d3d363d73和5af43d82803e903d91602b57fd5bf3均可以与上面的例子匹配。 现在,我们来分析这些操作码:这些操作码是EVM的底层操作码,为16进制,需两两读取
本文章只介绍两个关键的操作码: 3d :RETURNDATASIZE,这是solidity的return关键字的基础之一,是将returnData的大小推入堆栈 f3:return操作,将之前的计算值读出来,即从指定的内存位置提取数据,存储到returnData中,并终止当前的操作。此指令需要从堆栈中取出两个参数:内存的起始位置mem_offset和数据的长度length 我们将前10个字节:3d60ad80600a3d3981f3反编译得到:
contract Contract {
function main() {
var var0 = returndata.length;
memory[returndata.length:returndata.length + 0xad] = code[0x0a:0xb7];
return memory[var0:var0 + 0xad];
}
}
此段代码为克隆合约的构造方法,内容是将整个克隆合约的字节码返回给 EVM
作为opcode更直观更底层的描述:
// Inputs[3]
// {
// @0000 returndata.length
// @0006 returndata.length
// @0009 memory[returndata.length:returndata.length + 0xad]
// }
0000 3D RETURNDATASIZE
0001 60 PUSH1 0xad
0003 80 DUP1
0004 60 PUSH1 0x0a
0006 3D RETURNDATASIZE
0007 39 CODECOPY
0008 81 DUP2
0009 F3 *RETURN
// Stack delta = +1
// Outputs[3]
// {
// @0000 stack[0] = returndata.length
// @0007 memory[returndata.length:returndata.length + 0xad] = code[0x0a:0xb7]
// @0009 return memory[returndata.length:returndata.length + 0xad];
// }
// Block terminates
我们再将其后的173字节的opcode反编译得到:
contract Contract {
function main() {
var temp0 = msg.data.length;
memory[returndata.length:returndata.length + temp0] = msg.data[returndata.length:returndata.length + temp0];
var temp1 = returndata.length;
var temp2;
temp2, memory[returndata.length:returndata.length + returndata.length] = address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length]);
var temp3 = returndata.length;
memory[temp1:temp1 + temp3] = returndata[temp1:temp1 + temp3];
var var1 = temp1;
var var0 = returndata.length;
if (temp2) { return memory[var1:var1 + var0]; }
else { revert(memory[var1:var1 + var0]); }
}
}
这部分内容是利用 delegatecall 将调用进行转发的逻辑。
转化成opcode底层实现为:
label_0000:
// Inputs[15]
// {
// @0000 msg.data.length
// @0001 returndata.length
// @0002 returndata.length
// @0003 msg.data[returndata.length:returndata.length + msg.data.length]
// @0004 returndata.length
// @0005 returndata.length
// @0006 returndata.length
// @0007 msg.data.length
// @0008 returndata.length
// @001E msg.gas
// @001F address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length])
// @001F memory[returndata.length:returndata.length + msg.data.length]
// @0020 returndata.length
// @0023 returndata[returndata.length:returndata.length + returndata.length]
// @0025 returndata.length
// }
0000 36 CALLDATASIZE
0001 3D RETURNDATASIZE
0002 3D RETURNDATASIZE
0003 37 CALLDATACOPY
0004 3D RETURNDATASIZE
0005 3D RETURNDATASIZE
0006 3D RETURNDATASIZE
0007 36 CALLDATASIZE
0008 3D RETURNDATASIZE
0009 73 PUSH20 0xbebebebebebebebebebebebebebebebebebebebe
001E 5A GAS
001F F4 DELEGATECALL
0020 3D RETURNDATASIZE
0021 82 DUP3
0022 80 DUP1
0023 3E RETURNDATACOPY
0024 90 SWAP1
0025 3D RETURNDATASIZE
0026 91 SWAP2
0027 60 PUSH1 0x2b
0029 57 *JUMPI
// Stack delta = +2
// Outputs[5]
// {
// @0003 memory[returndata.length:returndata.length + msg.data.length] = msg.data[returndata.length:returndata.length + msg.data.length]
// @001F memory[returndata.length:returndata.length + returndata.length] = address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length])
// @0023 memory[returndata.length:returndata.length + returndata.length] = returndata[returndata.length:returndata.length + returndata.length]
// @0024 stack[1] = returndata.length
// @0026 stack[0] = returndata.length
// }
// Block ends with conditional jump to 0x002b, if address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length])label_002A:
// Incoming jump from 0x0029, if not address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length])
// Inputs[3]
// {
// @002A stack[-1]
// @002A memory[stack[-1]:stack[-1] + stack[-2]]
// @002A stack[-2]
// }
002A FD *REVERT
// Stack delta = -2
// Outputs[1] { @002A revert(memory[stack[-1]:stack[-1] + stack[-2]]); }
// Block terminateslabel_002B:
// Incoming jump from 0x0029, if address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length])
// Inputs[3]
// {
// @002C stack[-2]
// @002C memory[stack[-1]:stack[-1] + stack[-2]]
// @002C stack[-1]
// }
002B 5B JUMPDEST
002C F3 *RETURN
// Stack delta = -2
// Outputs[1] { @002C return memory[stack[-1]:stack[-1] + stack[-2]]; }
// Block terminates002D 00 *STOP
002E 00 *STOP
002F 00 *STOP
0030 00 *STOP
0031 00 *STOP
0032 00 *STOP
0033 00 *STOP
0034 00 *STOP
0035 00 *STOP
0036 00 *STOP
0037 00 *STOP
0038 00 *STOP
0039 00 *STOP
003A 00 *STOP
003B 00 *STOP
003C 00 *STOP
003D 00 *STOP
003E 00 *STOP
003F 00 *STOP
0040 00 *STOP
0041 00 *STOP
0042 00 *STOP
0043 00 *STOP
0044 00 *STOP
0045 00 *STOP
0046 00 *STOP
0047 00 *STOP
0048 00 *STOP
0049 00 *STOP
004A 00 *STOP
004B 00 *STOP
004C 00 *STOP
004D 00 *STOP
004E 00 *STOP
004F 00 *STOP
0050 00 *STOP
0051 00 *STOP
0052 00 *STOP
0053 00 *STOP
0054 00 *STOP
0055 00 *STOP
0056 00 *STOP
0057 00 *STOP
0058 00 *STOP
0059 00 *STOP
005A 00 *STOP
005B 00 *STOP
005C 00 *STOP
005D 00 *STOP
005E 00 *STOP
005F 00 *STOP
0060 00 *STOP
0061 00 *STOP
0062 00 *STOP
0063 00 *STOP
0064 00 *STOP
0065 00 *STOP
0066 00 *STOP
0067 00 *STOP
0068 00 *STOP
0069 00 *STOP
006A 00 *STOP
006B 00 *STOP
006C 01 ADD
006D 00 *STOP
006E 00 *STOP
006F 00 *STOP
0070 00 *STOP
0071 00 *STOP
0072 00 *STOP
0073 00 *STOP
0074 00 *STOP
0075 00 *STOP
0076 00 *STOP
0077 00 *STOP
0078 00 *STOP
0079 CF CF
007A CF CF
007B CF CF
007C CF CF
007D CF CF
007E CF CF
007F CF CF
0080 CF CF
0081 CF CF
0082 CF CF
0083 CF CF
0084 CF CF
0085 CF CF
0086 CF CF
0087 CF CF
0088 CF CF
0089 CF CF
008A CF CF
008B CF CF
008C CF CF
008D 00 *STOP
008E 00 *STOP
008F 00 *STOP
0090 00 *STOP
0091 00 *STOP
0092 00 *STOP
0093 00 *STOP
0094 00 *STOP
0095 00 *STOP
0096 00 *STOP
0097 00 *STOP
0098 00 *STOP
0099 00 *STOP
009A 00 *STOP
009B 00 *STOP
009C 00 *STOP
009D 00 *STOP
009E 00 *STOP
009F 00 *STOP
00A0 00 *STOP
00A1 00 *STOP
00A2 00 *STOP
00A3 00 *STOP
00A4 00 *STOP
00A5 00 *STOP
00A6 00 *STOP
00A7 00 *STOP
00A8 00 *STOP
00A9 00 *STOP
00AA 00 *STOP
00AB 00 *STOP
00AC 7B PUSH28 0x
另外:abi.encode(salt_, chainId_, tokenContract_, tokenId_)这行代码是将 salt_、chainId_、tokenContract_、tokenId_ 等数据拼接在 EIP-1167 的代理字节码后面,以便在创建的合约钱包中可以直接通过字节码读取到这些数据。
每一个NFT绑定账户必须将执行操作通过delegateCall操作委托给实现了IERC6551Account 接口的合约。下面是IERC6551Account 接口源码:
/// @dev the ERC-165 identifier for this interface is `0xeff4d378`
interface IERC6551Account {
event TransactionExecuted(address indexed target, uint256 indexed value, bytes data);
receive() external payable;function executeCall(
address to,
uint256 value,
bytes calldata data
) external payable returns (bytes memory);function token()
external
view
returns (uint256 chainId, address tokenContract, uint256 tokenId);function owner() external view returns (address);function nonce() external view returns (uint256);
}下面是实现了IERC6551Account 接口的示例代码:contract SampleAccount is IERC165, IERC1271, IERC6551Account, NonceManager {
receive() external payable {}function executeCall(
address to,
uint256 value,
bytes calldata data
) external payable returns (bytes memory result) {
require(msg.sender == owner(), "Not token owner");bool success;
(success, result) = to.call{value: value}(data);if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}function token()
external
view
returns (
uint256 chainId,
address tokenContract,
uint256 tokenId
)
{
uint256 length = address(this).code.length;
return (
abi.decode(
Bytecode.codeAt(address(this), length - 0x60, length),
(uint256, address, uint256)
)
);
}function owner() public view returns (address) {
(
uint256 chainId,
address tokenContract,
uint256 tokenId
) = this.token();
if (chainId != block.chainid) return address(0);return IERC721(tokenContract).ownerOf(tokenId);
}function nonce() public view virtual returns (uint256) {
return this.getNonce(address(this), 0);
}function supportsInterface(bytes4 interfaceId) public pure returns (bool) {
return (interfaceId == type(IERC165).interfaceId ||
interfaceId == type(IERC6551Account).interfaceId);
}function isValidSignature(bytes32 hash, bytes memory signature)
external
view
returns (bytes4 magicValue)
{
bool isValid = SignatureChecker.isValidSignatureNow(
owner(),
hash,
signature
);if (isValid) {
return IERC1271.isValidSignature.selector;
}return "";
}
}
ERC-6551应当注意的骗局:
可能存在以下骗局:
- Alice 拥有 ERC-721 代币 X,该代币拥有代币绑定账户 Y。 Alice 将 10ETH 存入账户 Y Bob 提出通过去中心化市场以 11ETH 的价格购买代币 X,假设他将收到账户 Y 中存储的 10ETH 以及账户Y绑定的代币X Alice从账户Y中提取10ETH,并立即接受Bob的报价 Bob收到代币X,但账户Y为空