作者:三襟(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为空