diff --git a/contracts/HumanStandardToken.sol b/contracts/HumanStandardToken.sol index 63620e7..8ab7a2c 100644 --- a/contracts/HumanStandardToken.sol +++ b/contracts/HumanStandardToken.sol @@ -10,7 +10,7 @@ Machine-based, rapid creation of many tokens would not necessarily need these ex 3) Optional approveAndCall() functionality to notify a contract if an approval() has occurred. .*/ -pragma solidity ^0.4.4; +pragma solidity ^0.4.6; import "./StandardToken.sol"; diff --git a/contracts/HumanStandardTokenFactory.sol b/contracts/HumanStandardTokenFactory.sol new file mode 100644 index 0000000..e843060 --- /dev/null +++ b/contracts/HumanStandardTokenFactory.sol @@ -0,0 +1,62 @@ +import "./HumanStandardToken.sol"; + +pragma solidity ^0.4.6; + +contract HumanStandardTokenFactory { + + mapping(address => address[]) public created; + mapping(address => bool) public isHumanToken; //verify without having to do a bytecode check. + bytes public humanStandardByteCode; + + function HumanStandardTokenFactory() { + //upon creation of the factory, deploy a HumanStandardToken (parameters are meaningless) and store the bytecode provably. + address verifiedToken = createHumanStandardToken(10000, "Verify Token", 3, "VTX"); + humanStandardByteCode = codeAt(verifiedToken); + } + + //verifies if a contract that has been deployed is a Human Standard Token. + //NOTE: This is a very expensive function, and should only be used in an eth_call. ~800k gas + function verifyHumanStandardToken(address _tokenContract) returns (bool) { + bytes memory fetchedTokenByteCode = codeAt(_tokenContract); + + if (fetchedTokenByteCode.length != humanStandardByteCode.length) { + return false; //clear mismatch + } + + //starting iterating through it if lengths match + for (uint i = 0; i < fetchedTokenByteCode.length; i ++) { + if (fetchedTokenByteCode[i] != humanStandardByteCode[i]) { + return false; + } + } + + return true; + } + + //for now, keeping this internal. Ideally there should also be a live version of this that any contract can use, lib-style. + //retrieves the bytecode at a specific address. + function codeAt(address _addr) internal returns (bytes o_code) { + assembly { + // retrieve the size of the code, this needs assembly + let size := extcodesize(_addr) + // allocate output byte array - this could also be done without assembly + // by using o_code = new bytes(size) + o_code := mload(0x40) + // new "memory end" including padding + mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) + // store length in memory + mstore(o_code, size) + // actually retrieve the code, this needs assembly + extcodecopy(_addr, add(o_code, 0x20), 0, size) + } + } + + function createHumanStandardToken(uint256 _initialAmount, string _name, uint8 _decimals, string _symbol) returns (address) { + + HumanStandardToken newToken = (new HumanStandardToken(_initialAmount, _name, _decimals, _symbol)); + created[msg.sender].push(address(newToken)); + isHumanToken[address(newToken)] = true; + newToken.transfer(msg.sender, _initialAmount); //the factory will own the created tokens. You must transfer them. + return address(newToken); + } +} diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol new file mode 100644 index 0000000..1af4964 --- /dev/null +++ b/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.4.6; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + modifier restricted() { + if (msg.sender == owner) _; + } + + function Migrations() { + owner = msg.sender; + } + + function setCompleted(uint completed) restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/contracts/StandardToken.sol b/contracts/StandardToken.sol index f82fdc3..c5dc557 100644 --- a/contracts/StandardToken.sol +++ b/contracts/StandardToken.sol @@ -7,7 +7,7 @@ If you deploy this, you won't have anything useful. Implements ERC 20 Token standard: https://github.com/ethereum/EIPs/issues/20 .*/ -pragma solidity ^0.4.4; +pragma solidity ^0.4.6; import "./Token.sol"; diff --git a/contracts/Token.sol b/contracts/Token.sol index 2f40232..5530241 100644 --- a/contracts/Token.sol +++ b/contracts/Token.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.4; +pragma solidity ^0.4.6; contract Token { diff --git a/contracts/TokenTester.sol b/contracts/TokenTester.sol new file mode 100644 index 0000000..5d7e7d4 --- /dev/null +++ b/contracts/TokenTester.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.4.6; + +import "./HumanStandardTokenFactory.sol"; + +//commented out for now as Factory > 3m gas to deploy, causing this to OOG. +contract TokenTester { + address public tokenContractAddress; + + /*function TokenTester() { + address factoryAddr = address(new HumanStandardTokenFactory()); + HumanStandardTokenFactory tokenFactory = HumanStandardTokenFactory(factoryAddr); + + tokenContractAddress = tokenFactory.createHumanStandardToken(10000, 'Simon Bucks', 1, 'SBX'); + }*/ +} diff --git a/migrations/1_initial_migration.js b/migrations/1_initial_migration.js new file mode 100644 index 0000000..4d5f3f9 --- /dev/null +++ b/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +var Migrations = artifacts.require("./Migrations.sol"); + +module.exports = function(deployer) { + deployer.deploy(Migrations); +}; diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js new file mode 100644 index 0000000..cd6a060 --- /dev/null +++ b/migrations/2_deploy_contracts.js @@ -0,0 +1,7 @@ +var HumanStandardTokenFactory = artifacts.require("./HumanStandardTokenFactory.sol"); +var HumanStandardToken = artifacts.require("./HumanStandardToken.sol"); + +module.exports = function(deployer) { + deployer.deploy(HumanStandardTokenFactory); + deployer.deploy(HumanStandardToken); +}; diff --git a/package.json b/package.json index fbc19a0..42aacd8 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "reflux-tx": "0.2.4", "sass-loader": "^3.1.2", "style-loader": "^0.13.0", + "truffle-hdwallet-provider": "0.0.3", "uport-lib": "^1.3.1", "web3": "0.16.0", "webpack": "^1.12.12" diff --git a/test/humanStandardToken.js b/test/humanStandardToken.js new file mode 100644 index 0000000..12fd084 --- /dev/null +++ b/test/humanStandardToken.js @@ -0,0 +1,291 @@ +contract("HumanStandardToken", function(accounts) { + +//CREATION + + it("creation: should create an initial balance of 10000 for the creator", function(done) { + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(ctr) { + return ctr.balanceOf.call(accounts[0]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 10000); + done(); + }).catch(done); + }); + + it("creation: test correct setting of vanity information", function(done) { + var ctr; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.name.call(); + }).then(function (result) { + assert.strictEqual(result, 'Simon Bucks'); + return ctr.decimals.call(); + }).then(function(result) { + assert.strictEqual(result.toNumber(), 1); + return ctr.symbol.call(); + }).then(function(result) { + assert.strictEqual(result, 'SBX'); + done(); + }).catch(done); + }); + + it("creation: should succeed in creating over 2^256 - 1 (max) tokens", function(done) { + //2^256 - 1 + HumanStandardToken.new('115792089237316195423570985008687907853269984665640564039457584007913129639935', 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(ctr) { + return ctr.totalSupply(); + }).then(function (result) { + var match = result.equals('1.15792089237316195423570985008687907853269984665640564039457584007913129639935e+77'); + assert.isTrue(match); + done(); + }).catch(done); + }); + +//TRANSERS +//normal transfers without approvals. + + //this is not *good* enough as the contract could still throw an error otherwise. + //ideally one should check balances before and after, but estimateGas currently always throws an error. + //it's not giving estimate on gas used in the event of an error. + it("transfers: ether transfer should be reversed.", function(done) { + var ctr; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return web3.eth.sendTransaction({from: accounts[0], to: ctr.address, value: web3.toWei("10", "Ether")}); + }).catch(function(result) { + done(); + }).catch(done); + }); + + + it("transfers: should transfer 10000 to accounts[1] with accounts[0] having 10000", function(done) { + var ctr; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.transfer(accounts[1], 10000, {from: accounts[0]}); + }).then(function (result) { + return ctr.balanceOf.call(accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 10000); + done(); + }).catch(done); + }); + + it("transfers: should fail when trying to transfer 10001 to accounts[1] with accounts[0] having 10000", function(done) { + var ctr; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.transfer.call(accounts[1], 10001, {from: accounts[0]}); + }).then(function (result) { + assert.isFalse(result); + done(); + }).catch(done); + }); + + it("transfers: should fail when trying to transfer zero.", function(done) { + var ctr; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.transfer.call(accounts[1], 0, {from: accounts[0]}); + }).then(function (result) { + assert.isFalse(result); + done(); + }).catch(done); + }); + + //NOTE: testing uint256 wrapping is impossible in this standard token since you can't supply > 2^256 -1. + + //todo: transfer max amounts. + +//APPROVALS + + it("approvals: msg.sender should approve 100 to accounts[1]", function(done) { + var ctr = null; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.approve(accounts[1], 100, {from: accounts[0]}); + }).then(function (result) { + return ctr.allowance.call(accounts[0], accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 100); + done(); + }).catch(done); + }); + + it("approvals: msg.sender should approve 100 to SampleRecipient and then NOTIFY SampleRecipient. It should succeed.", function(done) { + var ctr = null; + var sampleCtr = null + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return SampleRecipientSuccess.new({from: accounts[0]}); + }).then(function(result) { + sampleCtr = result; + return ctr.approveAndCall(sampleCtr.address, 100, '0x42', {from: accounts[0]}); + }).then(function (result) { + return ctr.allowance.call(accounts[0], sampleCtr.address); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 100); + return sampleCtr.value.call(); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 100); + done(); + }).catch(done); + }); + + it("approvals: msg.sender should approve 100 to SampleRecipient and then NOTIFY SampleRecipient and throw.", function(done) { + var ctr = null; + var sampleCtr = null + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return SampleRecipientThrow.new({from: accounts[0]}); + }).then(function(result) { + sampleCtr = result; + return ctr.approveAndCall.call(sampleCtr.address, 100, '0x42', {from: accounts[0]}); + }).catch(function (result) { + //It will catch OOG. + done(); + }).catch(done) + }); + + //bit overkill. But is for testing a bug + it("approvals: msg.sender approves accounts[1] of 100 & withdraws 20 once.", function(done) { + var ctr = null; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.balanceOf.call(accounts[0]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 10000); + return ctr.approve(accounts[1], 100, {from: accounts[0]}); + }).then(function (result) { + return ctr.balanceOf.call(accounts[2]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 0); + return ctr.allowance.call(accounts[0], accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 100); + return ctr.transferFrom.call(accounts[0], accounts[2], 20, {from: accounts[1]}); + }).then(function (result) { + return ctr.transferFrom(accounts[0], accounts[2], 20, {from: accounts[1]}); + }).then(function (result) { + return ctr.allowance.call(accounts[0], accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 80); + return ctr.balanceOf.call(accounts[2]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 20); + return ctr.balanceOf.call(accounts[0]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 9980); + done(); + }).catch(done); + }); + + //should approve 100 of msg.sender & withdraw 50, twice. (should succeed) + it("approvals: msg.sender approves accounts[1] of 100 & withdraws 20 twice.", function(done) { + var ctr = null; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.approve(accounts[1], 100, {from: accounts[0]}); + }).then(function (result) { + return ctr.allowance.call(accounts[0], accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 100); + return ctr.transferFrom(accounts[0], accounts[2], 20, {from: accounts[1]}); + }).then(function (result) { + return ctr.allowance.call(accounts[0], accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 80); + return ctr.balanceOf.call(accounts[2]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 20); + return ctr.balanceOf.call(accounts[0]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 9980); + //FIRST tx done. + //onto next. + return ctr.transferFrom(accounts[0], accounts[2], 20, {from: accounts[1]}); + }).then(function (result) { + return ctr.allowance.call(accounts[0], accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 60); + return ctr.balanceOf.call(accounts[2]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 40); + return ctr.balanceOf.call(accounts[0]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 9960); + done(); + }).catch(done); + }); + + //should approve 100 of msg.sender & withdraw 50 & 60 (should fail). + it("approvals: msg.sender approves accounts[1] of 100 & withdraws 50 & 60 (2nd tx should fail)", function(done) { + var ctr = null; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.approve(accounts[1], 100, {from: accounts[0]}); + }).then(function (result) { + return ctr.allowance.call(accounts[0], accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 100); + return ctr.transferFrom(accounts[0], accounts[2], 50, {from: accounts[1]}); + }).then(function (result) { + return ctr.allowance.call(accounts[0], accounts[1]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 50); + return ctr.balanceOf.call(accounts[2]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 50); + return ctr.balanceOf.call(accounts[0]); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 9950); + //FIRST tx done. + //onto next. + return ctr.transferFrom.call(accounts[0], accounts[2], 60, {from: accounts[1]}); + }).then(function (result) { + assert.isFalse(result); + done(); + }).catch(done); + }); + + it("approvals: attempt withdrawal from acconut with no allowance (should fail)", function(done) { + var ctr = null; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.transferFrom.call(accounts[0], accounts[2], 60, {from: accounts[1]}); + }).then(function (result) { + assert.isFalse(result); + done(); + }).catch(done); + }); + + it("approvals: allow accounts[1] 100 to withdraw from accounts[0]. Withdraw 60 and then approve 0 & attempt transfer.", function(done) { + var ctr = null; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.approve(accounts[1], 100, {from: accounts[0]}); + }).then(function (result) { + return ctr.transferFrom(accounts[0], accounts[2], 60, {from: accounts[1]}); + }).then(function (result) { + return ctr.approve(accounts[1], 0, {from: accounts[0]}); + }).then(function (result) { + return ctr.transferFrom.call(accounts[0], accounts[2], 10, {from: accounts[1]}); + }).then(function (result) { + assert.isFalse(result); + done(); + }).catch(done); + }); + + it("approvals: approve max (2^256 - 1)", function(done) { + var ctr = null; + HumanStandardToken.new(10000, 'Simon Bucks', 1, 'SBX', {from: accounts[0]}).then(function(result) { + ctr = result; + return ctr.approve(accounts[1],'115792089237316195423570985008687907853269984665640564039457584007913129639935' , {from: accounts[0]}); + }).then(function (result) { + return ctr.allowance(accounts[0], accounts[1]); + }).then(function (result) { + var match = result.equals('1.15792089237316195423570985008687907853269984665640564039457584007913129639935e+77'); + assert.isTrue(match); + done(); + }).catch(done); + }); + +}); diff --git a/test/humanStandardTokenFactory.js b/test/humanStandardTokenFactory.js new file mode 100644 index 0000000..6ab7b16 --- /dev/null +++ b/test/humanStandardTokenFactory.js @@ -0,0 +1,26 @@ +//This currently throws a stack underflow, and thus commented out. Contract is correctly deployed, but createHumanStandardToken throws underflow. +//Replicated under testrpc and debugging to fix this. + +contract("HumanStandardTokenFactory", function(accounts) { + + it("Verify a Human Standard Token once deployed using both verification functions.", function(done) { + var factory = null; + var newTokenAddr = null; + HumanStandardTokenFactory.new().then(function(ctr) { + factory = ctr; + + return factory.createHumanStandardToken.call(100000, "Simon Bucks", 2, "SBX", {from: accounts[0]}); + }).then(function(tokenContractAddr) { + newTokenAddr = tokenContractAddr; + return factory.createHumanStandardToken(100000, "Simon Bucks", 2, "SBX", {from: accounts[0]}); + }).then(function(result) { + return factory.verifyHumanStandardToken.call(newTokenAddr, {from: accounts[0]}); + }).then(function (result) { + assert.strictEqual(result, true); + return factory.isHumanToken.call(newTokenAddr, {from: accounts[0]}); + }).then(function (result) { + assert.strictEqual(result, true); + done(); + }).catch(done); + }); +}); diff --git a/test/standardToken.js b/test/standardToken.js new file mode 100644 index 0000000..977dde7 --- /dev/null +++ b/test/standardToken.js @@ -0,0 +1 @@ +/* This has moved to humandStandardToken.js */ diff --git a/test/tokenTester.js b/test/tokenTester.js new file mode 100644 index 0000000..7591070 --- /dev/null +++ b/test/tokenTester.js @@ -0,0 +1,18 @@ +//currently commented out as TokenTester is causing a OOG error due to the Factory being too big +//Not fully needed as factory & separate tests cover token creation. + +/*contract("TokenTester", function(accounts) { + it("creates 10000 initial tokens", function(done) { + var tester = TokenTester.at(TokenTester.deployed_address); + tester.tokenContractAddress.call() + .then(function(tokenContractAddr) { + var tokenContract = HumanStandardToken.at(tokenContractAddr); + return tokenContract.balanceOf.call(TokenTester.deployed_address); + }).then(function (result) { + assert.strictEqual(result.toNumber(), 10000); // 10000 as specified in TokenTester.sol + done(); + }).catch(done); + }); + + //todo:add test on retrieving addresses +});*/ diff --git a/truffle.js b/truffle.js index 7273191..3e11662 100644 --- a/truffle.js +++ b/truffle.js @@ -1,11 +1,20 @@ +var HDWalletProvider = require("truffle-hdwallet-provider"); +var mnemonic = ""; // 12 word mnemonic +var provider = new HDWalletProvider(mnemonic, "https://ropsten.infura.io/"); + +// display address to screen to fund for further operations +console.log(provider.getAddress()); + module.exports = { - "build": "webpack", - "deploy": [ - "HumanStandardToken" - ], - "rpc": { - // Default RPC configuration. - "host": "localhost", - "port": 8545 + networks: { + development: { + host: "localhost", + port: 8545, + network_id: "*" // Match any network id + }, + ropsten: { + provider: provider, + network_id: 3 + } } -} +};