# EIP 721: ERC-721 非同质化代币标准

作者 状态 类型 分类 创建时间 依赖
William Entriken github.com@phor.net, Dieter Shirley dete@axiomzen.co, Jacob Evans jacob@dekz.net, Nastassia Sachs nastassia.sachs@protonmail.com Final Standards Track ERC 2018-01-24 165

# 简要说明

非同质化(Non-Fungible Token,以下简称 NFT 或 NFTs)代币标准。

译者注: ERC-721 标准提出时,solidity 版本是0.4.20,所有此标准在0.4.20基础上编写。

# 摘要

提案描述了智能合约中实现 NFT 的标准API。 标准提供了跟踪和转移NFTs的基本功能。

提案考虑了个人拥有和交易NFT的场景,以及委托给第三方(如:代理人/钱包/拍卖人,称为 “操作员”)的情况。

NFT可以代表对数字或物理资产的所有权。 我们考虑了资产的多样性,并且我们知道您会想要代表更多:

  • 实体财产 - 如:房屋,独特的艺术品
  • 虚拟收藏品 - 如:小猫,收藏卡的独特图片
  • “负”资产 - 如:贷款等

总的来说,所有房屋都是不同的,没有两只小猫是一样的。 NFT是可区分的,必须分别跟踪每个所有者。

# 动机

标准接口可以方便 钱包/代理/拍卖应用 与以太坊上的任何 NFT 进行交互。 提供简单的ERC-721智能合约以及跟踪数量众多 NFT的合约。 其他应用将在下面讨论。

该标准的灵感来自ERC20代币标准,并建立自EIP-20创建以来的两年经验。 ERC20 无法跟踪NFT,因为每种资产都是不同的(不可替代的),而ERC20每个代币的数量都是相同的(可替代的)。

该标准与ERC20之间的差异在下面进行说明。

# 规范

每个符合ERC-721的合同都必须实现 ERC721ERC165 接口(受以下“说明”约束):

pragma solidity ^0.4.20;

/// @title ERC-721非同质化代币标准
/// @dev See https://learnblockchain.cn/docs/eips/eip-721.html
///  Note: ERC-165 接口id 为 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
    /// @dev 当任何NFT的所有权更改时(不管哪种方式),就会触发此事件。
    ///  包括在创建时(`from` == 0)和销毁时(`to` == 0), 合约创建时除外。
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

    /// @dev 当更改或确认NFT的授权地址时触发。
    ///  零地址表示没有授权的地址。
    ///  发生 `Transfer` 事件时,同样表示该NFT的授权地址(如果有)被重置为“无”(零地址)。
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

    /// @dev 所有者启用或禁用操作员时触发。(操作员可管理所有者所持有的NFTs)
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

    /// @notice 统计所持有的NFTs数量
    /// @dev NFT 不能分配给零地址,查询零地址同样会异常
    /// @param _owner : 待查地址
    /// @return 返回数量,也许是0
    function balanceOf(address _owner) external view returns (uint256);

    /// @notice 返回所有者
    /// @dev NFT 不能分配给零地址,查询零地址抛出异常
    /// @param _tokenId NFT 的id
    /// @return 返回所有者地址
    function ownerOf(uint256 _tokenId) external view returns (address);

    /// @notice 将NFT的所有权从一个地址转移到另一个地址
    /// @dev 如果`msg.sender` 不是当前的所有者(或授权者)抛出异常
    /// 如果 `_from` 不是所有者、`_to` 是零地址、`_tokenId` 不是有效id 均抛出异常。
    ///  当转移完成时,函数检查  `_to` 是否是合约,如果是,调用 `_to`的 `onERC721Received` 并且检查返回值是否是 `0x150b7a02` (即:`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`)  如果不是抛出异常。
    /// @param _from :当前的所有者
    /// @param _to :新的所有者
    /// @param _tokenId :要转移的token id.
    /// @param data : 附加额外的参数(没有指定格式),传递给接收者。
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

    /// @notice 将NFT的所有权从一个地址转移到另一个地址,功能同上,不带data参数。
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice 转移所有权 -- 调用者负责确认`_to`是否有能力接收NFTs,否则可能永久丢失。
    /// @dev 如果`msg.sender` 不是当前的所有者(或授权者、操作员)抛出异常
    /// 如果 `_from` 不是所有者、`_to` 是零地址、`_tokenId` 不是有效id 均抛出异常。
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

    /// @notice 更改或确认NFT的授权地址
    /// @dev 零地址表示没有授权的地址。
    ///  如果`msg.sender` 不是当前的所有者或操作员
    /// @param _approved 新授权的控制者
    /// @param _tokenId : token id
    function approve(address _approved, uint256 _tokenId) external payable;

    /// @notice 启用或禁用第三方(操作员)管理 `msg.sender` 所有资产
    /// @dev 触发 ApprovalForAll 事件,合约必须允许每个所有者可以有多个操作员。
    /// @param _operator 要添加到授权操作员列表中的地址
    /// @param _approved True 表示授权, false 表示撤销
    function setApprovalForAll(address _operator, bool _approved) external;

    /// @notice 获取单个NFT的授权地址
    /// @dev 如果 `_tokenId` 无效,抛出异常。
    /// @param _tokenId :  token id
    /// @return 返回授权地址, 零地址表示没有。
    function getApproved(uint256 _tokenId) external view returns (address);

    /// @notice 查询一个地址是否是另一个地址的授权操作员
    /// @param _owner 所有者
    /// @param _operator 代表所有者的授权操作员
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

