Depending 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. 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 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. Solidity is a contract-oriented, high-level programming language. If you are new to Solidity, this might be a little challenging. On the other hand, if you are already familiar with creating smart contracts, I hope this post will help you get better at testing. I recommend you try CryptoZombies if you find yourself confused with Solidity code in this post.
Smart contracts are not a good fit for a “move fast and break things” kind of mindset. The blockchain is immutable in the sense that, once approved, a transaction is going to stay. Since smart contract deployment happens through a transaction, this results 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. Contracts upgradeability gives too much power in the hands of an owner of such a contract. This raises a question about the real decentralization of the app.
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 deploying contracts to a particular network. Despite all that, I am going with Truffle.
Truffle makes it possible to kick off the project using one of many boxes. A box is a boilerplate containing something more than just the necessary minimum. We are interested in starting a new project from scratch.
Look around; there is not much there yet. The only interesting bit is a
Migrations.sol contract and its migration file. The 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 the audibility of the smart contracts code. It is still in the experimental phase.
We are going to build a contract which allows for funds raising. The 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 the 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 with all transactions, it is a publicly visible Ether transfer. We are just unable to refund those funds.
With this 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.
Now, with an empty contract defined, we can create a testing contract.
Now, run tests.
Yay! 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.
Each smart contract has an address. An instance of each smart contract is implicitly convertible to its address and this balance returns the 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.
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!
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.
Apart from creating a new contract during tests, we would also like to access contracts deployed through a migration.
It fails as we do not have any migration for our Funding contract.
We can now rerun tests.
The next feature on the roadmap is accepting donations. Let’s start with a test in Solidity.
We use a unit called Finney. You should know that the smallest, indivisible unit of Ether is called Wei (it fits
- 1 Ether is 10¹⁸ Wei
- 1 Finney is 10¹⁵ Wei
- 1 Szabo is 10¹² Wei
- 1 Shannon is 10⁹ Wei
The implementation can follow.
For now, making tests pass is everything.
Now, we would like to keep track of who donated how much.
For tracking the balance of a 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.
By now, tests should pass.
Our donators can now donate, but there is no time constraint. We would like users to send us some Ether but only until fundraising is finished. We can get a current block timestamp by reading the
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 the current timestamp. This way, we could mock this contract in tests, injecting it as a dependency.
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.
After we have changed the timestamp of the Clock contract, the fundraising is finished. After you add a new contract, you have to remember to migrate it.
Now, to the implementation.
Tests should now pass.
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.
End of part one
This concludes the first part of the introduction to TDD in Solidity. In the next article, we take a step back with our approach in testing time-related smart contracts. Apart from this, you will also see how to test for errors. We will also complete the rest of the Funding smart contract by adding withdrawal and refund features.
You did an excellent job building a strong foundation for a smart contract. Grab a cup of coffee, do a few squats, and you are ready for the second part: Ethereum: Test-driven development with Solidity (part 2)!
On GitHub (MichalZalecki/tdd-solidity-intro) you can find the full source code of test suits and the smart contact.