Skip to content

Instantly share code, notes, and snippets.

@wighawag
Created May 30, 2019 21:09
Show Gist options
  • Save wighawag/9404f81a2a6d0cfa2134549f186d4d46 to your computer and use it in GitHub Desktop.
Save wighawag/9404f81a2a6d0cfa2134549f186d4d46 to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.4.20+commit.3155dd80.js&optimize=false&gist=
pragma solidity ^0.4.20;
contract ERC165Query {
bytes4 constant InvalidID = 0xffffffff;
bytes4 constant ERC165ID = 0x01ffc9a7;
function doesContractImplementInterface(address _contract, bytes4 _interfaceId) external view returns (bool) {
uint256 success;
uint256 result;
(success, result) = noThrowCall(_contract, ERC165ID);
if ((success==0)||(result==0)) {
return false;
}
(success, result) = noThrowCall(_contract, InvalidID);
if ((success==0)||(result!=0)) {
return false;
}
(success, result) = noThrowCall(_contract, _interfaceId);
if ((success==1)&&(result==1)) {
return true;
}
return false;
}
function noThrowCall(address _contract, bytes4 _interfaceId) constant internal returns (uint256 success, uint256 result) {
bytes4 erc165ID = ERC165ID;
assembly {
let x := mload(0x40) // Find empty storage location using "free memory pointer"
mstore(x, erc165ID) // Place signature at beginning of empty storage
mstore(add(x, 0x04), _interfaceId) // Place first argument directly next to signature
success := staticcall(
30000, // 30k gas
_contract, // To addr
x, // Inputs are stored at location x
0x24, // Inputs are 36 bytes long
x, // Store output over input (saves space)
0x20) // Outputs are 32 bytes long
result := mload(x) // Load the result
}
}
}
pragma solidity ^0.4.20;
import "./ERC165Query.sol";
interface ERC165 {
function supportsInterface(bytes4 _id) external returns (bool);
}
// An example contract that support an interface (0xeeeeeeee) and is valid as per EIP-165 rules
// When called on this interface, it throw all the time (not implemented), we could imagine that to indicate it refuse to receive some sort of tokens
// it has the property that its `supportsInterface` implementation require an important amount of gas (specified approximatively by its constructor) to show case the issue
// Note : we could make variant of this contract that throws on some condition, etc... that would not remove the issue, just increase the threshold
contract ValidExampleAsPerEIP165 is ERC165{
uint256 gas;
function ValidExampleAsPerEIP165(uint256 _gas) public {
gas = _gas;
}
function supportsInterface(bytes4 _id) external returns (bool) {
uint256 start = msg.gas;
while(start - msg.gas < gas) {}
return _id == 0xeeeeeeee || _id == 0x01ffc9a7;
}
}
// the example show the issue of using the ERC165Query implementation posted on EIP-165
contract FailureCase {
ERC165Query query;
function FailureCase(ERC165Query _query) public {
query = _query;
}
// it is a contract that use an interface as a mechanism for refusal.
function test(address to) external returns (bool) {
// ... here we could have logic for token transfer, whatever, it will not affect the issue since these operations happen before
if (query.doesContractImplementInterface(to, 0xeeeeeeee)) { // it checks if the recipient accept call to an interface (0xeeeeeeee)
bool success = to.call(0xeeeeeeee); // if it does call it
require(success); // if such call throws it means the receiver refuse to receive the tokens, the call thus abort
} // if the recipient does not implement the interface 0xeeeeeeee, the contract consider it as acceptance, it thus continue the call
}
}
// now as per EIP-165, The contract should throw in all condition since ValidExampleAsPerEIP165
// a ) is supporting the interface 0xeeeeeeee
// b) is throwing when called on 0xeeeeeeee
// and as per the logic of the method 'test' the require should be executed all the time
// But with the ERC165Query used here (the one specified in EIP-165) the call do not throw when :
// - ValidExampleAsPerEIP165 is deployed with a valid of at least 21000 as constructor parameter
// - FailureCase.test is executed on that contract with gasLimit = 87500
// This is the bug since it should throw.
// increasing the gasLimit make it fails as expected, try with gasLimit = 875000
// STEPS TO REPRODUCE :
// - deploy ERC165Query (save its address: A1)
// - deploy ValidExampleAsPerEIP165 with param = 21000 (save its address A2)
// - deploy FailureCase with param = <A1>
// execute FailureCase.test with param = <A2> and gasLimit = 87500 => it succeed (THIS IS THE BUG)
// execute FailureCase.test with param = <A2> and gasLimit = 875000 => it throws
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment