diff --git a/contracts/Exploitable.sol b/contracts/Exploitable.sol index 4528d3d..a88469f 100644 --- a/contracts/Exploitable.sol +++ b/contracts/Exploitable.sol @@ -1,13 +1,17 @@ -pragma solidity 0.5.0; +pragma solidity ^0.5.2; import "./Negotiator.sol"; +import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; contract Exploitable { + using SafeMath for uint256; + mapping(address => uint256) public balances; address payable[] public investors; Negotiator public negotiator; address payable public owner; + uint256 public bounty; constructor(Negotiator _negotiator) public { owner = msg.sender; @@ -20,7 +24,7 @@ contract Exploitable { function deposit() payable public { investors.push(msg.sender); - balances[msg.sender] += msg.value; + balances[msg.sender] = balances[msg.sender].add(msg.value); } function withdraw() public { @@ -35,54 +39,53 @@ contract Exploitable { msg.sender.transfer(balances[msg.sender]); } - function pay(uint256 bountyId, string memory publicKey) public { + function increaseBounty() payable public { require(msg.sender == owner); + bounty = bounty.add(msg.value); + } - // Credit investors X% less as bounty is sent to zeroday - for (uint256 i = 0; i < investors.length; i++) { - uint256 balance = balances[investors[i]]; - balances[investors[i]] *= balance * ((100 - percentageEIP1337()) / 100); - } - uint256 bounty = address(this).balance * (percentageEIP1337() / 100); + function decreaseBounty(uint256 amount) public { + require(msg.sender == owner); + bounty = bounty.sub(amount); + msg.sender.transfer(amount); + } + + function pay(uint256 bountyId, string memory publicKey) public { + require(msg.sender == owner); negotiator.pay.value(bounty)(bountyId, publicKey); + bounty = bounty.sub(bounty); } - function decide(uint256 bountyId, bool decision) public { + function decide( + uint256 bountyId, + bool decision, + string memory reason + ) public { require(msg.sender == owner); - negotiator.decide(bountyId, decision); + negotiator.decide(bountyId, decision, reason); } function restore() payable public { require(tx.origin == owner); require(msg.sender == address(negotiator)); - - // Increase investors balances after vulnerability has been declined - for (uint256 i = 0; i < investors.length; i++) { - uint256 balance = balances[investors[i]]; - balances[investors[i]] *= balance * ((percentageEIP1337() - 100) / 100); - } + bounty = bounty.add(msg.value); } function exit() public { require(tx.origin == owner); require(msg.sender == address(negotiator)); + // TODO: Send to Claimable contract instead of paying out directly. for (uint256 i = 0; i < investors.length; i++) { uint256 balance = balances[investors[i]]; - // Subtract bounty amount from balance of investors - uint256 balanceAfterBounty = balance * ((100 - percentageEIP1337()) / 100); - + // decrease investor credit to zero + balances[investors[i]] = 0; // send non-blocking so that function doesn't fail - investors[i].send(balanceAfterBounty); + investors[i].send(balance); } - selfdestruct(owner); } - function implementsEIP1337() public pure returns (bool) { + function implementsExploitable() public pure returns (bool) { return true; } - - function percentageEIP1337() public pure returns (uint256) { - return 10; - } } diff --git a/contracts/IExploitable.sol b/contracts/IExploitable.sol index 0cc5594..e6e160d 100644 --- a/contracts/IExploitable.sol +++ b/contracts/IExploitable.sol @@ -1,10 +1,9 @@ -pragma solidity 0.5.0; +pragma solidity ^0.5.2; contract IExploitable { - function implementsEIP1337() public pure returns (bool); - function percentageEIP1337() public pure returns (uint256); + function implementsExploitable() public pure returns (bool); function pay(uint256 bountyId, string memory publicKey) public; function restore() public payable; - function decide(uint256 bountyId, bool decision) public; + function decide(uint256 bountyId, bool decision, string memory reason) public; function exit() public; } diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol index d74ad25..3b81232 100644 --- a/contracts/Migrations.sol +++ b/contracts/Migrations.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.0; +pragma solidity ^0.5.2; contract Migrations { address public owner; diff --git a/contracts/Negotiator.sol b/contracts/Negotiator.sol index 8964600..e40d48b 100644 --- a/contracts/Negotiator.sol +++ b/contracts/Negotiator.sol @@ -1,4 +1,4 @@ -pragma solidity 0.5.0; +pragma solidity ^0.5.2; import "./IExploitable.sol"; @@ -7,27 +7,30 @@ contract Negotiator { struct Vuln { IExploitable exploitable; address payable attacker; - uint256 damage; string key; uint256 bounty; - string hash; + string plain; + string encrypted; Status status; + string reason; + uint256 paidAt; } - enum Status {Commited, Paid, Revealed, Exited, Declined} + enum Status {Commited, Paid, Revealed, Exited, Declined, Timeout} Vuln[] public vulns; + uint256 public timeout = 1 days; event Commit( uint256 indexed id, address indexed exploitable, - uint256 indexed damage, address attacker ); event Reveal( uint256 indexed id, - string indexed hash + string indexed plain, + string indexed encrypted ); event Pay( @@ -41,72 +44,88 @@ contract Negotiator { bool indexed exit ); - // TODO: Decide what to do with this constructor - constructor() public { - } - function commit( - IExploitable exploitable, - uint256 damage + IExploitable exploitable ) public returns (uint256 id) { - require(exploitable.implementsEIP1337()); - require(damage <= address(exploitable).balance); + require(exploitable.implementsExploitable()); id = vulns.push(Vuln({ exploitable: exploitable, attacker: msg.sender, - damage: damage, key: "", bounty: 0, - hash: "", - status: Status.Commited + plain: "", + encrypted: "", + status: Status.Commited, + reason: "", + paidAt: 0 })) - 1; - emit Commit(id, address(exploitable), damage, msg.sender); - } - - function reveal(uint256 id, string memory hash) public { - Vuln storage vuln = vulns[id]; - require(vuln.status == Status.Paid); - require(msg.sender == vuln.attacker); - - vuln.hash = hash; - vuln.status = Status.Revealed; - emit Reveal(id, hash); + emit Commit(id, address(exploitable), msg.sender); } function pay(uint256 id, string memory key) public payable { Vuln storage vuln = vulns[id]; - uint256 bounty = vuln.damage * (vuln.exploitable.percentageEIP1337() / 100); - require(msg.value >= bounty); require(msg.sender == address(vuln.exploitable)); require(vuln.status == Status.Commited); vuln.key = key; + vuln.paidAt = block.timestamp; vuln.bounty = msg.value; vuln.status = Status.Paid; emit Pay(id, key, msg.value); } - // TODO: Add string reason - // TODO: Decide should also work after a time out, in case the attacker - // never reveals a secret - function decide(uint256 id, bool exit) public { + function reveal( + uint256 id, + string memory plain, + string memory encrypted + ) public { + Vuln storage vuln = vulns[id]; + require(vuln.status == Status.Paid); + require(msg.sender == vuln.attacker); + + vuln.plain = plain; + vuln.encrypted = encrypted; + vuln.status = Status.Revealed; + emit Reveal(id, plain, encrypted); + } + + function decide(uint256 id, bool exit, string memory reason) public { Vuln storage vuln = vulns[id]; require(msg.sender == address(vuln.exploitable)); - require(vuln.status == Status.Revealed); - if (exit) { - vuln.status = Status.Exited; - vuln.exploitable.exit(); - vuln.attacker.send(vuln.bounty); - emit Decide(id, true); - } else { - vuln.status = Status.Declined; + if (timedout(id)) { + vuln.status = Status.Timeout; + vuln.reason = "Attacker didn't reveal in time."; vuln.exploitable.restore.value(vuln.bounty)(); + vuln.bounty = 0; emit Decide(id, false); + } else if (vuln.status == Status.Revealed) { + if (exit) { + vuln.status = Status.Exited; + vuln.reason = reason; + vuln.exploitable.exit(); + vuln.attacker.send(vuln.bounty); + vuln.bounty = 0; + emit Decide(id, true); + } else { + vuln.status = Status.Declined; + vuln.reason = reason; + vuln.exploitable.restore.value(vuln.bounty)(); + vuln.bounty = 0; + emit Decide(id, false); + } + } else { + revert(); } } + function timedout(uint256 _id) public view returns (bool) { + Vuln storage vuln = vulns[_id]; + return vuln.status == Status.Paid && + vuln.paidAt + timeout > block.timestamp; + } + function length() public view returns (uint256) { return vulns.length; } @@ -130,9 +149,4 @@ contract Negotiator { } } } - - function reward(uint256 id) public view returns (uint256) { - Vuln storage vuln = vulns[id]; - return vuln.damage * (vuln.exploitable.percentageEIP1337() / 100); - } } diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index ac1f559..f26b590 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -15,6 +15,9 @@ module.exports = function(deployer, network, accounts) { Exploitable.abi, instance.address ); + await contract.methods + .increaseBounty() + .send({ from: accounts[0], value: web3.utils.toWei("0.5", "ether") }); return contract.methods .deposit() .send({ from: accounts[0], value: web3.utils.toWei("1", "ether") }); diff --git a/package-lock.json b/package-lock.json index e3a40dd..1d316ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4328,11 +4328,6 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, - "i": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/i/-/i-0.3.6.tgz", - "integrity": "sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0=" - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4572,20 +4567,35 @@ } }, "ipld-dag-pb": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/ipld-dag-pb/-/ipld-dag-pb-0.15.2.tgz", - "integrity": "sha512-9mzeYW4FneGROH+/PXMbXsfy3cUsMYHaI6vUu8nNpSTyQdGF+fa1ViA+jvqWzM8zXYwG4OOSCAAADssJeELAvw==", + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/ipld-dag-pb/-/ipld-dag-pb-0.15.3.tgz", + "integrity": "sha512-J1RJzSVCaOpxPmSzXbwVNsAZPHctjY4OjqG1dMIG86Z37CKvuy1QwCFkDhNccUTcQpF3sXfj5e0ZUyMM035vzg==", "requires": { "async": "^2.6.1", "bs58": "^4.0.1", "cids": "~0.5.4", "class-is": "^1.1.0", - "is-ipfs": "~0.4.2", + "is-ipfs": "~0.6.0", "multihashing-async": "~0.5.1", "protons": "^1.0.1", "pull-stream": "^3.6.9", "pull-traverse": "^1.0.3", "stable": "~0.1.8" + }, + "dependencies": { + "is-ipfs": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/is-ipfs/-/is-ipfs-0.6.0.tgz", + "integrity": "sha512-q/CO69rN+vbw9eGXGQOAa15zXq+pSyhdKvE7mqvuplDu67LyT3H9t3RyYQvKpueN7dL4f6fbyjEMPp9J3rJ4qA==", + "requires": { + "bs58": "^4.0.1", + "cids": "~0.5.6", + "mafmt": "^v6.0.7", + "multiaddr": "^6.0.4", + "multibase": "~0.6.0", + "multihashes": "~0.4.13" + } + } } }, "is-accessor-descriptor": { @@ -5535,6 +5545,11 @@ "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.8.0.tgz", "integrity": "sha512-Gwj4KnJOW15YeTJKO5frFd/WDO5Mc0zxXqL9oHx3+e9rBqW8EVARqQHSaIXznUdljrD6pvbNGW2ZGXKPEfYJfw==" }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, "mout": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz", @@ -9032,6 +9047,11 @@ "wrappy": "1" } }, + "openzeppelin-solidity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/openzeppelin-solidity/-/openzeppelin-solidity-2.2.0.tgz", + "integrity": "sha512-HfQq0xyT+EPs/lTWEd5Odu4T7CYdYe+qwf54EH28FQZthp4Bs6IWvOlOumTdS2dvpwZoTXURAopHn2LN1pwAGQ==" + }, "opn": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz", diff --git a/package.json b/package.json index 8c30bf1..a81f5f9 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "bignumber.js": "^8.0.2", "eth-ecies": "^1.0.3", "foundation-sites": "^6.5.3", - "i": "^0.3.6", "ipfs-http-client": "^29.1.1", "mobx": "^5.9.0", "mobx-react": "^5.4.3", "mobx-router": "git+https://github.com/TimDaub/mobx-router.git#0xdefaceme", + "moment": "^2.24.0", + "multihashes": "^0.4.14", "npm": "^6.8.0", + "openzeppelin-solidity": "^2.2.0", "react": "^16.8.2", "react-dom": "^16.8.2", "react-foundation": "^0.9.6", diff --git a/src/config.json b/src/config.json index 3034644..c01998f 100644 --- a/src/config.json +++ b/src/config.json @@ -13,6 +13,7 @@ "REVEALED": "#FF5722", "EXITED": "#4CAF50", "DECLINED": "#F44336", + "TIMEOUT": "#E91E63", "NETWORKS": { "MAINNET": "#00E676", "RINKEBY": "#FFEA00" diff --git a/src/stores/Vulnerabilities.js b/src/stores/Vulnerabilities.js index 9a28b02..9902a3b 100644 --- a/src/stores/Vulnerabilities.js +++ b/src/stores/Vulnerabilities.js @@ -89,15 +89,13 @@ class Vulnerabilities { } }); - commit = flow(function*(web3, account, exploitable, damage) { + commit = flow(function*(web3, account, exploitable) { this.state = "pending"; const contract = yield this.contract(web3); try { - yield contract.methods - .commit(exploitable, damage) - .send({ from: account }); + yield contract.methods.commit(exploitable).send({ from: account }); this.state = "done"; } catch (err) { diff --git a/src/stores/Vulnerability.js b/src/stores/Vulnerability.js index 9753747..f2bc81e 100644 --- a/src/stores/Vulnerability.js +++ b/src/stores/Vulnerability.js @@ -4,6 +4,7 @@ import BigNumber from "bignumber.js"; import ecies from "eth-ecies"; import wallet from "ethereumjs-wallet"; import axios from "axios"; +import multihash from "multihashes"; import config from "../config"; @@ -22,19 +23,27 @@ class Vulnerability { @observable attacker = 0x0; @observable - damage = "0"; - @observable key = 0x0; @observable bounty = "0"; @observable - hash = ""; + plain = ""; + @observable + encrypted = ""; @observable status = -1; @observable balance = "0"; @observable code = ""; + @observable + reason = ""; + @observable + paidAt = 0; + @observable + timedout = false; + @observable + timeout = 0; // Computed values @observable @@ -103,55 +112,58 @@ class Vulnerability { const { exploitable, attacker, - damage, key, bounty, - hash, - status + plain, + encrypted, + status, + reason, + paidAt } = yield contract.methods.vulns(id).call({ from: account }); this.id = id; this.exploitable = exploitable; this.attacker = attacker; - this.damage = damage; this.key = key; this.bounty = bounty; - this.hash = hash; - this.status = status; + this.plain = plain; + this.encrypted = encrypted; + this.status = parseInt(status, 10); + this.reason = reason; + this.paidAt = parseInt(paidAt, 10); } catch (err) { console.log(err); this.state = "error"; } try { - this.balance = yield web3.eth.getBalance(this.exploitable); - this.state = "done"; + this.timedout = yield contract.methods.timedout(id).call({ account }); } catch (err) { console.log(err); this.state = "error"; } - }); - - compute = flow(function*(web3, account, id) { - this.state = "pending"; try { - // TODO: Put these statements into separate try catch clauses - yield this.fetch(web3, account, id); - - const negotiator = yield this.negotiator(web3); + this.timeout = parseInt( + yield contract.methods.timeout().call({ account }), + 10 + ); + } catch (err) { + console.log(err); + this.state = "error"; + } - this.tmpbounty = yield negotiator.methods - .reward(id) - .call({ from: account }); + try { + this.balance = yield web3.eth.getBalance(this.exploitable); this.state = "done"; } catch (err) { console.log(err); this.state = "error"; } + }); - // TODO: Separate these two concepts: - // 1. get bounty - // 2. compute private key + compute = flow(function*(web3, account, id) { + this.state = "pending"; + yield this.fetch(web3, account, id); let { privateKey } = web3.eth.accounts.create(); privateKey = privateKey.substring(2, privateKey.length); @@ -182,19 +194,24 @@ class Vulnerability { // TODO: Put these statements into separate try catch clauses yield this.fetch(web3, account, id); + // Generate IPFS hash without uploading it + const plain = (yield ipfs.add(new Buffer(vuln), { onlyHash: true }))[0] + .hash; + const key = this.key.substring(2, this.key.length); const userPublicKey = new Buffer(key, "hex"); const bufferData = new Buffer(vuln); const encryptedData = ecies.encrypt(userPublicKey, bufferData); const contract = yield this.negotiator(web3); - const hash = (yield ipfs.add( + const encrypted = (yield ipfs.add( new Buffer(encryptedData.toString("base64")) ))[0]["hash"]; - console.log(hash); try { - yield contract.methods.reveal(id, hash).send({ from: account }); + yield contract.methods + .reveal(id, plain, encrypted) + .send({ from: account }); } catch (err) { console.log(err); this.state = "error"; @@ -214,7 +231,7 @@ class Vulnerability { let vuln; try { vuln = (yield axios.get( - `https://${config.IPFS_PROVIDER}/ipfs/${this.hash}` + `https://${config.IPFS_PROVIDER}/ipfs/${this.encrypted}` )).data; } catch (err) { console.log(err); @@ -236,15 +253,21 @@ class Vulnerability { this.state = "done"; }); - decide = flow(function*(web3, account, id, exit) { + decide = flow(function*(web3, account, id, exit, reason) { this.state = "pending"; - // NOTE: We assume that Vulnerability is fully loaded. - // We don't call fetch here. + try { + yield this.fetch(web3, account, id); + } catch (err) { + console.log(err); + this.state = "error"; + } const exploitable = yield this.exploitableContract(web3, this.exploitable); try { - yield exploitable.methods.decide(id, exit).send({ from: account }); + yield exploitable.methods + .decide(id, exit, reason) + .send({ from: account }); this.state = "done"; } catch (err) { console.log(err); diff --git a/src/utils/helpers.js b/src/utils/helpers.js index a9b574a..3d8a453 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -52,8 +52,7 @@ export function shortenBalance(value) { } } -export function statusToLabel(status) { - status = parseInt(status, 10); +export function statusToLabel(status, reason) { switch (status) { case 0: return ; @@ -62,8 +61,22 @@ export function statusToLabel(status) { case 2: return ; case 3: - return ; + return ( + + ); case 4: - return ; + return ( + + ); + case 5: + return ( + + ); } } diff --git a/src/views/Commit.js b/src/views/Commit.js index 6d1a438..bb211c4 100644 --- a/src/views/Commit.js +++ b/src/views/Commit.js @@ -8,7 +8,7 @@ import styled from "styled-components"; import views from "../views"; import { Button, Header, Disclaimer, Form, Footer, Input } from "../components"; -@inject("account", "web3", "vulnerabilities") +@inject("router", "account", "web3", "vulnerabilities") @observer class Commit extends Component { constructor(props) { @@ -30,11 +30,9 @@ class Commit extends Component { async onCommit() { const exploitable = this.refs.exploitable.value; - let damage = this.refs.damage.value; const { router, vulnerabilities, web3, account } = this.props; - damage = web3.utils.toWei(damage, "ether"); - await vulnerabilities.commit(web3, account, exploitable, damage); + await vulnerabilities.commit(web3, account, exploitable); router.goTo(views.list, null, { router }); } @@ -60,14 +58,6 @@ class Commit extends Component { committing vulnerabilities:
And that's it. Happy committing!
@@ -98,10 +88,6 @@ class Commit extends Component {+ {vulnerability.status === 3 + ? "The contract was exited. A bounty was paid out to the attacker and funds were sent back to the contract users. The operator gave the following reasons for shutting down the contract:" + : ""}{" "} + {vulnerability.status === 4 + ? "The contract is still running with all its funds on the network. No bounty was paid out. The operator gave the following reason for declining the report:" + : ""} + {vulnerability.status === 5 + ? "The attacker didn't submit the report in a timely maner. This lead the operator to reclaim their bounty. Their contract is hence still running. Their reasoning was:" + : ""} + . +
+{vulnerability.reason}
+