Michal Zalecki

Mar 28, 2018 - 

5 min read  - 

Ethereum: Test-driven development with Solidity (part 2)

This is the second part of the test-driven introduction to Solidity. In this part, we use JavaScript to test time-related features of our smart contract. Apart from this, you will see how to check for errors. We will also complete the rest of the smart contract by adding withdrawal and refund features.

If you did not read the first part, I highly recommend doing so: Ethereum: Test-driven development with Solidity (part 1).

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 many 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.

Calling increaseTime results in two RPC calls. You will not find them on the Ethereum wiki page. Both evm_increaseTime and evm_mine are non-standard methods provided by Ganache – the blockchain for Ethereum development we use when running tests.

By now, this should be the entire Funding contract. This implementation is much more straightforward than the one we used before.

Tests should now be passing.

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. The new functions for handling state-reverting exceptions are require()assert() and revert(). You can read more about the differences between these calls here.

All exceptions bubble up, and there is no try...catch in Solidity. So, how can we test for throws using just Solidity? The Low-level call function returns false if an error occurred and true otherwise.

The first 4 bytes of the call data specify a function we want to call. We can get a function selector by taking the first 4 bytes of the Keccak hash of the function signature.

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.

We can now restrict the time for calling donate with a onlyNotFinished modifier.

Both new tests should now pass.

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 the contract duration.

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.

Each transaction on Ethereum costs some amount of gas and represents resources required to change contract states. Due to how EVM works, by default, the fallback function runs with a very little gas (2300). It is not enough to modify the state. 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, the 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 usea deployed contract, instead of using one created by a testing contract. Trying to withdraw in such a case should always fail. One restriction is that you cannot specify constructor params — the migration script has already deployed this contract.

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 suggest you go with a different assertion library than the standard one. Available assert.throws does not work well with async code.

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!

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 duration to 0, the contract is finished from the beginning, but then we cannot donate to have something to withdraw. We cannot move time forward unless we use the Clock contract again. I write tests for this case solely in JavaScript.

Implementing refund function can be tricky. Your intuition may tell you to loop through your donators and transfer them their funds. The problem with this solution is that the more donators you have, the more gas needs to be paid 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.

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.

Congratulations, you have implemented all the features and have decent test coverage!

Refactor with OpenZeppelin

There is a commonly used pattern in our code: saving an owner and restricting function call only to a deployer of the contract.

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.

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.

The ownable contract from OpenZeppelin is a little different than ours; it adds the possibility to transfer ownership.

The other thing we can also improve is to make sure that we safely perform our math operations. Since most math we do is just adding Wei, we should be fine and won’t worry about integer overflow. In general, to make sure that the state of our smart contract will be ok, we can use a SafeMath library which provides uint256 with four new methods: muldivsub, and add.

The tests are still passing if you got that right.

Conclusion

An important takeaway from this post is to think twice before deciding when 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, as it is just easier. JavaScript testing is also closer to how you are going to use your contracts from the client application. A 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 future posts, I am going to cover events (which I intentionally skipped in this post), deployment to the publicly accessible network and the creation of the client using Web3.js.

You can find the full source code of test suits and the smart contact on GitHub (MichalZalecki/tdd-solidity-intro).

Similar posts

Michal Zalecki

Aug 23, 2018 - 

6 min read  - 

Michal Zalecki

Aug 9, 2018 - 

5 min read  - 

Michal Zalecki

Jul 5, 2018 - 

4 min read  - 

Let’s work together

Tooploox Sp. z o.o.

hello@tooploox.com

EU: + 48 733 888 088

U.S.: + 1 415 800 2835

Business Partnerships

business@tooploox.com

Marketing & PR

marketing@tooploox.com

Recruitment

join@tooploox.com

Office Management

office@tooploox.com

Wrocław (HQ)

ul. Tęczowa 7
53-601 Wrocław
Poland
See on the map

Warsaw

ul. Foksal 18
00-372 Warszawa
Poland
See on the map

Gdańsk

ul. Rybaki Górne 4/1
80-861 Gdańsk
Poland
See on the map

We use cookies for analytics and to improve our site - more info in our privacy policy