合约界面查询标准(Standard Interface Detection)

作者:待补充

来源:Medium

原文链接:待补充

著权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

目前以太坊开发生态系统中有各式各样的合约,最有名例如ERC20代币标准、加密猫使用的NFT代币,还有其他不断出现的新概念。但即便是ERC20的正式标准也是经过一年多的时间才确立,而市面上早已存在许多不同的衍生版本。

如果合约拥有者有公布原始码、编译版本,或是使用Etherscan验证合约代码的功能,则你至少可以验证部署的合约代码是否正确,然后再从原始码来得知该合约支援哪些功能。有没有其他更方便的方式?

EIP165(Standard Interface Detection)的目的即是为了订立一个查询合约介面的标准。

EIP165

介面?

以ERC20为例,一个标准的ERC20合约必须要包含name()、balanceOf()、transfer()、transferFrom()、approve()等等的函式,这即是一个标准的ERC20介面。 但并非要包含所有规定必须要有的函式才能算是介面,由name()、balanceOf()、transfer()三个函式组成也可以叫做一个介面(只是就不会称为标准ERC20介面,而是例如ERC20TransferOnly的名称)。如果你的ERC20多包含像是mine()、burn()等的函式,则一样也是一个介面(名称可能是ERC20Mineable)。 一个合约可以有多个介面。

如何区分/识别一个介面?

每个函式都有自己的函式识别码(function identifier),例如ERC20的transfer函式的识别码是0x23b872dd,计算方式是杂凑后取前四个byte,如下。

0x23b872dd = bytes4(sha3(“transferFrom(address,address,uint256)”))

在Solidity 0.4.17版之后,函式多了一个selector值,会回传该函式的识别码,不需要再自己计算。

而一个介面的识别码则是由该介面所有的函式的识别码XOR后的值,举例如下。 pragma solidity ^0.4.20; contract InterfaceExample {     function foo(string name) returns (uint256);     function bar(address addr) returns (uint256);         function interfaceID() constant returns (bytes4) {         return this.foo.selector ^ this.bar.selector;     } }

如何确认一个合约是否支援某个界面?

如果一个合约支援EIP165,则必须要包含supportsInterface函式:

