diff --git a/README.md b/README.md index 8d8ac3bd4..4ef3b8096 100644 --- a/README.md +++ b/README.md @@ -22,22 +22,90 @@ npm i zeppelin-solidity After that, you'll get all the library's contracts in the `contracts/zeppelin` folder. You can use the contracts in the library like so: ```js -import "./zeppelin/Rejector.sol"; +import "./zeppelin/Ownable.sol"; -contract MetaCoin is Rejector { +contract MyContract is Ownable { ... } ``` > NOTE: The current distribution channel is npm, which is not ideal. [We're looking into providing a better tool for code distribution](https://github.com/OpenZeppelin/zeppelin-solidity/issues/13), and ideas are welcome. +## Add your own bounty contract + +To create a bounty for your contract, inherit from the base Bounty contract and provide an implementation for `deployContract()` returning the new contract address. + +``` +import "./zeppelin/Bounty.sol"; +import "./YourContract.sol"; + +contract YourBounty is Bounty { + function deployContract() internal returns(address) { + return new YourContract() + } +} +``` + +### Implement invariant logic into your smart contract + +At contracts/YourContract.sol + +``` +contract YourContract { + function checkInvariant() returns(bool) { + // Implement your logic to make sure that none of the state is broken. + } +} +``` + +### Deploy your bounty contract as usual + +At `migrations/2_deploy_contracts.js` + +``` +module.exports = function(deployer) { + deployer.deploy(YourContract); + deployer.deploy(YourBounty); +}; +``` + +### Add a reward to the bounty contract + +After deploying the contract, send rewards money into the bounty contract. + +From `truffle console` + +``` +address = 'your account address' +reward = 'reward to pay to a researcher' + +web3.eth.sendTransaction({ + from:address, + to:bounty.address, + value: web3.toWei(reward, "ether") +} + +``` + +### Researchers hack the contract and claim their reward. + +For each researcher who wants to hack the contract and claims the reward, refer to our [test](./test/Bounty.js) for the detail. + +### Ends the contract + +If you manage to protect your contract from security researchers and wants to end the bounty, kill the contract so that all the rewards go back to the owner of the bounty contract. + +``` +bounty.kill() +``` + #### Truffle Beta Support We also support Truffle Beta npm integration. If you're using Truffle Beta, the contracts in `node_modules` will be enough, so feel free to delete the copies at your `contracts` folder. If you're using Truffle Beta, you can use Zeppelin contracts like so: ```js -import "zeppelin-solidity/contracts/Rejector.sol"; +import "zeppelin-solidity/contracts/Ownable.sol"; -contract MetaCoin is Rejector { +contract MyContract is Ownable { ... } ``` @@ -61,8 +129,20 @@ Interested in contributing to Zeppelin? - Issue tracker: https://github.com/OpenZeppelin/zeppelin-solidity/issues - Contribution guidelines: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/CONTRIBUTING.md -## Projects using Zeppelin -- [Blockparty](https://github.com/makoto/blockparty) +## Collaborating organizations and audits by Zeppelin +- [Golem](https://golem.network/) +- [Mediachain](https://golem.network/) +- [Truffle](http://truffleframework.com/) +- [Firstblood](http://firstblood.io/) +- [Rootstock](http://www.rsk.co/) +- [Consensys](https://consensys.net/) +- [DigixGlobal](https://www.dgx.io/) +- [Coinfund](https://coinfund.io/) +- [DemocracyEarth](http://democracy.earth/) +- [Signatura](https://signatura.co/) +- [Ether.camp](http://www.ether.camp/) + +among others... ## Contracts TODO diff --git a/contracts/Bounty.sol b/contracts/Bounty.sol new file mode 100644 index 000000000..0f2ab6c2b --- /dev/null +++ b/contracts/Bounty.sol @@ -0,0 +1,50 @@ +pragma solidity ^0.4.4; +import './PullPayment.sol'; +import './Killable.sol'; + +/* + * Bounty + * This bounty will pay out to a researcher if he/she breaks invariant logic of + * the contract you bet reward against. + */ + +contract Target { + function checkInvariant() returns(bool); +} + +contract Bounty is PullPayment, Killable { + Target target; + bool public claimed; + mapping(address => address) public researchers; + + event TargetCreated(address createdAddress); + + function() payable { + if (claimed) throw; + } + + function createTarget() returns(Target) { + target = Target(deployContract()); + researchers[target] = msg.sender; + TargetCreated(target); + return target; + } + + function deployContract() internal returns(address); + + function checkInvariant() returns(bool){ + return target.checkInvariant(); + } + + function claim(Target target) { + address researcher = researchers[target]; + if (researcher == 0) throw; + // Check Target contract invariants + if (target.checkInvariant()) { + throw; + } + asyncSend(researcher, this.balance); + claimed = true; + } + +} diff --git a/contracts/Claimable.sol b/contracts/Claimable.sol new file mode 100644 index 000000000..40e38d49c --- /dev/null +++ b/contracts/Claimable.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.4.0; +import './Ownable.sol'; + +/* + * Claimable + * Extension for the Ownable contract, where the ownership needs to be claimed + */ + +contract Claimable is Ownable { + address public pendingOwner; + + modifier onlyPendingOwner() { + if (msg.sender == pendingOwner) + _; + } + + function transfer(address newOwner) onlyOwner { + pendingOwner = newOwner; + } + + function claimOwnership() onlyPendingOwner { + owner = pendingOwner; + pendingOwner = 0x0; + } + +} diff --git a/contracts/Killable.sol b/contracts/Killable.sol index 28e5d9e98..485576d76 100644 --- a/contracts/Killable.sol +++ b/contracts/Killable.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; import "./Ownable.sol"; /* @@ -7,6 +7,6 @@ import "./Ownable.sol"; */ contract Killable is Ownable { function kill() { - if (msg.sender == owner) suicide(owner); + if (msg.sender == owner) selfdestruct(owner); } } diff --git a/contracts/LimitBalance.sol b/contracts/LimitBalance.sol new file mode 100644 index 000000000..d8c29baec --- /dev/null +++ b/contracts/LimitBalance.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.4.4; +contract LimitBalance { + + uint public limit; + + function LimitBalance(uint _limit) { + limit = _limit; + } + + modifier limitedPayable() { + if (this.balance > limit) { + throw; + } + _; + + } + +} diff --git a/contracts/LimitFunds.sol b/contracts/LimitFunds.sol deleted file mode 100644 index 5f3200c33..000000000 --- a/contracts/LimitFunds.sol +++ /dev/null @@ -1,12 +0,0 @@ -pragma solidity ^0.4.0; -contract LimitFunds { - - uint LIMIT = 5000; - - function() { throw; } - - function deposit() { - if (this.balance > LIMIT) throw; - } - -} diff --git a/contracts/Migrations.sol b/contracts/Migrations.sol index 608bba8f9..5e1a61e11 100644 --- a/contracts/Migrations.sol +++ b/contracts/Migrations.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; contract Migrations { address public owner; uint public last_completed_migration; diff --git a/contracts/Ownable.sol b/contracts/Ownable.sol index ab8e9583d..2998fa403 100644 --- a/contracts/Ownable.sol +++ b/contracts/Ownable.sol @@ -1,4 +1,5 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; + /* * Ownable * Base contract with an owner @@ -16,7 +17,7 @@ contract Ownable { } function transfer(address newOwner) onlyOwner { - owner = newOwner; + if (newOwner != address(0)) owner = newOwner; } } diff --git a/contracts/PullPayment.sol b/contracts/PullPayment.sol index b8d094455..c05f99caf 100644 --- a/contracts/PullPayment.sol +++ b/contracts/PullPayment.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; /* * PullPayment * Base contract supporting async send for pull payments. diff --git a/contracts/Rejector.sol b/contracts/Rejector.sol deleted file mode 100644 index 05525d5e9..000000000 --- a/contracts/Rejector.sol +++ /dev/null @@ -1,9 +0,0 @@ -pragma solidity ^0.4.0; -/* - * Rejector - * Base contract for rejecting direct deposits. - * Fallback function throws immediately. - */ -contract Rejector { - function() { throw; } -} diff --git a/contracts/SafeMath.sol b/contracts/SafeMath.sol index 2d4bebf77..16e8dae76 100644 --- a/contracts/SafeMath.sol +++ b/contracts/SafeMath.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; /** * Math operations with safety checks diff --git a/contracts/Stoppable.sol b/contracts/Stoppable.sol index d649ce019..ad48f6bc5 100644 --- a/contracts/Stoppable.sol +++ b/contracts/Stoppable.sol @@ -1,24 +1,23 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; + +import "./Ownable.sol"; /* * Stoppable * Abstract contract that allows children to implement an - * emergency stop mechanism. + * emergency stop mechanism. */ -contract Stoppable { - address public curator; +contract Stoppable is Ownable { bool public stopped; modifier stopInEmergency { if (!stopped) _; } modifier onlyInEmergency { if (stopped) _; } - function Stoppable(address _curator) { - if (_curator == 0) throw; - curator = _curator; - } - - function emergencyStop() external { - if (msg.sender != curator) throw; + function emergencyStop() external onlyOwner { stopped = true; } + function release() external onlyOwner onlyInEmergency { + stopped = false; + } + } diff --git a/contracts/bounties/CrowdsaleTokenBounty.sol b/contracts/bounties/CrowdsaleTokenBounty.sol deleted file mode 100644 index 74b4c0cd4..000000000 --- a/contracts/bounties/CrowdsaleTokenBounty.sol +++ /dev/null @@ -1,38 +0,0 @@ -pragma solidity ^0.4.0; -import '../PullPayment.sol'; -import '../token/CrowdsaleToken.sol'; - -/* - * Bounty - * This bounty will pay out if you can cause a CrowdsaleToken's balance - * to be lower than its totalSupply, which would mean that it doesn't - * have sufficient ether for everyone to withdraw. - */ -contract CrowdsaleTokenBounty is PullPayment { - - bool public claimed; - mapping(address => address) public researchers; - - function() { - if (claimed) throw; - } - - function createTarget() returns(CrowdsaleToken) { - CrowdsaleToken target = new CrowdsaleToken(); - researchers[target] = msg.sender; - return target; - } - - function claim(CrowdsaleToken target) { - address researcher = researchers[target]; - if (researcher == 0) throw; - // Check CrowdsaleToken contract invariants - // Customize this to the specifics of your contract - if (target.totalSupply() == target.balance) { - throw; - } - asyncSend(researcher, this.balance); - claimed = true; - } - -} diff --git a/contracts/bounties/SimpleTokenBounty.sol b/contracts/bounties/SimpleTokenBounty.sol deleted file mode 100644 index 4b54ded6d..000000000 --- a/contracts/bounties/SimpleTokenBounty.sol +++ /dev/null @@ -1,38 +0,0 @@ -pragma solidity ^0.4.0; -import '../PullPayment.sol'; -import '../token/SimpleToken.sol'; - -/* - * Bounty - * This bounty will pay out if you can cause a SimpleToken's balance - * to be lower than its totalSupply, which would mean that it doesn't - * have sufficient ether for everyone to withdraw. - */ -contract SimpleTokenBounty is PullPayment { - - bool public claimed; - mapping(address => address) public researchers; - - function() { - if (claimed) throw; - } - - function createTarget() returns(SimpleToken) { - SimpleToken target = new SimpleToken(); - researchers[target] = msg.sender; - return target; - } - - function claim(SimpleToken target) { - address researcher = researchers[target]; - if (researcher == 0) throw; - // Check SimpleToken contract invariants - // Customize this to the specifics of your contract - if (target.totalSupply() == target.balance) { - throw; - } - asyncSend(researcher, this.balance); - claimed = true; - } - -} diff --git a/contracts/examples/BadArrayUse.sol b/contracts/examples/BadArrayUse.sol index d76160dfd..d5c1c7b7b 100644 --- a/contracts/examples/BadArrayUse.sol +++ b/contracts/examples/BadArrayUse.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; import '../PullPayment.sol'; // UNSAFE CODE, DO NOT USE! diff --git a/contracts/examples/BadFailEarly.sol b/contracts/examples/BadFailEarly.sol index 801ed4754..0ad95a92e 100644 --- a/contracts/examples/BadFailEarly.sol +++ b/contracts/examples/BadFailEarly.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; // UNSAFE CODE, DO NOT USE! contract BadFailEarly { diff --git a/contracts/examples/BadPushPayments.sol b/contracts/examples/BadPushPayments.sol index c8d6b9d19..510b39473 100644 --- a/contracts/examples/BadPushPayments.sol +++ b/contracts/examples/BadPushPayments.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; // UNSAFE CODE, DO NOT USE! contract BadPushPayments { diff --git a/contracts/examples/GoodArrayUse.sol b/contracts/examples/GoodArrayUse.sol index 150eb6ba0..ce450293e 100644 --- a/contracts/examples/GoodArrayUse.sol +++ b/contracts/examples/GoodArrayUse.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; import '../PullPayment.sol'; contract GoodArrayUse is PullPayment { diff --git a/contracts/examples/GoodFailEarly.sol b/contracts/examples/GoodFailEarly.sol index 1dc1b8d32..7a3ec9e60 100644 --- a/contracts/examples/GoodFailEarly.sol +++ b/contracts/examples/GoodFailEarly.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; contract GoodFailEarly { diff --git a/contracts/examples/GoodPullPayments.sol b/contracts/examples/GoodPullPayments.sol index 6a03bbc02..9236d0a4f 100644 --- a/contracts/examples/GoodPullPayments.sol +++ b/contracts/examples/GoodPullPayments.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; contract GoodPullPayments { address highestBidder; uint highestBid; diff --git a/contracts/examples/ProofOfExistence.sol b/contracts/examples/ProofOfExistence.sol index 95f2412cb..a4ec0d45f 100644 --- a/contracts/examples/ProofOfExistence.sol +++ b/contracts/examples/ProofOfExistence.sol @@ -1,12 +1,10 @@ -pragma solidity ^0.4.0; - -import "../Rejector.sol"; +pragma solidity ^0.4.4; /* * Proof of Existence example contract * see https://medium.com/zeppelin-blog/the-hitchhikers-guide-to-smart-contracts-in-ethereum-848f08001f05 */ -contract ProofOfExistence is Rejector { +contract ProofOfExistence { mapping (bytes32 => bool) public proofs; diff --git a/contracts/examples/PullPaymentBid.sol b/contracts/examples/PullPaymentBid.sol index 5cf4fe578..01c91c991 100644 --- a/contracts/examples/PullPaymentBid.sol +++ b/contracts/examples/PullPaymentBid.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; import '../PullPayment.sol'; diff --git a/contracts/examples/StoppableBid.sol b/contracts/examples/StoppableBid.sol index 364c186f8..0edbd8b3e 100644 --- a/contracts/examples/StoppableBid.sol +++ b/contracts/examples/StoppableBid.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; import '../PullPayment.sol'; import '../Stoppable.sol'; @@ -7,10 +7,6 @@ contract StoppableBid is Stoppable, PullPayment { address public highestBidder; uint public highestBid; - function StoppableBid(address _curator) - Stoppable(_curator) - PullPayment() {} - function bid() external stopInEmergency { if (msg.value <= highestBid) throw; @@ -22,7 +18,7 @@ contract StoppableBid is Stoppable, PullPayment { } function withdraw() onlyInEmergency { - suicide(curator); + selfdestruct(owner); } } diff --git a/contracts/test-helpers/BasicTokenMock.sol b/contracts/test-helpers/BasicTokenMock.sol new file mode 100644 index 000000000..9f19e47ea --- /dev/null +++ b/contracts/test-helpers/BasicTokenMock.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.4.4; +import '../token/BasicToken.sol'; + +// mock class using BasicToken +contract BasicTokenMock is BasicToken { + + function BasicTokenMock(address initialAccount, uint initialBalance) { + balances[initialAccount] = initialBalance; + totalSupply = initialBalance; + } + +} diff --git a/contracts/test-helpers/InsecureTargetBounty.sol b/contracts/test-helpers/InsecureTargetBounty.sol new file mode 100644 index 000000000..5087cb6a6 --- /dev/null +++ b/contracts/test-helpers/InsecureTargetBounty.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.4.4; + +import "../Bounty.sol"; + +contract InsecureTargetMock { + function checkInvariant() returns(bool){ + return false; + } +} + +contract InsecureTargetBounty is Bounty { + function deployContract() internal returns (address) { + return new InsecureTargetMock(); + } +} diff --git a/contracts/test-helpers/LimitBalanceMock.sol b/contracts/test-helpers/LimitBalanceMock.sol new file mode 100644 index 000000000..c0f05ba8b --- /dev/null +++ b/contracts/test-helpers/LimitBalanceMock.sol @@ -0,0 +1,10 @@ +pragma solidity ^0.4.4; +import '../LimitBalance.sol'; + +// mock class using LimitBalance +contract LimitBalanceMock is LimitBalance(1000) { + + function limitedDeposit() payable limitedPayable { + } + +} diff --git a/contracts/test-helpers/PullPaymentMock.sol b/contracts/test-helpers/PullPaymentMock.sol index 51cb3e107..0c8c3d943 100644 --- a/contracts/test-helpers/PullPaymentMock.sol +++ b/contracts/test-helpers/PullPaymentMock.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; import '../PullPayment.sol'; // mock class using PullPayment diff --git a/contracts/test-helpers/SecureTargetBounty.sol b/contracts/test-helpers/SecureTargetBounty.sol new file mode 100644 index 000000000..f83daf407 --- /dev/null +++ b/contracts/test-helpers/SecureTargetBounty.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.4.4; + +import "../Bounty.sol"; + +contract SecureTargetMock { + function checkInvariant() returns(bool){ + return true; + } +} + +contract SecureTargetBounty is Bounty { + function deployContract() internal returns (address) { + return new SecureTargetMock(); + } +} diff --git a/contracts/test-helpers/StandardTokenMock.sol b/contracts/test-helpers/StandardTokenMock.sol new file mode 100644 index 000000000..5b222be9a --- /dev/null +++ b/contracts/test-helpers/StandardTokenMock.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.4.4; +import '../token/StandardToken.sol'; + +// mock class using StandardToken +contract StandardTokenMock is StandardToken { + + function StandardTokenMock(address initialAccount, uint initialBalance) { + balances[initialAccount] = initialBalance; + totalSupply = initialBalance; + } + +} diff --git a/contracts/test-helpers/StoppableMock.sol b/contracts/test-helpers/StoppableMock.sol new file mode 100644 index 000000000..6d3464bdf --- /dev/null +++ b/contracts/test-helpers/StoppableMock.sol @@ -0,0 +1,22 @@ +pragma solidity ^0.4.4; +import '../Stoppable.sol'; + +// mock class using Stoppable +contract StoppableMock is Stoppable { + bool public drasticMeasureTaken; + uint public count; + + function StoppableMock() { + drasticMeasureTaken = false; + count = 0; + } + + function normalProcess() external stopInEmergency { + count++; + } + + function drasticMeasure() external onlyInEmergency { + drasticMeasureTaken = true; + } + +} diff --git a/contracts/token/BasicToken.sol b/contracts/token/BasicToken.sol new file mode 100644 index 000000000..704de675d --- /dev/null +++ b/contracts/token/BasicToken.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.4.4; + +import './ERC20Basic.sol'; +import '../SafeMath.sol'; + +/** + * Basic token + * Basic version of StandardToken, with no allowances + */ +contract BasicToken is ERC20Basic, SafeMath { + + mapping(address => uint) balances; + + function transfer(address _to, uint _value) { + if (balances[msg.sender] < _value) { + throw; + } + balances[msg.sender] = safeSub(balances[msg.sender], _value); + balances[_to] = safeAdd(balances[_to], _value); + Transfer(msg.sender, _to, _value); + } + + function balanceOf(address _owner) constant returns (uint balance) { + return balances[_owner]; + } + +} diff --git a/contracts/token/CrowdsaleToken.sol b/contracts/token/CrowdsaleToken.sol index 7fd363cb0..ba3100cbc 100644 --- a/contracts/token/CrowdsaleToken.sol +++ b/contracts/token/CrowdsaleToken.sol @@ -1,34 +1,34 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; -import "../StandardToken.sol"; +import "./StandardToken.sol"; /* * Simple ERC20 Token example, with crowdsale token creation */ contract CrowdsaleToken is StandardToken { - string public name = "CrowdsaleToken"; - string public symbol = "CRW"; - uint public decimals = 18; + string public name = "CrowdsaleToken"; + string public symbol = "CRW"; + uint public decimals = 18; - // 1 ether = 500 example tokens - uint PRICE = 500; + // 1 ether = 500 example tokens + uint PRICE = 500; - function () payable { - createTokens(msg.sender); - } - - function createTokens(address recipient) payable { - if (msg.value == 0) throw; - - uint tokens = safeMul(msg.value, getPrice()); + function () payable { + createTokens(msg.sender); + } + + function createTokens(address recipient) payable { + if (msg.value == 0) throw; - totalSupply = safeAdd(totalSupply, tokens); - balances[recipient] = safeAdd(balances[recipient], tokens); - } - - // replace this with any other price function - function getPrice() constant returns (uint result){ - return PRICE; - } + uint tokens = safeMul(msg.value, getPrice()); + + totalSupply = safeAdd(totalSupply, tokens); + balances[recipient] = safeAdd(balances[recipient], tokens); + } + + // replace this with any other price function + function getPrice() constant returns (uint result){ + return PRICE; + } } diff --git a/contracts/ERC20.sol b/contracts/token/ERC20.sol similarity index 96% rename from contracts/ERC20.sol rename to contracts/token/ERC20.sol index 42b65a617..9d32c69fd 100644 --- a/contracts/ERC20.sol +++ b/contracts/token/ERC20.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; // see https://github.com/ethereum/EIPs/issues/20 diff --git a/contracts/token/ERC20Basic.sol b/contracts/token/ERC20Basic.sol new file mode 100644 index 000000000..4f7ca36d1 --- /dev/null +++ b/contracts/token/ERC20Basic.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.4.4; + + +contract ERC20Basic { + uint public totalSupply; + function balanceOf(address who) constant returns (uint); + function transfer(address to, uint value); + event Transfer(address indexed from, address indexed to, uint value); +} diff --git a/contracts/token/SimpleToken.sol b/contracts/token/SimpleToken.sol index ccb34769f..fc7efd0d6 100644 --- a/contracts/token/SimpleToken.sol +++ b/contracts/token/SimpleToken.sol @@ -1,6 +1,6 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; -import "../StandardToken.sol"; +import "./StandardToken.sol"; /* * Very simple ERC20 Token example, where all tokens are pre-assigned @@ -9,14 +9,14 @@ import "../StandardToken.sol"; */ contract SimpleToken is StandardToken { - string public name = "SimpleToken"; - string public symbol = "SIM"; - uint public decimals = 18; - uint public INITIAL_SUPPLY = 10000; - - function SimpleToken() { - totalSupply = INITIAL_SUPPLY; - balances[msg.sender] = INITIAL_SUPPLY; - } + string public name = "SimpleToken"; + string public symbol = "SIM"; + uint public decimals = 18; + uint public INITIAL_SUPPLY = 10000; + + function SimpleToken() { + totalSupply = INITIAL_SUPPLY; + balances[msg.sender] = INITIAL_SUPPLY; + } } diff --git a/contracts/StandardToken.sol b/contracts/token/StandardToken.sol similarity index 96% rename from contracts/StandardToken.sol rename to contracts/token/StandardToken.sol index 69b8e186f..f2a2138bd 100644 --- a/contracts/StandardToken.sol +++ b/contracts/token/StandardToken.sol @@ -1,7 +1,7 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; import './ERC20.sol'; -import './SafeMath.sol'; +import '../SafeMath.sol'; /** * ERC20 token diff --git a/migrations/2_deploy_contracts.js b/migrations/2_deploy_contracts.js index 8b400d5d1..012dc9590 100644 --- a/migrations/2_deploy_contracts.js +++ b/migrations/2_deploy_contracts.js @@ -2,8 +2,11 @@ module.exports = function(deployer) { deployer.deploy(PullPaymentBid); deployer.deploy(BadArrayUse); deployer.deploy(ProofOfExistence); - deployer.deploy(SimpleTokenBounty); - deployer.deploy(CrowdsaleTokenBounty); deployer.deploy(Ownable); + deployer.deploy(Claimable); deployer.deploy(LimitFunds); + if(deployer.network == 'test'){ + deployer.deploy(SecureTargetBounty); + deployer.deploy(InsecureTargetBounty); + }; }; diff --git a/package.json b/package.json index b9c0b7a11..cb1549efb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zeppelin-solidity", - "version": "0.0.10", + "version": "0.0.11", "description": "Secure Smart Contract library for Solidity", "main": "truffle.js", "devDependencies": {}, diff --git a/test/BasicToken.js b/test/BasicToken.js new file mode 100644 index 000000000..12b411c41 --- /dev/null +++ b/test/BasicToken.js @@ -0,0 +1,49 @@ +contract('BasicToken', function(accounts) { + + it("should return the correct totalSupply after construction", function(done) { + return BasicTokenMock.new(accounts[0], 100) + .then(function(token) { + return token.totalSupply(); + }) + .then(function(totalSupply) { + assert.equal(totalSupply, 100); + }) + .then(done); + }) + + it("should return correct balances after transfer", function(done) { + var token; + return BasicTokenMock.new(accounts[0], 100) + .then(function(_token) { + token = _token; + return token.transfer(accounts[1], 100); + }) + .then(function() { + return token.balanceOf(accounts[0]); + }) + .then(function(balance) { + assert.equal(balance, 0); + }) + .then(function() { + return token.balanceOf(accounts[1]); + }) + .then(function(balance) { + assert.equal(balance, 100); + }) + .then(done); + }); + + it("should throw an error when trying to transfer more than balance", function(done) { + var token; + return BasicTokenMock.new(accounts[0], 100) + .then(function(_token) { + token = _token; + return token.transfer(accounts[1], 101); + }) + .catch(function(error) { + if (error.message.search('invalid JUMP') == -1) throw error + }) + .then(done); + }); + +}); diff --git a/test/Bounty.js b/test/Bounty.js new file mode 100644 index 000000000..17eb80a66 --- /dev/null +++ b/test/Bounty.js @@ -0,0 +1,141 @@ +var sendReward = function(sender, receiver, value){ + web3.eth.sendTransaction({ + from:sender, + to:receiver, + value: value + }) +} + +contract('Bounty', function(accounts) { + + it("sets reward", function(done){ + var owner = accounts[0]; + var reward = web3.toWei(1, "ether"); + + SecureTargetBounty.new(). + then(function(bounty){ + sendReward(owner, bounty.address, reward); + assert.equal(reward, web3.eth.getBalance(bounty.address).toNumber()) + }). + then(done); + }) + + it("empties itself when killed", function(done){ + var owner = accounts[0]; + var reward = web3.toWei(1, "ether"); + var bounty; + + SecureTargetBounty.new(). + then(function(_bounty){ + bounty = _bounty; + sendReward(owner, bounty.address, reward); + assert.equal(reward, web3.eth.getBalance(bounty.address).toNumber()) + return bounty.kill() + }). + then(function(){ + assert.equal(0, web3.eth.getBalance(bounty.address).toNumber()) + }). + then(done); + }) + + describe("Against secure contract", function(){ + it("checkInvariant returns true", function(done){ + var bounty; + + SecureTargetBounty.new(). + then(function(_bounty) { + bounty = _bounty; + return bounty.createTarget(); + }). + then(function() { + return bounty.checkInvariant.call() + }). + then(function(result) { + assert.isTrue(result); + }). + then(done); + }) + + it("cannot claim reward", function(done){ + var owner = accounts[0]; + var researcher = accounts[1]; + var reward = web3.toWei(1, "ether"); + + SecureTargetBounty.new(). + then(function(bounty) { + var event = bounty.TargetCreated({}); + event.watch(function(err, result) { + event.stopWatching(); + if (err) { throw err } + var targetAddress = result.args.createdAddress; + sendReward(owner, bounty.address, reward); + assert.equal(reward, web3.eth.getBalance(bounty.address).toNumber()) + bounty.claim(targetAddress, {from:researcher}). + then(function(){ throw("should not come here")}). + catch(function() { + return bounty.claimed.call(); + }). + then(function(result) { + assert.isFalse(result); + bounty.withdrawPayments({from:researcher}). + then(function(){ throw("should not come here")}). + catch(function() { + assert.equal(reward, web3.eth.getBalance(bounty.address).toNumber()) + done(); + }) + }) + }) + bounty.createTarget({from:researcher}); + }) + }) + }) + + describe("Against broken contract", function(){ + it("checkInvariant returns false", function(done){ + var bounty; + + InsecureTargetBounty.new(). + then(function(_bounty) { + bounty = _bounty; + return bounty.createTarget(); + }). + then(function() { + return bounty.checkInvariant.call() + }). + then(function(result) { + assert.isFalse(result); + }). + then(done); + }) + + it("claims reward", function(done){ + var owner = accounts[0]; + var researcher = accounts[1]; + var reward = web3.toWei(1, "ether"); + + InsecureTargetBounty.new(). + then(function(bounty) { + var event = bounty.TargetCreated({}); + event.watch(function(err, result) { + event.stopWatching(); + if (err) { throw err } + var targetAddress = result.args.createdAddress; + sendReward(owner, bounty.address, reward); + assert.equal(reward, web3.eth.getBalance(bounty.address).toNumber()) + bounty.claim(targetAddress, {from:researcher}). + then(function() { + return bounty.claimed.call(); + }). + then(function(result) { + assert.isTrue(result); + return bounty.withdrawPayments({from:researcher}) + }). + then(function() { + assert.equal(0, web3.eth.getBalance(bounty.address).toNumber()) + }).then(done); + }) + bounty.createTarget({from:researcher}); + }) + }) + }) +}); diff --git a/test/Claimable.js b/test/Claimable.js new file mode 100644 index 000000000..464b7406e --- /dev/null +++ b/test/Claimable.js @@ -0,0 +1,71 @@ +contract('Claimable', function(accounts) { + var claimable; + + beforeEach(function() { + return Claimable.new().then(function(deployed) { + claimable = deployed; + }); + }); + + it("should have an owner", function(done) { + return claimable.owner() + .then(function(owner) { + assert.isTrue(owner != 0); + }) + .then(done) + }); + + it("changes pendingOwner after transfer", function(done) { + var newOwner = accounts[1]; + return claimable.transfer(newOwner) + .then(function() { + return claimable.pendingOwner(); + }) + .then(function(pendingOwner) { + assert.isTrue(pendingOwner === newOwner); + }) + .then(done) + }); + + it("should prevent to claimOwnership from no pendingOwner", function(done) { + return claimable.claimOwnership({from: accounts[2]}) + .then(function() { + return claimable.owner(); + }) + .then(function(owner) { + assert.isTrue(owner != accounts[2]); + }) + .then(done) + }); + + it("should prevent non-owners from transfering" ,function(done) { + return claimable.transfer(accounts[2], {from: accounts[2]}) + .then(function() { + return claimable.pendingOwner(); + }) + .then(function(pendingOwner) { + assert.isFalse(pendingOwner === accounts[2]); + }) + .then(done) + }); + + describe("after initiating a transfer", function () { + var newOwner; + + beforeEach(function () { + newOwner = accounts[1]; + return claimable.transfer(newOwner); + }); + + it("changes allow pending owner to claim ownership", function(done) { + return claimable.claimOwnership({from: newOwner}) + .then(function() { + return claimable.owner(); + }) + .then(function(owner) { + assert.isTrue(owner === newOwner); + }) + .then(done) + }); + }); +}); diff --git a/test/LimitBalance.js b/test/LimitBalance.js new file mode 100644 index 000000000..ce75821a0 --- /dev/null +++ b/test/LimitBalance.js @@ -0,0 +1,64 @@ +contract('LimitBalance', function(accounts) { + var lb; + + beforeEach(function() { + return LimitBalanceMock.new().then(function(deployed) { + lb = deployed; + }); + }); + + var LIMIT = 1000; + + it("should expose limit", function(done) { + return lb.limit() + .then(function(limit) { + assert.equal(limit, LIMIT); + }) + .then(done) + }); + + it("should allow sending below limit", function(done) { + var amount = 1; + return lb.limitedDeposit({value: amount}) + .then(function() { + assert.equal(web3.eth.getBalance(lb.address), amount); + }) + .then(done) + }); + + it("shouldnt allow sending above limit", function(done) { + var amount = 1100; + return lb.limitedDeposit({value: amount}) + .catch(function(error) { + if (error.message.search('invalid JUMP') == -1) throw error + }) + .then(done) + }); + + it("should allow multiple sends below limit", function(done) { + var amount = 500; + return lb.limitedDeposit({value: amount}) + .then(function() { + assert.equal(web3.eth.getBalance(lb.address), amount); + return lb.limitedDeposit({value: amount}) + }) + .then(function() { + assert.equal(web3.eth.getBalance(lb.address), amount*2); + }) + .then(done) + }); + + it("shouldnt allow multiple sends above limit", function(done) { + var amount = 500; + return lb.limitedDeposit({value: amount}) + .then(function() { + assert.equal(web3.eth.getBalance(lb.address), amount); + return lb.limitedDeposit({value: amount+1}) + }) + .catch(function(error) { + if (error.message.search('invalid JUMP') == -1) throw error; + }) + .then(done) + }); + +}); diff --git a/test/Ownable.js b/test/Ownable.js index 83ae4b93e..2e9f59c55 100644 --- a/test/Ownable.js +++ b/test/Ownable.js @@ -1,6 +1,13 @@ contract('Ownable', function(accounts) { + var ownable; + + beforeEach(function() { + return Ownable.new().then(function(deployed) { + ownable = deployed; + }); + }); + it("should have an owner", function(done) { - var ownable = Ownable.deployed(); return ownable.owner() .then(function(owner) { assert.isTrue(owner != 0); @@ -9,7 +16,6 @@ contract('Ownable', function(accounts) { }); it("changes owner after transfer", function(done) { - var ownable = Ownable.deployed(); var other = accounts[1]; return ownable.transfer(other) .then(function() { @@ -22,7 +28,6 @@ contract('Ownable', function(accounts) { }); it("should prevent non-owners from transfering" ,function(done) { - var ownable = Ownable.deployed(); var other = accounts[2]; return ownable.transfer(other, {from: accounts[2]}) .then(function() { @@ -34,4 +39,20 @@ contract('Ownable', function(accounts) { .then(done) }); + it("should guard ownership against stuck state" ,function(done) { + var ownable = Ownable.deployed(); + + return ownable.owner() + .then(function (originalOwner) { + return ownable.transfer(null, {from: originalOwner}) + .then(function() { + return ownable.owner(); + }) + .then(function(newOwner) { + assert.equal(originalOwner, newOwner); + }) + .then(done); + }); + }); + }); diff --git a/test/StandardToken.js b/test/StandardToken.js new file mode 100644 index 000000000..ec2f158cd --- /dev/null +++ b/test/StandardToken.js @@ -0,0 +1,108 @@ +contract('StandardToken', function(accounts) { + + it("should return the correct totalSupply after construction", function(done) { + return StandardTokenMock.new(accounts[0], 100) + .then(function(token) { + return token.totalSupply(); + }) + .then(function(totalSupply) { + assert.equal(totalSupply, 100); + }) + .then(done); + }) + + it("should return the correct allowance amount after approval", function(done) { + var token; + return StandardTokenMock.new() + .then(function(_token) { + token = _token; + return token.approve(accounts[1], 100); + }) + .then(function() { + return token.allowance(accounts[0], accounts[1]); + }) + .then(function(allowance) { + assert.equal(allowance, 100); + }) + .then(done); + }); + + it("should return correct balances after transfer", function(done) { + var token; + return StandardTokenMock.new(accounts[0], 100) + .then(function(_token) { + token = _token; + return token.transfer(accounts[1], 100); + }) + .then(function() { + return token.balanceOf(accounts[0]); + }) + .then(function(balance) { + assert.equal(balance, 0); + }) + .then(function() { + return token.balanceOf(accounts[1]); + }) + .then(function(balance) { + assert.equal(balance, 100); + }) + .then(done); + }); + + it("should throw an error when trying to transfer more than balance", function(done) { + var token; + return StandardTokenMock.new(accounts[0], 100) + .then(function(_token) { + token = _token; + return token.transfer(accounts[1], 101); + }) + .catch(function(error) { + if (error.message.search('invalid JUMP') == -1) throw error + }) + .then(done); + }); + + it("should return correct balances after transfering from another account", function(done) { + var token; + return StandardTokenMock.new(accounts[0], 100) + .then(function(_token) { + token = _token; + return token.approve(accounts[1], 100); + }) + .then(function() { + return token.transferFrom(accounts[0], accounts[2], 100, {from: accounts[1]}); + }) + .then(function() { + return token.balanceOf(accounts[0]); + }) + .then(function(balance) { + assert.equal(balance, 0); + return token.balanceOf(accounts[2]); + }) + .then(function(balance) { + assert.equal(balance, 100) + return token.balanceOf(accounts[1]); + }) + .then(function(balance) { + assert.equal(balance, 0); + }) + .then(done); + }); + + it("should throw an error when trying to transfer more than allowed", function(done) { + var token; + return StandardTokenMock.new(accounts[0], 100) + .then(function(_token) { + token = _token; + return token.approve(accounts[1], 99); + }) + .then(function() { + return token.transferFrom(accounts[0], accounts[2], 100, {from: accounts[1]}); + }) + .catch(function(error) { + if (error.message.search('invalid JUMP') == -1) throw error + }) + .then(done); + }); + +}); diff --git a/test/Stoppable.js b/test/Stoppable.js new file mode 100644 index 000000000..3fa4f3ad2 --- /dev/null +++ b/test/Stoppable.js @@ -0,0 +1,108 @@ +contract('Stoppable', function(accounts) { + + it("can perform normal process in non-emergency", function(done) { + var stoppable; + return StoppableMock.new() + .then(function(_stoppable) { + stoppable = _stoppable; + return stoppable.count(); + }) + .then(function(count) { + assert.equal(count, 0); + }) + .then(function () { + return stoppable.normalProcess(); + }) + .then(function() { + return stoppable.count(); + }) + .then(function(count) { + assert.equal(count, 1); + }) + .then(done); + }); + + it("can not perform normal process in emergency", function(done) { + var stoppable; + return StoppableMock.new() + .then(function(_stoppable) { + stoppable = _stoppable; + return stoppable.emergencyStop(); + }) + .then(function () { + return stoppable.count(); + }) + .then(function(count) { + assert.equal(count, 0); + }) + .then(function () { + return stoppable.normalProcess(); + }) + .then(function() { + return stoppable.count(); + }) + .then(function(count) { + assert.equal(count, 0); + }) + .then(done); + }); + + + it("can not take drastic measure in non-emergency", function(done) { + var stoppable; + return StoppableMock.new() + .then(function(_stoppable) { + stoppable = _stoppable; + return stoppable.drasticMeasure(); + }) + .then(function() { + return stoppable.drasticMeasureTaken(); + }) + .then(function(taken) { + assert.isFalse(taken); + }) + .then(done); + }); + + it("can take a drastic measure in an emergency", function(done) { + var stoppable; + return StoppableMock.new() + .then(function(_stoppable) { + stoppable = _stoppable; + return stoppable.emergencyStop(); + }) + .then(function() { + return stoppable.drasticMeasure(); + }) + .then(function() { + return stoppable.drasticMeasureTaken(); + }) + .then(function(taken) { + assert.isTrue(taken); + }) + .then(done); + }); + + it("should resume allowing normal process after emergency is over", function(done) { + var stoppable; + return StoppableMock.new() + .then(function(_stoppable) { + stoppable = _stoppable; + return stoppable.emergencyStop(); + }) + .then(function () { + return stoppable.release(); + }) + .then(function() { + return stoppable.normalProcess(); + }) + .then(function() { + return stoppable.count(); + }) + .then(function(count) { + assert.equal(count, 1); + }) + .then(done); + }); + +}); diff --git a/test/TestOwnable.sol b/test/TestOwnable.sol index 1a5433598..cb3cbcf21 100644 --- a/test/TestOwnable.sol +++ b/test/TestOwnable.sol @@ -1,4 +1,4 @@ -pragma solidity ^0.4.0; +pragma solidity ^0.4.4; import "truffle/Assert.sol"; import "truffle/DeployedAddresses.sol"; import "../contracts/Ownable.sol"; diff --git a/truffle.js b/truffle.js index 6e3970801..dc2d3eaf9 100644 --- a/truffle.js +++ b/truffle.js @@ -1,14 +1,4 @@ module.exports = { - build: { - "index.html": "index.html", - "app.js": [ - "javascripts/app.js" - ], - "app.css": [ - "stylesheets/app.css" - ], - "images/": "images/" - }, rpc: { host: "localhost", port: 8545