interface ERC165 {
    /// @notice 是否合约实现了接口
    /// @param interfaceID  ERC-165定义的接口id
    /// @dev 函数要少于  30,000 gas.
    /// @return 合约实现了 `interfaceID`(不为  0xffffffff)返回`true` , 否则false.
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

如果合约(应用)要接受NFT的安全转账,则必须实现以下接口

/// @dev 按 ERC-165 标准,接口id为 0x150b7a02.
interface ERC721TokenReceiver {
    /// @notice 处理接收NFT
    /// @dev ERC721智能合约在`transfer`完成后,在接收这地址上调用这个函数。
    /// 函数可以通过revert 拒绝接收。返回非`0x150b7a02` 也同样是拒绝接收。
    /// 注意: 调用这个函数的 msg.sender是ERC721的合约地址
    /// @param _operator :调用 `safeTransferFrom` 函数的地址。
    /// @param _from :之前的NFT拥有者
    /// @param _tokenId : NFT token id
    /// @param _data : 附加信息
    /// @return 正确处理时返回 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
    function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

以下元信息扩展是可选的(查看后面的“说明”部分),但是可以提供一些资产代表的信息以便查询。

/// @title ERC-721非同质化代币标准, 可选元信息扩展
/// @dev See https://learnblockchain.cn/docs/eips/eip-721.html
///  Note: 按 ERC-165 标准,接口id为  0x5b5e139f.
interface ERC721Metadata /* is ERC721 */ {
    /// @notice NFTs 集合的名字
    function name() external view returns (string _name);

    /// @notice NFTs 缩写代号
    function symbol() external view returns (string _symbol);

