Depends on how you count, second and third generation blockchain applications are not bound by restrictions of underlying protocols. Programmers can create smart contracts, distributed applications with access to code-controlled accounts - opposed to a private key. Use cases go far beyond exchanging value and applies where users benefit from replacing trust between parties with code.
Ethereum Blockchain is a decentralized platform which provides us with a runtime environment for smart contracts called Ethereum Virtual Machine (EVM). Contracts are completely isolated with limited access to other smart contracts. If you are new in this space, for now, you can think about Ethereum as a slow but reliable and secure computer.
In this post, I would like to introduce you to Solidity through TDD approach. If you are new to Solidity, this might be a little challenge. I will do my best and try to make a gentle learning curve. On the other hand, if you are already familiar with creating smart contracts, I hope this post will help you to get better at testing. I recommend you try CryptoZombies if you find yourself confused with Solidity code in this post.
There are a few reasons why I find testing smart contracts very important and interesting. You can choose Solidity or JavaScript as a language for writing tests. You can also write in both! Depends on what you would like to test, one testing environment is superior to another.
Smart contracts are not a good fit for “move fast and break things” kind of mindset. The blockchain is immutable in the sense that a once approved transaction is going to stay. Since smart contract deployment happens through a transaction, this originates in the inability to fix issues quickly. That is why having a reliable set of tests is so crucial. There are of course different techniques which allow you to introduce escape hatches and migrate to a new, improved version of the smart contract. It comes with a set of potential security vulnerabilities and too often gives a little bit too much power in the hands of an owner of the contract. This raises a question about real decentralization of the app.
Setup
The easiest way to start with smart contracts development for Ethereum is through Remix, an online IDE. It does not require any setup and integrates nicely with MetaMask to allow you for deploying contracts to a particular network. Despite all that I am going with Truffle.
Truffle is a popular development framework written in JavaScript. It comes neither with any convention for your smart contracts nor utility library but provides you with a local environment for developing, testing and deploying contracts. For starters, install truffle locally.
Truffle makes it possible to kick off the project using one of many boxes. A box is a boilerplate containing something more than just a necessary minimum. We are interested in starting a new project from scratch.
./node_modules/.bin/truffle init
Look around; there is not much there yet. The only interesting bit is a Migrations.sol
contract and its migration file. History of migrations you are going to make over time is recorded on-chain through a Migrations contract.
Solidity is not the only language in which one can create a smart contract on EVM. Solidity compiles directly to EVM bytecode. There’s also LLL (low level, 1 step above EVM bytecode) and Serpent (LLL’s super-set) which I would not recommend due to known security issues. Another language is Vyper which aims to provide better security through simplicity and to increase audibility of the smart contracts code. It is still in experimental phase.
Rules
We are going to build a contract which allows for funds raising. Mechanics are the same as a single Kickstarter campaign. There is a particular time to reach the goal. If this does not happen, donators are free to request a refund of transferred Ether. If a goal is reached, the owner of the smart contact can withdraw funds. We want to also allow for “anonymous” donation which is merely a transfer of funds to a smart contract. I am saying anonymous, but as all transactions, it is a publicly visible Ether transfer. We are just unable to refund those funds.
With clearly defined scope we can start implementing our smart contract.
Setting an owner
The first feature we want our smart contract to have is an owner. Before we start writing the first test let’s create an empty Funding contract so our tests can compile.
// contracts/Funding.solpragmasolidity^0.4.17;contractFunding{}
Now, with an empty contract defined we can create a testing contract.
// contracts/FundingTest.solpragmasolidity^0.4.17;import"truffle/Assert.sol";import"../contracts/Funding.sol";contractFundingTest{}
Now, run tests.
$ ./node_modules/.bin/truffle test
Compiling ./contracts/Funding.sol...
Compiling ./contracts/Migrations.sol...
Compiling ./test/FundingTest.sol...
Compiling truffle/Assert.sol...
0 passing (0ms)
Yey! If you got it right, contracts should compile without any errors. But we still don’t have any tests; we need to fix that. We want Funding to store an address of its deployer as an owner.
contractFundingTest{functiontestSettingAnOwnerDuringCreation()public{Fundingfunding=newFunding();Assert.equal(funding.owner(),this,"An owner is different than a deployer");}}
Each smart contract has an address. An instance of each smart contract is implicitly convertible to its address and this.balance returns contract’s balance. One smart contract can instantiate another, so we expect that owner of funding
is still the same contract. Now, to the implementation.
contractFunding{addresspublicowner;functionFunding()public{owner=msg.sender;}}
Like in C#, a constructor of the contract has to have the same name as a class (contract in this case). A sender of the message inside a constructor is a deployer. Let’s rerun the tests!
FundingTest
✓ testSettingAnOwnerDuringCreation (64ms)
1 passing (408ms)
We can create an equivalent test in JavaScript.
// test/FundingTest.jsconstFunding=artifacts.require("Funding");contract("Funding",accounts=>{const[firstAccount]=accounts;it("sets an owner",async()=>{constfunding=awaitFunding.new();assert.equal(awaitfunding.owner.call(),firstAccount);});});
In JavaScript, we can require a contract using artifacts.require
. Instead of describe
which you may know from other testing frameworks we use contract
which does some cleanup and provides a list of available accounts. The first account is used by default during tests.
FundingTest
✓ testSettingAnOwnerDuringCreation (66ms)
Contract: Funding
✓ sets an owner (68ms)
2 passing (551ms)
Apart from creating a new contract during tests, we would also like to access contracts deployed through a migration.
import"truffle/DeployedAddresses.sol";contractFundingTest{functiontestSettingAnOwnerOfDeployedContract()public{Fundingfunding=Funding(DeployedAddresses.Funding());Assert.equal(funding.owner(),msg.sender,"An owner is different than a deployer");}}
It fails as we do not have any migration for our Funding contract.
// migrations/2_funding.jsconstFunding=artifacts.require("./Funding.sol");module.exports=function(deployer){deployer.deploy(Funding);};
We can now rerun tests.
FundingTest
✓ testSettingAnOwnerDuringCreation (70ms)
✓ testSettingAnOwnerOfDeployedContract (63ms)
Contract: Funding
✓ sets an owner (62ms)
3 passing (744ms)
Accepting donations
Next feature on the roadmap is accepting donations. Let’s start with a test in Solidity.
contractFundingTest{uintpublicinitialBalance=10ether;functiontestAcceptingDonations()public{Fundingfunding=newFunding();Assert.equal(funding.raised(),0,"Initial raised amount is different than 0");funding.donate.value(10finney)();funding.donate.value(20finney)();Assert.equal(funding.raised(),30finney,"Raised amount is different than sum of donations");}}
We use a unit called Finney. You should know that the smallest, indivisible unit of Ether is called Wei (it fits uint
type).
- 1 Ether is 10^18 Wei
- 1 Finney is 10^15 Wei
- 1 Szabo is 10^12 Wei
- 1 Shannon is 10^9 Wei
Initially, a contract has no spare ethers to transfer so we can set an initial balance. Ten ether is more than enough. Let’s write an equivalent JavaScript test.
constFINNEY=10**15;contract("Funding",accounts=>{const[firstAccount,secondAccount]=accounts;it("accepts donations",async()=>{constfunding=awaitFunding.new();awaitfunding.donate({from:firstAccount,value:10*FINNEY});awaitfunding.donate({from:secondAccount,value:20*FINNEY});assert.equal(awaitfunding.raised.call(),30*FINNEY);});});
Implementation can be following.
contractFunding{uintpublicraised;addresspublicowner;functionFunding()public{owner=msg.sender;}functiondonate()publicpayable{raised+=msg.value;}}
For now, it is everything to make tests pass.
FundingTest
✓ testSettingAnOwnerDuringCreation (68ms)
✓ testSettingAnOwnerOfDeployedContract (63ms)
✓ testAcceptingDonations (80ms)
Contract: Funding
✓ sets an owner (56ms)
✓ accepts donations (96ms)
5 passing (923ms)
Now, we would like to keep track of who donated how much.
functiontestTrackingDonatorsBalance()public{Fundingfunding=newFunding();funding.donate.value(5finney)();funding.donate.value(15finney)();Assert.equal(funding.balances(this),20finney,"Donator balance is different than sum of donations");}
Testing with JavaScript gives us an ability to test for multiple different accounts.
it("keeps track of donator balance",async()=>{constfunding=awaitFunding.new();awaitfunding.donate({from:firstAccount,value:5*FINNEY});awaitfunding.donate({from:secondAccount,value:15*FINNEY});awaitfunding.donate({from:secondAccount,value:3*FINNEY});assert.equal(awaitfunding.balances.call(firstAccount),5*FINNEY);assert.equal(awaitfunding.balances.call(secondAccount),18*FINNEY);});
For tracking a balance of particular user, we can use mapping. We have to mark a function as payable
, so it allows users to send ethers along with function calls.
contractFunding{uintpublicraised;addresspublicowner;mapping(address=>uint)publicbalances;functionFunding()public{owner=msg.sender;}functiondonate()publicpayable{balances[msg.sender]+=msg.value;raised+=msg.value;}}
By now, tests should pass.
FundingTest
✓ testSettingAnOwnerDuringCreation (61ms)
✓ testSettingAnOwnerOfDeployedContract (65ms)
✓ testAcceptingDonations (97ms)
✓ testTrackingDonatorsBalance (61ms)
Contract: Funding
✓ sets an owner (51ms)
✓ accepts donations (96ms)
✓ keeps track of donator balance (134ms)
7 passing (1s)
Time constraint
Our donators can now donate, but there is no time constraint. We would like users to send us some Ether but only until funds rising is finished. We can get a current block timestamp by reading now
property. If you start writing tests in Solidity, you will quickly realize that there is no easy way to manipulate block time from the testing smart contract. There is also no sleep
method which would allow us to set a tiny duration, wait for a second or two and try again simulating that time for donating is up.
The other solution would be to make it possible to set an address of the contract from which we read current timestamp. This way we could mock this contract in tests injecting it as a dependency.
// contracts/Clock.solpragmasolidity^0.4.17;contractClock{uintprivatetimestamp;functiongetNow()publicviewreturns(uint){if(timestamp>0){returntimestamp;}returnnow;}functionsetNow(uint_timestamp)publicreturns(uint){timestamp=_timestamp;}}
This is how we can implement a Clock contract. We would need to restrict changing the timestamp to the owner, but it is not that important right now. It is enough to make tests green.
functiontestFinishingFundRising()public{Clockclock=Clock(DeployedAddresses.Clock());Fundingfunding=newFunding(1days,address(clock));Assert.equal(funding.isFinished(),false,"Is finished before time is up");clock.setNow(now+1days);Assert.equal(funding.isFinished(),true,"Is not finished before time is up");}
After we have changed the timestamp of the Clock contract, fundraising is finished. After you add a new contract, you have to remember to migrate it.
// migrations/2_funding.jsconstFunding=artifacts.require("./Funding.sol");constClock=artifacts.require("./Clock.sol");constDAY=3600*24;module.exports=asyncfunction(deployer){awaitdeployer.deploy(Clock);awaitdeployer.deploy(Funding,DAY,Clock.address);};
Now to the implementation.
import"./Clock.sol";contractFunding{[...]uintpublicfinishesAt;Clockclock;functionFunding(uint_duration,address_clockAddress)public{owner=msg.sender;clock=Clock(_clockAddress);finishesAt=clock.getNow()+_duration;}functionisFinished()publicviewreturns(bool){returnfinishesAt<=clock.getNow();}[...]}
Tests should now pass.
FundingTest
✓ testSettingAnOwnerDuringCreation (86ms)
✓ testAcceptingDonations (112ms)
✓ testTrackingDonatorsBalance (64ms)
✓ testFinishingFundRising (58ms)
Contract: Funding
✓ sets an owner (64ms)
✓ accepts donations (115ms)
✓ keeps track of donator balance (165ms)
7 passing (1s)
Although tests are passing, I would stop here for a moment. We had to create a separate contract, acting as a dependency, just to be able to test the implementation. I was proud of myself but taking into consideration that we have just added another attack vector I think this solution is somewhat dumb rather than smart. Let’s take a step back.
JSON-RPC for the rescue
I have already mentioned that there is no easy way to manipulate block time from Solidity (at least at the time of writing). JSON-RPC is a stateless, remote procedure call protocol. Ethereum provides multiple methods which we can remotely execute. One of the use cases for it is creating Oracles. We are not going to use JSON-RPC directly but through web3.js which provides a convenient abstraction for RPC calls.
// source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/increaseTime.jsmodule.exports.increaseTime=functionincreaseTime(duration){constid=Date.now();returnnewPromise((resolve,reject)=>{web3.currentProvider.sendAsync({jsonrpc:"2.0",method:"evm_increaseTime",params:[duration],id:id},err1=>{if(err1)returnreject(err1);web3.currentProvider.sendAsync({jsonrpc:"2.0",method:"evm_mine",id:id+1},(err2,res)=>{returnerr2?reject(err2):resolve(res);});});});};
Calling increaseTime
results in two RPC calls. You will not find them on Ethereum wiki page. Both evm_increaseTime
and evm_mine
are non-standard methods provided by Ganache - blockchain for Ethereum development we use when running tests.
const{increaseTime}=require("./utils");constDAY=3600*24;contract("Funding",accounts=>{[...]letfunding;beforeEach(async()=>{funding=awaitFunding.new(DAY);});it("finishes fund raising when time is up",async()=>{assert.equal(awaitfunding.isFinished.call(),false);awaitincreaseTime(DAY);assert.equal(awaitfunding.isFinished.call(),true);});});
By now, this should be the entire Funding contract. This implementation is much more straightforward than the one we used before.
// contracts/Funding.solpragmasolidity^0.4.17;contractFunding{uintpublicraised;uintpublicfinishesAt;addresspublicowner;mapping(address=>uint)publicbalances;functionFunding(uint_duration)public{owner=msg.sender;finishesAt=now+_duration;}functionisFinished()publicviewreturns(bool){returnfinishesAt<=now;}functiondonate()publicpayable{balances[msg.sender]+=msg.value;raised+=msg.value;}}
Tests should be now passing.
FundingTest
✓ testSettingAnOwnerDuringCreation (64ms)
✓ testSettingAnOwnerOfDeployedContract (57ms)
✓ testAcceptingDonations (78ms)
✓ testTrackingDonatorsBalance (54ms)
Contract: Funding
✓ sets an owner
✓ accepts donations (60ms)
✓ keeps track of donator balance (89ms)
✓ finishes fund raising when time is up (38ms)
8 passing (1s)
Modifiers and testing throws
We can now tell whether fundraising finished, but we are not doing anything with this information. Let’s put a limitation on how long people can donate.
Since Solidity 0.4.13, a throw
is deprecated. New function for handling state-reverting exceptions are require()
, assert()
and revert()
. You can read more about differences between those calls here.
All exceptions bubble up, and there is no try...catch
in Solidity. So how to test for throws using just Solidity? Low-level call
function returns false if an error occurred and true otherwise. You can also use a proxy contract to achieve the same in what you may consider as more elegant way although I prefer one-liners.
functiontestDonatingAfterTimeIsUp()public{Fundingfunding=newFunding(0);boolresult=funding.call.value(10finney)(bytes4(bytes32(keccak256("donate()"))));Assert.equal(result,false,"Allows for donations when time is up");}
I am cheating here a little bit because a contract has a duration set to 0 which makes it out-of-date from the get-go. In JavaScript we can just use try...catch
to handle an error.
it("does not allow for donations when time is up",async()=>{awaitfunding.donate({from:firstAccount,value:10*FINNEY});awaitincreaseTime(DAY);try{awaitfunding.donate({from:firstAccount,value:10*FINNEY});assert.fail();}catch(err){assert.ok(/revert/.test(err.message));}});
We can now restrict time for calling donate
with onlyNotFinished
modifier.
contractFunding{[...]modifieronlyNotFinished(){require(!isFinished());_;}functionisFinished()publicviewreturns(bool){returnfinishesAt<=now;}functiondonate()publiconlyNotFinishedpayable{balances[msg.sender]+=msg.value;raised+=msg.value;}}
Both new tests should now pass.
FundingTest
✓ testSettingAnOwnerDuringCreation (72ms)
✓ testSettingAnOwnerOfDeployedContract (55ms)
✓ testAcceptingDonations (78ms)
✓ testTrackingDonatorsBalance (56ms)
✓ testDonatingAfterTimeIsUp (46ms)
Contract: Funding
✓ sets an owner
✓ accepts donations (54ms)
✓ keeps track of donator balance (85ms)
✓ finishes fund raising when time is up
✓ does not allow for donations when time is up (52ms)
10 passing (1s)
Withdrawal
We accept donations, but it is not yet possible to withdraw any funds. An owner should be able to do it only when the goal has been reached. We also cannot set a goal. We would like to do it when deploying a contract - as we did when we were setting contract duration.
contractFundingTest{Fundingfunding;function()publicpayable{}functionbeforeEach()public{funding=newFunding(1days,100finney);}functiontestWithdrawalByAnOwner()public{uintinitBalance=this.balance;funding.donate.value(50finney)();boolresult=funding.call(bytes4(bytes32(keccak256("withdraw()"))));Assert.equal(result,false,"Allows for withdrawal before reaching the goal");funding.donate.value(50finney)();Assert.equal(this.balance,initBalance-100finney,"Balance before withdrawal doesn't correspond the sum of donations");result=funding.call(bytes4(bytes32(keccak256("withdraw()"))));Assert.equal(result,true,"Doesn't allow for withdrawal after reaching the goal");Assert.equal(this.balance,initBalance,"Balance after withdrawal doesn't correspond the sum of donations");}functiontestWithdrawalByNotAnOwner()public{// Make sure to check what goal is set in the migration (here also 100 Finney)funding=Funding(DeployedAddresses.Funding());funding.donate.value(100finney)();boolresult=funding.call(bytes4(bytes32(keccak256("withdraw()"))));Assert.equal(result,false,"Allows for withdrawal by not an owner");}}
A lot is going on here. First of all, this empty function marked as payable allows contracts to accept Ether via standard transaction (without data) like it would be an ordinary account controlled by a public key. This unnamed function is called a fallback function. It neither can have any arguments nor return a value. There is such small amount of gas to use (2300) that it would be impossible to modify a state anyway. We have to implement this function to test withdrawing funds to the testing contract.
Truffle will also call beforeEach
hook before every test so we can move creating a new contract there as we are doing it in JavaScript. In a test case, we can overwrite a variable pointing to the funding contract. It requires different constructor params or referring to an already deployed contract.
From Solidity, we are not able to select an address from which we want to make a transaction. By design address of the smart contract is going to be used. What we can do to test withdrawal from an account which is not an owner is to use deployed contract instead of using created by a testing contract. Trying to withdraw in such case should always fail. One restriction is that you cannot specify a constructor params - the migration script has already deployed this contract.
it("allows an owner to withdraw funds when goal is reached",async()=>{awaitfunding.donate({from:secondAccount,value:30*FINNEY});awaitfunding.donate({from:thirdAccount,value:70*FINNEY});constinitBalance=web3.eth.getBalance(firstAccount);assert.equal(web3.eth.getBalance(funding.address),100*FINNEY);awaitfunding.withdraw();constfinalBalance=web3.eth.getBalance(firstAccount);assert.ok(finalBalance.greaterThan(initBalance));// hard to be exact due to the gas usage});it("does not allow non-owners to withdraw funds",async()=>{funding=awaitFunding.new(DAY,100*FINNEY,{from:secondAccount});awaitfunding.donate({from:firstAccount,value:100*FINNEY});try{awaitfunding.withdraw();assert.fail();}catch(err){assert.ok(/revert/.test(err.message));}});
No surprise on the JavaScipt side and that is a good thing. Access to multiple accounts makes it less hacky than a Solidity test case. You would like to probably get rid of this nasty try catch and a regex. I would suggest you would go with a different assertion library than the standard one. Available assert.throws
does not work well with async code.
contractFunding{[...]uintpublicgoal;modifieronlyOwner(){require(owner==msg.sender);_;}modifieronlyFunded(){require(isFunded());_;}function()publicpayable{}functionFunding(uint_duration,uint_goal)public{owner=msg.sender;finishesAt=now+_duration;goal=_goal;}functionisFunded()publicviewreturns(bool){returnraised>=goal;}functionwithdraw()publiconlyOwneronlyFunded{owner.transfer(this.balance);}}
We already store the owner of the contract. Restricting access to particular functions using an onlyOwner
modifier is a popular convention. Popular enough to export it to a reusable piece of code but we will cover this later. The rest of the code should not come as a surprise, you have seen it all!
FundingTest
✓ testSettingAnOwnerDuringCreation (54ms)
✓ testSettingAnOwnerOfDeployedContract (58ms)
✓ testAcceptingDonations (67ms)
✓ testTrackingDonatorsBalance (46ms)
✓ testDonatingAfterTimeIsUp (39ms)
✓ testWithdrawalByAnOwner (73ms)
✓ testWithdrawalByNotAnOwner (54ms)
Contract: Funding
✓ sets an owner
✓ accepts donations (53ms)
✓ keeps track of donator balance (87ms)
✓ finishes fund raising when time is up
✓ does not allow for donations when time is up (74ms)
✓ allows an owner to withdraw funds when goal is reached (363ms)
✓ does not allow non-owners to withdraw funds (81ms)
14 passing (2s)
Refund
Currently, funds are stuck, and donators are unable to retrieve their Ether when a goal is not achieved within a specified time. We need to make sure it is possible. Two conditions have to be met so users can get their Ether back. Duration is set in a construct so if we set a 0 duration contract is finished from the beginning, but then we cannot donate to have something to withdraw. We cannot move time forward unless we use Clock
contract again. I write tests for this case solely in JavaScript.
it("allows to withdraw funds after time is up and goal is not reached",async()=>{awaitfunding.donate({from:secondAccount,value:50*FINNEY});constinitBalance=web3.eth.getBalance(secondAccount);assert.equal((awaitfunding.balances.call(secondAccount)),50*FINNEY);awaitincreaseTime(DAY);awaitfunding.refund({from:secondAccount});constfinalBalance=web3.eth.getBalance(secondAccount);assert.ok(finalBalance.greaterThan(initBalance));// hard to be exact due to the gas usage});it("does not allow to withdraw funds after time in up and goal is reached",async()=>{awaitfunding.donate({from:secondAccount,value:100*FINNEY});assert.equal((awaitfunding.balances.call(secondAccount)),100*FINNEY);awaitincreaseTime(DAY);try{awaitfunding.refund({from:secondAccount});assert.fail();}catch(err){assert.ok(/revert/.test(err.message));}});it("does not allow to withdraw funds before time in up and goal is not reached",async()=>{awaitfunding.donate({from:secondAccount,value:50*FINNEY});assert.equal((awaitfunding.balances.call(secondAccount)),50*FINNEY);try{awaitfunding.refund({from:secondAccount});assert.fail();}catch(err){assert.ok(/revert/.test(err.message));}});
Implementing refund function can be tricky. Your intuition may tell you to loop through your donators and transfer them their funds. Problem with this solution is that the more donators you have the more gas to pay and it is not only looping but also making multiple transactions. You would like to keep the cost of running a function low and predictable. Let’s just allow each user to withdraw their donation.
contractFunding{[...]modifieronlyFinished(){require(isFinished());_;}modifieronlyNotFunded(){require(!isFunded());_;}modifieronlyFunded(){require(isFunded());_;}functionrefund()publiconlyFinishedonlyNotFunded{uintamount=balances[msg.sender];require(amount>0);balances[msg.sender]=0;msg.sender.transfer(amount);}}
We would like to save the amount to transfer first and then zero the balance. It is an implementation of the withdrawal pattern. Transfering an amount straight from the balances mapping introduces a security risk of re-entrancy - calling back multiple refunds.
FundingTest
✓ testSettingAnOwnerDuringCreation (64ms)
✓ testSettingAnOwnerOfDeployedContract (92ms)
✓ testAcceptingDonations (107ms)
✓ testTrackingDonatorsBalance (64ms)
✓ testDonatingAfterTimeIsUp (52ms)
✓ testWithdrawalByAnOwner (98ms)
✓ testWithdrawalByNotAnOwner (54ms)
Contract: Funding
✓ sets an owner
✓ accepts donations (60ms)
✓ keeps track of donator balance (91ms)
✓ finishes fund raising when time is up
✓ does not allow for donations when time is up (77ms)
✓ allows an owner to withdraw funds when goal is reached (425ms)
✓ does not allow non-owners to withdraw funds (38ms)
✓ allows to withdraw funds after time is up and goal is not reached (380ms)
✓ does not allow to withdraw funds after time in up and goal is reached (66ms)
✓ does not allow to withdraw funds before time in up and goal is not reached (45ms)
17 passing (3s)
Congratulations, you have all features implemented and decent test coverage 👏
Refactor
There is a commonly used pattern in our code. Saving an owner and restricting function call only to a deployer of the contract.
// contracts/Ownable.solpragmasolidity^0.4.17;contractOwnable{addresspublicowner;modifieronlyOwner(){require(owner==msg.sender);_;}functionOwnable()public{owner=msg.sender;}}
In Solidity, we can reuse existing code using libraries or through extending other contracts. Libraries are separately deployed, called using a DELEGATECALL
and a good fit for implementing custom data structure like a linked list. Behaviour can be easily shared using an inheritance.
import"./Ownable.sol";contractFundingisOwnable{functionFunding(uint_duration,uint_goal)public{finishesAt=now+_duration;goal=_goal;}}
OpenZeppelin is a library which provides multiple contracts and seamlessly integrates with Truffle. You can explore them and reuse well-tested code in your smart contracts. Ownable contract from OpenZeppelin is a little different than ours; it adds a possibility to transfer ownership.
npm install --save-exact zeppelin-solidity
import"zeppelin-solidity/contracts/ownership/Ownable.sol";contractFundingisOwnable{}
Tests are still passing if you got that right.
Conclusion
An important takeaway from this post is to think twice to decide well in which cases you want to test smart contracts using JavaScript and when using Solidity. The rule of thumb is that smart contracts interacting with each other should be tested using Solidity. The rest can be tested using JavaScript. JavaScript testing is also closer to how you are going to use your contracts from the client application. Well written test suit can be a useful resource on how to interact with your smart contracts.
Stay tuned for more Solidity related content. In the future post I am going to cover events (I intentionaly skipped in this post), deploy to the publicly accessible network and create a frontend using web3.
You can find the full source code of test suits and the smart contact on GitHub (MichalZalecki/tdd-solidity-intro).