contract InterfaceExample {     // 此函式的識別碼為0x01ffc9a7     function supportsInterface(bytes4 interfaceID) external view returns (bool); }

要确认一个合约是否支援某个界面,首先要确认该合约是否支援EIP165。表示如果你呼叫这个合约的supportsInterface函式并带入参数0x01ffc9a7,它必须要回传true。

但这里要注意的是,如果你呼叫一个不存在的函式,则合约会去执行fallback函式。如果该合约有fallback函式且成功执行,则你最后都会得到true的回传值(false positive)。所以当你第一次呼叫supportsInterface(0x01ffc9a7)并得到true的回传值时,你必须要再呼叫一次supportsInterface(0xffffffff)来确认,如果supportsInterface(0xffffffff)回传true,表示该合约不支援EIP165,因为你得到的true并非supportsInterface回传的true;如果supportsInterface(0xffffffff)回传false,则表示该合约支援EIP165。

接着查询是否支援指定的界面,呼叫supportsInterface并带入你欲查询的interfaceID作为参数。以下是支援EIP165的合约简单范例: pragma solidity ^0.4.20;

contract InterfaceExample {     //注意0xffffffff不可设为true     mapping(bytes4 => bool) internal supportedInterfaces;

    function InterfaceExample() internal {         supportedInterfaces[this.supportsInterface.selector] = true;     }

    function supportsInterface(bytes4 interfaceID) external view returns (bool) {         return supportedInterfaces [interfaceID];     } }

要注意的是EIP165规定supportInterface消耗的gas要少于30000。虽然没办法强制,但你最好假设其他人呼叫你合约的supportInterface时会设定gas限额为30000,如果用超过就会导致函式因为OutOfGas结束并回传false。

EIP820(EIP165的延伸)

EIP165设计是由合约自己去实作supportsInterface函式,EIP820则是透过一个Registry合约让大家来登记自己的合约包含哪些介面,而且对象不只限于合约,单纯的使用者帐户(External Owned Account)也可以登记让其他人知道自己有哪些介面可以互动。

例如你可能想要在收到代币时能做出反应(例如触发event等),但这只有在你的帐户是一个合约的时候才有办法做到。透过EIP820,即便是单纯的使用者帐户,你可以登记如果有人转代币给你时,它要去哪个地址呼叫例如tokenFallback(或onTokenReceived)函式来完成你预期收到代币要做的事。而这也是代币标准
ERC777的目标。

登记人的资格

首先,帐户的拥有者可以为自己的帐户登记哪个地址(addr)帮自己实作哪些了哪些介面(这里介面识别码用iHash代替)。EIP820也提供了manager的机制,让帐户拥有者可以指定其他地址(可以是合约也可以是单纯的使用者帐户)来担任自己的manager,替自己登记。和manager相关的资料和函式如下:

contract ERC820Registry {     // manager改变时所触发的event     event ManagerChanged(address indexed addr, address indexed newManager);     //纪录各个地址的manager的资料     mapping (address => address) managers;     //身份检查的modifier     modifier canManage(address addr) {         require(getManager(addr) == msg.sender);         _;     }     // manager预设为自己     function getManager(address addr) public view returns(address) {         if (managers[addr] == 0) {             return addr;         } else {             return managers[addr];         }     }     function setManager(address addr, address newManager) public canManage(addr) {         managers[addr] = newManager == addr ? 0 : newManager;         ManagerChanged(addr, newManager);     } }

登记的方式

和介面登记的相关资料和函式如下: contract ERC820Registry {     event InterfaceImplementerSet(address indexed addr, bytes32 indexed interfaceHash, address indexed implementer);     mapping (address => mapping(bytes32 => address)) interfaces;     function getInterfaceImplementer(address addr, bytes32 iHash) constant public returns (address);     function setInterfaceImplementer(address addr, bytes32 iHash, address implementer) public canManage(addr); }

Implementer是实作介面的合约地址,如果是合约来登记自己提供的介面的话,Implementer会设为自己。

要注意的是EIP820的介面识别码是使用bytes32,但为了兼容EIP165的介面,在getInterfaceImplementer里会先检查是否是要查询EIP165的介面。因为EIP165的介面识别码是bytes4,所以如果是要透过EIP820查询面,你必须要将EIP165的介面识别码后方补上28个0。下方是查询EIP820介面的函式getInterfaceImplementer:

function getInterfaceImplementer(address addr, bytes32 iHash) constant public returns (address) {     //检查是不是要查询EIP165的介面     if (isERC165Interface(iHash)) {         bytes4 i165Hash = bytes4(iHash);         return erc165InterfaceSupported(addr, i165Hash) ? addr : 0;     }     return interfaces[addr][iHash]; }

支援EIP165的函式:

function isERC165Interface(bytes32 iHash) internal pure returns (bool); function erc165InterfaceSupported(address _contract, bytes4 _interfaceId) constant public returns (bool); function erc165UpdateCache(address _contract, bytes4 _interfaceId) public; function erc165InterfaceSupported_NoCache(address _contract, bytes4 _interfaceId) public constant returns (bool);

其中erc165UpdateCache会将查询过或登记过的EIP165的介面存下来(存在mapping (address => mapping(bytes4 => bool)) erc165Cache;中),这样就不必每次查询都要再经过EIP165的步骤去确认是否支援特定介面。

指定Implementer须经过对方确认

任何人都可以登记任何合约地址为自己的Implementer,这会有一个潜在的漏洞:攻击者发行代币A(部署在合约X),并透过EIP820先登记代币A的Implementer为合约X,这时大家都是正常的以合约X上的逻辑去交换代币A;某天攻击者忽然将代币A的Implementer改为价格较高的代币B背后的合约Y,这时大家以为自己还是在交换代币A,但其实做的交易都是送到合约Y去,也就是大家变成在交换代币B。此时攻击者可以大量收购代币A(卖方以为自己是在卖代币A),借此以廉价的代币A价格买到较贵的代币B。

所以当你要登记Implementer时,EIP820会确认对方同意你登记它为你EIP820上的Implementer。

bytes32 constant ERC820_ACCEPT_MAGIC = keccak256(“ERC820_ACCEPT_MAGIC”); interface ERC820ImplementerInterface {     // Implementer必须要支援下列函式,且这个函式必须回传ERC820_ACCEPT_MAGIC这个值表示同意做为addr这个地址的implementer     function canImplementInterfaceForAddress(address addr, bytes32 interfaceHash) view public returns(bytes32); } Implementer合约必须要支援canImplementInterfaceForAddress这个函式。当你指定某个合约为Implementer时,EIP820合约会呼叫对方的canImplementInterfaceForAddress函式,对方回传ERC820_ACCEPT_MAGIC才会被视为同意。下方是登记EIP820介面的函式setInterfaceImplementer: function setInterfaceImplementer(address addr, bytes32 iHash, address implementer) public canManage(addr) {     //不能是EIP165的介面     require(!isERC165Interface(iHash));     //呼叫canImplementInterfaceForAddress并确认回传值是否为ERC820_ACCEPT_MAGIC     if ((implementer != 0) && (implementer!=msg.sender)) { require(ERC820ImplementerInterface(implementer).canImplementInterfaceForAddress(addr, iHash) == ERC820_ACCEPT_MAGIC);         }     interfaces[addr][iHash] = implementer;     InterfaceImplementerSet(addr, iHash, implementer); }

最后要注意的是,EIP165和EIP820的目的都是方便让别人知道你合约的介面,或让其他合约(例如想要能支援各种代币的交换合约或交易所合约)能够自动判断该使用哪个介面来和你的合约互动。但函式是否真的如预期执行还是要靠验证比对合约原始码和部署代码。

Reference: [1] 
https://github.com/ethereum/EIPs/issues/165 [2] 
https://github.com/ethereum/EIPs/pull/639 [3] 
https://github.com/ethereum /EIPs/pull/881 [4] 
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md [5] 
https://github.com/ethereum/EIPs/issues/820 [6] 
https://github.com/ethereum/EIPs/pull/906 [7] 
https://github.com/ethereum/EIPs/issues/672

文章发布只为分享区块链技术内容,版权归原作者所有,观点仅代表作者本人,绝不代表区块链兄弟赞同其观点或证实其描述。

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享