    /// @notice 一个给定资产的唯一的统一资源标识符(URI)
    /// @dev 如果 `_tokenId` 无效,抛出异常. URIs在 RFC 3986 定义,
    /// URI 也许指向一个 符合 "ERC721 元数据 JSON Schema" 的 JSON 文件
    function tokenURI(uint256 _tokenId) external view returns (string);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

以下是 "ERC721 元数据 JSON Schema" 描述:

{
    "title": "Asset Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "指示NFT代表什么"
        },
        "description": {
            "type": "string",
            "description": "描述NFT 代表的资产"
        },
        "image": {
            "type": "string",
            "description": "指向NFT表示资产的资源的URI(MIME 类型为 image/*) , 可以考虑宽度在320到1080像素之间,宽高比在1.91:1到4:5之间的图像。
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

以下枚举扩展信息是可选的(查看后面的“说明”部分),但是可以提供NFTs的完整列表,以便NFT可被发现。

/// @title ERC-721非同质化代币标准枚举扩展信息
/// @dev See https://learnblockchain.cn/docs/eips/eip-721.html
///  Note: 按 ERC-165 标准,接口id为 0x780e9d63.
interface ERC721Enumerable /* is ERC721 */ {
    /// @notice  NFTs 计数
    /// @return  返回合约有效跟踪(所有者不为零地址)的 NFT数量
    function totalSupply() external view returns (uint256);

    /// @notice 枚举索引NFT
    /// @dev 如果 `_index` >= `totalSupply()` 则抛出异常
    /// @param _index 小于 `totalSupply()`的索引号
    /// @return 对应的token id(标准不指定排序方式)
    function tokenByIndex(uint256 _index) external view returns (uint256);

    /// @notice 枚举索引某个所有者的 NFTs
    /// @dev  如果 `_index` >= `balanceOf(_owner)` 或 `_owner` 是零地址,抛出异常
    /// @param _owner 查询的所有者地址
    /// @param _index 小于 `balanceOf(_owner)` 的索引号
    /// @return 对应的token id (标准不指定排序方式)
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 说明

Solidity 0.4.20 接口语法不足以表达 ERC-721 标准文档,ERC-721标准的合约还必须遵守以下规则:

  • Solidity issue #3412: 上面的接口为每个函数包括明确的可变性声明。 可变性声明从弱到强依次为:payable,隐含不可支付, viewpure。 实现必须满足此接口中的可变性声明,或使用更强的可变性声明。 例如,接口中的 payable 函数可以在合约中实现为不可支付(即未指定)。
  • Solidity issue #3419: 实现 ERC721MetadataERC721Enumerable 接口同时也需要实现 ERC721, ERC-721 要求实现ERC-165接口。
  • Solidity issue #2330: 标记为 external 的函数也可以使用 public 可见性。

# 原理阐述

许多的智能合约提案依赖于跟踪可区分的资产。 如 Decentraland的LAND,CryptoPunk中的eponymous punks,以及使用DMarket或EnjinCoin等系统的游戏内物品。 将来的用途包括跟踪现实世界资产,例如房地产(如Ubitquity或Propy等公司所设想的)。在这些情况下,这些项目不能像账本中的数字那样“集中”在一起,而是每个资产必须单独和原子地跟踪所有权。不管资产的性质如何,如果具有允许跨职能资产管理和销售平台的标准化接口,那么生态系统将变得更加强大。

"NFT" 选词

"NFT" 几乎是所有接受调查的人都满意的词,并且广泛适用于可区分的数字资产领域。

考虑的替代方案:可区分的资产,所有权,代币(通证),资产,权益,票据

NFT 身份ID

每个NFT都由ERC-721智能合约内部的唯一 uint256 ID标识。 该识别码在整个协议期内均不得更改。 (合约地址, tokenId) 对将成为以太坊链上特定资产的全球唯一且完全合格的标识符。 尽管某些ERC-721智能合约可能会方便地以ID为0起始并为每个新的NFT加1,但调用者不得假设ID号具有任何特定的模式,并且必须将ID视为“黑匣子” 。 另请注意,NFT可能会变得无效(被销毁)。 请参阅支持的枚举接口的枚举功能。

由于UUIDs和sha3哈希可以直接转换为 uint256 ,因此使用 uint256 可实现更广泛的应用。

代币转移(或称转账)

ERC-721 标准有两个转移函数safeTransferFrom (重载了带 data 和不带 data 参数 二种函数形式) 及 transferFrom,转移可由一下角色发起:

  • NFT的所有者
  • NFT的被授权(approved)地址
  • NFT当前所有者授权的(authorized)操作员

此外, 授权的操作员也可以为NFT设置授权(approved)地址,这可以为钱包、经纪人和拍卖应用提供一套强有力的工具,方便快速使用大量的NFT。

转移方法仅仅列出了特定条件下需要抛出异常的情况,我们自己的实现也可以在其他情况下抛出异常, 这可以实现一些有趣效果:

  • 如果合约暂定,可以禁用转账 — 如 CryptoKitties 合约(611行)
  • 将接收NFT的某些地址列入黑名单 — 如 CryptoKitties 合约(565, 566行)
  • 禁用非安全的转账 — 除非 _tomsg.sendercountOf(_to) 为非零 或之前是非零(这些情况是安全的) , 否则 transferFrom 抛出异常。
  • 向交易双方收取费用 — 从零地址向非零地址授权( approve )可以要求支付费用,还原到( approve) 零地址时退款;调用任何transfer时要求支付费用;要求 transfer 参数 _to 等于 msg.sender ;要求 transfer 参数 _to 是 NFT 授权(approved)的地址
  • 只读的NFT注册表 — 使用函数 unsafeTransfer, transferFrom, approve 以及 setApprovalForAll 时都抛出异常。

Failed transactions will throw, a best practice identified in ERC-223, ERC-677, ERC-827 and OpenZeppelin's implementation of SafeERC20.sol. ERC-20 defined an allowance feature, this caused a problem when called and then later modified to a different amount, as on OpenZeppelin issue #438. In ERC-721, there is no allowance because every NFT is unique, the quantity is none or one. Therefore we receive the benefits of ERC-20's original design without problems that have been later discovered.

Creating of NFTs ("minting") and destruction NFTs ("burning") is not included in the specification. Your 合约 may implement these by other means. Please see the event documentation for your responsibilities when creating or destroying NFTs.

We questioned if the operator parameter on onERC721Received was necessary. In all cases we could imagine, if the 操作员 was important then that 操作员 could transfer the token to themself and then send it -- then they would be the from address. This seems contrived because we consider the 操作员 to be a temporary owner of the token (and transferring to themself is redundant). When the 操作员 sends the token, it is the 操作员 acting on their own accord, NOT the 操作员 acting on behalf of the token holder. This is why the 操作员 and the previous token owner are both significant to the token recipient.

Alternatives considered: only allow two-step ERC-20 style transaction, require that transfer functions never throw, require all functions to return a boolean indicating the success of the operation.

ERC-165 接口

We chose Standard Interface Detection (ERC-165) to expose the interfaces that a ERC-721 智能合约 supports.

A future EIP may create a global registry of interfaces for 合约s. We strongly support such an EIP and it would allow your ERC-721 implementation to implement ERC721Enumerable, ERC721Metadata, or other interfaces by delegating to a separate 合约.

Gas 及复杂度 (regarding the enumeration extension)

This specification contemplates implementations that manage a few and arbitrarily large numbers of NFTs. If your application is able to grow then avoid using for/while loops in your code (see CryptoKitties bounty issue #4). These indicate your 合约 may be unable to scale and gas costs will rise over time without bound.

We have deployed 合约, XXXXERC721, to Testnet which instantiates and tracks 340282366920938463463374607431768211456 different deeds (2^128). That's enough to assign every IPV6 address to an Ethereum account owner, or to track ownership of nanobots a few micron in size and in aggregate totalling half the size of Earth. You can query it from the blockchain. And every function takes less gas than querying the ENS.

This illustration makes clear: the ERC-721 standard scales.

Alternatives considered: remove the asset enumeration function if it requires a for-loop, return a Solidity array type from enumeration functions.

Privacy

Wallets/brokers/auctioneers identified in the motivation section have a strong need to identify which NFTs an owner owns.

It may be interesting to consider a use case where NFTs are not enumerable, such as a private registry of property ownership, or a partially-private registry. However, privacy cannot be attained because an attacker can simply (!) call ownerOf for every possible tokenId.

Metadata Choices (metadata extension)

We have required name and symbol functions in the metadata extension. Every token EIP and draft we reviewed (ERC-20, ERC-223, ERC-677, ERC-777, ERC-827) included these functions.

We remind implementation authors that the empty string is a valid response to name and symbol if you protest to the usage of this mechanism. We also remind everyone that any 智能合约 can use the same name and symbol as your 合约. How a client may determine which ERC-721 智能合约 are well-known (canonical) is outside the scope of this standard.

A mechanism is provided to associate NFTs with URIs. We expect that many implementations will take advantage of this to provide metadata for each NFT. The image size recommendation is taken from Instagram, they probably know much about image usability. The URI MAY be mutable (i.e. it changes from time to time). We considered an NFT representing ownership of a house, in this case metadata about the house (image, occupants, etc.) can naturally change.

Metadata is returned as a string value. Currently this is only usable as calling from web3, not from other 合约. This is acceptable because we have not considered a use case where an on-blockchain application would query such information.

Alternatives considered: put all metadata for each asset on the blockchain (too expensive), use URL templates to query metadata parts (URL templates do not work with all URL schemes, especially P2P URLs), multiaddr network address (not mature enough)

Community Consensus

A significant amount of discussion occurred on the original ERC-721 issue, additionally we held a first live meeting on Gitter that had good representation and well advertised (on Reddit, in the Gitter #ERC channel, and the original ERC-721 issue). Thank you to the participants:

A second event was held at ETHDenver 2018 to discuss distinguishable asset standards (notes to be published).

We have been very inclusive in this process and invite anyone with questions or contributions into our discussion. However, this standard is written only to support the identified use cases which are listed herein.

# 向后兼容

We have adopted balanceOf, totalSupply, name and symbol semantics from the ERC-20 specification. An implementation may also include a function decimals that returns uint8(0) if its goal is to be more compatible with ERC-20 while supporting this standard. However, we find it contrived to require all ERC-721 implementations to support the decimals function.

Example NFT implementations as of February 2018:

  • CryptoKitties -- Compatible with an earlier version of this standard.
  • CryptoPunks -- Partially ERC-20 compatible, but not easily generalizable because it includes auction functionality directly in 合约 and uses function names that explicitly refer to the assets as "punks".
  • Auctionhouse Asset Interface -- The author needed a generic interface for the Auctionhouse ÐApp (currently ice-boxed). His "Asset" 合约 is very simple, but is missing ERC-20 compatibility, approve() functionality, and metadata. This effort is referenced in the discussion for EIP-173.

Note: "Limited edition, collectible tokens" like Curio Cards and Rare Pepe are not distinguishable assets. They're actually a collection of individual fungible tokens, each of which is tracked by its own 智能合约 with its own total supply (which may be 1 in extreme cases).

The onERC721Received function specifically works around old deployed 合约 which may inadvertently return 1 (true) in certain circumstances even if they don't implement a function (see Solidity DelegateCallReturnValue bug). By returning and checking for a magic value, we are able to distinguish actual affirmative responses versus these vacuous trues.

# 测试用例

0xcert ERC-721 Token includes test cases written using Truffle.

# 实现

0xcert ERC721 -- a reference implementation

  • MIT licensed, so you can freely use it for your projects
  • Includes test cases
  • Active bug bounty, you will be paid if you find errors

Su Squares -- an advertising platform where you can rent space and place images

  • Complete the Su Squares Bug Bounty Program to seek problems with this standard or its implementation
  • Implements the complete standard and all optional interfaces

ERC721ExampleDeed -- an example implementation

  • Implements using the OpenZeppelin project format

XXXXERC721, by William Entriken -- a scalable example implementation

  • Deployed on testnet with 1 billion assets and supporting all lookups with the metadata extension. This demonstrates that scaling is NOT a problem.

# 参考引用

Standards

  1. ERC-20 代币标准. https://learnblockchain.cn/docs/eips/eip-20.html
  2. ERC-165 接口监测. https://learnblockchain.cn/docs/eips/eip-165.html
  3. ERC-173 Owned Standard. https://learnblockchain.cn/docs/eips/eip-173.html
  4. ERC-223 代币标准. https://learnblockchain.cn/docs/eips/eip-223.html
  5. ERC-677 transferAndCall 代币标准. https://learnblockchain.cn/docs/eips/eip-677.html
  6. ERC-827 代币标准. https://learnblockchain.cn/docs/eips/eip-827.html
  7. Ethereum Name Service (ENS). https://ens.domains
  8. Instagram -- What's the Image Resolution? https://help.instagram.com/1631821640426723
  9. JSON Schema. https://json-schema.org/
  10. Multiaddr. https://github.com/multiformats/multiaddr
  11. RFC 2119 Key words for use in RFCs to Indicate Requirement Levels. https://www.ietf.org/rfc/rfc2119.txt

Issues

  1. The Original ERC-721 Issue. https://github.com/ethereum/eips/issues/721
  2. Solidity Issue #2330 -- Interface Functions are External. https://github.com/ethereum/solidity/issues/2330
  3. Solidity Issue #3412 -- Implement Interface: Allow Stricter Mutability. https://github.com/ethereum/solidity/issues/3412
  4. Solidity Issue #3419 -- Interfaces Can't Inherit. https://github.com/ethereum/solidity/issues/3419
  5. Solidity Issue #3494 -- Compiler Incorrectly Reasons About the selector Function. https://github.com/ethereum/solidity/issues/3494
  6. Solidity Issue #3544 -- Cannot Calculate Selector of Function Named transfer. https://github.com/ethereum/solidity/issues/3544
  7. CryptoKitties Bounty Issue #4 -- Listing all Kitties Owned by a User is O(n^2). https://github.com/axiomzen/cryptokitties-bounty/issues/4
  8. OpenZeppelin Issue #438 -- Implementation of approve method violates ERC20 standard. https://github.com/OpenZeppelin/zeppelin-solidity/issues/438
  9. Solidity DelegateCallReturnValue Bug. https://solidity.readthedocs.io/en/develop/bugs.html#DelegateCallReturnValue

Discussions

  1. Reddit (announcement of first live discussion). https://www.reddit.com/r/ethereum/comments/7r2ena/friday_119_live_discussion_on_erc_nonfungible/
  2. Gitter #EIPs (announcement of first live discussion). https://gitter.im/ethereum/EIPs?at=5a5f823fb48e8c3566f0a5e7
  3. ERC-721 (announcement of first live discussion). https://github.com/ethereum/eips/issues/721#issuecomment-358369377
  4. ETHDenver 2018. https://ethdenver.com

NFT Implementations and Other Projects

  1. CryptoKitties. https://www.cryptokitties.co
  2. 0xcert ERC-721 Token. https://github.com/0xcert/ethereum-erc721
  3. Su Squares. https://tenthousandsu.com
  4. Decentraland. https://decentraland.org
  5. CryptoPunks. https://www.larvalabs.com/cryptopunks
  6. DMarket. https://www.dmarket.io
  7. Enjin Coin. https://enjincoin.io
  8. Ubitquity. https://www.ubitquity.io
  9. Propy. https://tokensale.propy.com
  10. CryptoKitties Deployed Contract. https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d#code
  11. Su Squares Bug Bounty Program. https://github.com/fulldecent/su-squares-bounty
  12. XXXXERC721. https://github.com/fulldecent/erc721-example
  13. ERC721ExampleDeed. https://github.com/nastassiasachs/ERC721ExampleDeed
  14. Curio Cards. https://mycuriocards.com
  15. Rare Pepe. https://rarepepewallet.com
  16. Auctionhouse Asset Interface. https://github.com/dob/auctionhouse/blob/master/contracts/Asset.sol
  17. OpenZeppelin SafeERC20.sol Implementation. https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/token/ERC20/SafeERC20.sol

# 版权

Copyright and related rights waived via CC0.