Skip to content

Commit

Permalink
Release 1.1.0 (#1)
Browse files Browse the repository at this point in the history
* Fix tests

* Improve README

* 1.1.0

* Improve examples
  • Loading branch information
MrLuit authored Feb 24, 2019
1 parent 31fb539 commit 046eb56
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 23 deletions.
47 changes: 34 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@
[![](https://img.shields.io/david/MrLuit/selfdestruct-detect.svg?style=flat-square)](https://david-dm.org/MrLuit/selfdestruct-detect)
[![](https://img.shields.io/github/license/MrLuit/selfdestruct-detect.svg?style=flat-square)](https://github.com/MrLuit/selfdestruct-detect/blob/master/LICENSE)

Detect the possibility of a self-destruction happening during the execution of an Ethereum smart contract by determining whether the runtime bytecode ran by the [Ethereum Virtual Machine](https://medium.com/@jeff.ethereum/optimising-the-ethereum-virtual-machine-58457e61ca15) contains a possibly reachable `SELFDESTRUCT` instruction.
Detect the possibility of a self-destruction happening during the execution of an Ethereum smart contract by determining whether the runtime bytecode ran by the [Ethereum Virtual Machine](https://medium.com/@jeff.ethereum/optimising-the-ethereum-virtual-machine-58457e61ca15) contains a (possibly) reachable `SELFDESTRUCT` instruction.

## Usage

> npm i selfdestruct-detect
## Features
- Unreachable code (like the [metadata hash](https://solidity.readthedocs.io/en/latest/metadata.html)) will not result in a false positive

## How does it work?
First of all, the application breaks down bytecode into its opcodes. Secondly, it loops over the opcodes, and skips over push data (if any). If it comes across a halting opcode (`STOP`, `RETURN`, `REVERT`, `INVALID`, `SELFDESTRUCT`), it will correctly assume all following opcodes are **unreachable**, until it can find a valid jump destination (`JUMPDEST`). While this method does prevent false positives caused by the metadata hash included by the Solidity compiler, it will not detect whether code is unreachable due to exceptional halting, excluding halting due to the `INVALID` (`0xfe`) opcode. Please note that his tool is also not able to determine whether a contract will *actually* self destruct at any given time, it only detects whether it *might* be possible.
First of all, the application breaks down bytecode into its opcodes. Secondly, it loops over the opcodes, and skips over push data (if any). If it comes across a halting opcode (`STOP`, `RETURN`, `REVERT`, `INVALID`, `SELFDESTRUCT`), it will correctly assume all following opcodes are **unreachable**, until it can find a valid jump destination (`JUMPDEST`). While this method does prevent some false positives (like the [metadata hash](https://solidity.readthedocs.io/en/latest/metadata.html), included by the Solidity compiler), it will not detect whether code is unreachable due to exceptional halting, excluding halting due to the `INVALID` (`0xfe`) opcode.

**Please note that this tool is unable to determine whether a contract will *actually* self destruct at any given time, it only detects whether it *might* be possible.**

## Why is this useful?
Ethereum's Constantinople fork introduces a new opcode called [CREATE2](https://eips.ethereum.org/EIPS/eip-1014), which allows contracts to deploy other contracts at address `keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]` (instead of the usual `keccak256(rlp([sender, nonce]))`). Unfortunetaly, including `keccak256(init_code)` in the address formula does not prevent contracts from redeploying different code at the same address. For example, `init_code` could essentially just be *"call contract x for runtime bytecode"* which would allow for `keccak256(initCode)` to stay the same, even if contract x decides to return different runtime bytecode.

While there is no way to deploy code at an address which already exists in the state, contracts can remove themselves from the state by self-destructing (using the `SELFDESTRUCT` opcode), which would allow different code to be redeployed. Since (currently) there's no way of finding out whether a contract was deployed using `CREATE2`, a possible detection method for this attack vector would be to check whether the runtime bytecode contains a `SELFDESTRUCT` opcode. Interfaces which allow users to interact with Ethereum smart contracts can (and should!) implement this check to warn users when interacting with unstable smart contracts.

Read more: [Potential security implications of CREATE2? (EIP-1014)](https://ethereum-magicians.org/t/potential-security-implications-of-create2-eip-1014/2614)

## Example

Expand All @@ -24,18 +30,33 @@ First of all, the application breaks down bytecode into its opcodes. Secondly, i
const { mightSelfdestruct } = require("selfdestruct-detect");
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("https://api.mycryptoapi.com/eth"));

web3.eth.getCode("0x06012c8cf97BEaD5deAe237070F9587f8E7A266d").then(code => { /* CryptoKitties contract */
console.log(mightSelfdestruct(code));
const CryptoKitties = "0x06012c8cf97BEaD5deAe237070F9587f8E7A266d";

web3.eth.getCode(CryptoKitties).then(code => {
if(mightSelfdestruct(code)) {
console.log("Warning: CryptoKitties contract possibly contains a self-destruct method!");
} else {
console.log("Success: CryptoKitties contract does not contain a reachable self-destruct instruction.");
}
});
```

#### Browser
```javascript
const { mightSelfdestruct } = window.SelfdestructDetector;
const web3 = new Web3(window.web3.currentProvider);
web3.eth.getCode("0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359", function(err,code) { /* DAI contract */
const { mightSelfdestruct } = window.SelfdestructDetect;
const web3 = new Web3(new Web3.providers.HttpProvider("https://api.mycryptoapi.com/eth"));
const DAI = "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359";

web3.eth.getCode(DAI, function(err, code) {
if(err) throw err;
console.log(mightSelfdestruct(code));
if(mightSelfdestruct(code)) {
console.log("Warning: DAI contract possibly contains a self-destruct method!");
} else {
console.log("Success: DAI contract does not contain a reachable self-destruct instruction.");
}
});
```
```

## References
- [EIP 1014: Skinny CREATE2](https://eips.ethereum.org/EIPS/eip-1014)
- [Potential security implications of CREATE2? (EIP-1014)](https://ethereum-magicians.org/t/potential-security-implications-of-create2-eip-1014/2614)
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "selfdestruct-detect",
"version": "1.0.0",
"version": "1.1.0",
"description": "Detect the possibility of a self-destruction happening during the execution of an Ethereum smart contract by determining whether the runtime bytecode ran by the Ethereum Virtual Machine contains a possibly reachable SELFDESTRUCT instruction",
"main": "lib/SelfdestructDetect.node.js",
"module": "lib/SelfdestructDetect.js",
Expand Down
2 changes: 1 addition & 1 deletion tests/contracts/metadata.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pragma solidity 0.5.4;

contract Contract {
string public data = "xyz";
bytes32 constant data = "[randomData]";
}
9 changes: 6 additions & 3 deletions tests/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'mocha';
import { expect } from 'chai';
import { mightSelfdestruct } from '../src';
import generateFFMetadataContract from './utils/generateFFMetadata';
import Contract from './utils/contract.class';

describe('Contracts', () => {
describe('hello_world.sol', () => {
const contract = new Contract("hello_world.sol");
const contract = new Contract();
contract.loadFile("hello_world.sol");

it('should compile without errors', () => {
expect(contract.valid(), contract.errors().join("\n")).to.be.true;
Expand All @@ -17,7 +19,8 @@ describe('Contracts', () => {
});

describe('selfdestruct.sol', () => {
const contract = new Contract("selfdestruct.sol");
const contract = new Contract();
contract.loadFile("selfdestruct.sol");

it('should compile without errors', () => {
expect(contract.valid(), contract.errors().join("\n")).to.be.true;
Expand All @@ -29,7 +32,7 @@ describe('Contracts', () => {
});

describe('metadata.sol', () => {
const contract = new Contract("metadata.sol");
const contract = generateFFMetadataContract();

it('should compile without errors', () => {
expect(contract.valid(), contract.errors().join("\n")).to.be.true;
Expand Down
27 changes: 23 additions & 4 deletions tests/utils/contract.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const solc = require("solc");
export default class Contract {
private output: any;

constructor(filename: string) {
loadFile(filename: string) {
const source = fs.readFileSync("./tests/contracts/" + filename, "utf8");
const input = {
language: 'Solidity',
Expand All @@ -24,6 +24,25 @@ export default class Contract {
this.output = JSON.parse(solc.compile(JSON.stringify(input)));
}

load(name: string, content: string) {
const input = {
language: 'Solidity',
sources: {
[name]: {
content: content
}
},
settings: {
outputSelection: {
'*': {
'*': [ '*' ]
}
}
}
}
this.output = JSON.parse(solc.compile(JSON.stringify(input)));
}

valid() {
return 'contracts' in this.output && (!('errors' in this.output) || this.output.errors.length === 0);
}
Expand All @@ -41,13 +60,13 @@ export default class Contract {
return bytecode;
}

hash(): string | false {
hash(): string {
const regex = /a165627a7a72305820([a-f0-9]{64})0029$/;
const match = this.bytecode().match(regex);
if (match && match[1]) {
return 'bzzr://' + match[1];
return match[1];
} else {
return false;
return '';
}
}
}
16 changes: 16 additions & 0 deletions tests/utils/generateFFMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Contract from './contract.class';
import * as crypto from 'crypto';
import * as fs from 'fs';

const metadata = fs.readFileSync("./tests/contracts/metadata.sol", "utf8");

export default () => {
while(true) {
const contract = new Contract();
const randomData = crypto.randomBytes(16).toString("hex");
contract.load("metadata.sol", metadata.replace("[randomData]", randomData));
if(contract.hash().includes('ff')) {
return contract;
}
}
}

0 comments on commit 046eb56

Please sign in to comment.