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 that, 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 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.js
module.exports.increaseTime = function increaseTime(duration) {
const id = Date.now();
return new Promise((resolve, reject) => {
web3.currentProvider.sendAsync(
{
jsonrpc: "2.0",
method: "evm_increaseTime",
params: [duration],
id: id
},
err1 => {
if (err1) return reject(err1);
web3.currentProvider.sendAsync(
{
jsonrpc: "2.0",
method: "evm_mine",
id: id + 1
},
(err2, res) => {
return err2 ? 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");
const DAY = 3600 * 24;
contract("Funding", accounts => {
[...]
let funding;
beforeEach(async () => {
funding = await Funding.new(DAY);
});
it("finishes fundraising when time is up", async () => {
assert.equal(await funding.isFinished.call(), false);
await increaseTime(DAY);
assert.equal(await funding.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.sol
pragma solidity ^0.4.17;
contract Funding {
uint public raised;
uint public finishesAt;
address public owner;
mapping(address => uint) public balances;
constructor(uint _duration) public {
owner = msg.sender;
finishesAt = now + _duration;
}
function isFinished() public view returns (bool) {
return finishesAt <= now;
}
function donate() public payable {
balances[msg.sender] += msg.value;
raised += msg.value;
}
}
Tests should be now passing.
FundingTest
✓ testSettingAnOwnerDuringCreation (64ms)
✓ testSettingAnOwnerOfDeployedContract (57ms)
✓ testAcceptingDonations (78ms)
✓ testTrackingDonorsBalance (54ms)
Contract: Funding
✓ sets an owner
✓ accepts donations (60ms)
✓ keeps track of donator balance (89ms)
✓ finishes fundraising 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.
function testDonatingAfterTimeIsUp() public {
Funding funding = new Funding(0);
bool result = funding.call.value(10 finney)(bytes4(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 () => {
await funding.donate({ from: firstAccount, value: 10 * FINNEY });
await increaseTime(DAY);
try {
await funding.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.
contract Funding {
[...]
modifier onlyNotFinished() {
require(!isFinished());
_;
}
function isFinished() public view returns (bool) {
return finishesAt <= now;
}
function donate() public onlyNotFinished payable {
balances[msg.sender] += msg.value;
raised += msg.value;
}
}
Both new tests should now pass.
FundingTest
✓ testSettingAnOwnerDuringCreation (72ms)
✓ testSettingAnOwnerOfDeployedContract (55ms)
✓ testAcceptingDonations (78ms)
✓ testTrackingDonorsBalance (56ms)
✓ testDonatingAfterTimeIsUp (46ms)
Contract: Funding
✓ sets an owner
✓ accepts donations (54ms)
✓ keeps track of donator balance (85ms)
✓ finishes fundraising 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.
contract FundingTest {
Funding funding;
function () public payable {}
function beforeEach() public {
funding = new Funding(1 days, 100 finney);
}
function testWithdrawalByAnOwner() public {
uint initBalance = this.balance;
funding.donate.value(50 finney)();
bool result = funding.call(bytes4(keccak256("withdraw()")));
Assert.equal(result, false, "Allows for withdrawal before reaching the goal");
funding.donate.value(50 finney)();
Assert.equal(this.balance, initBalance - 100 finney, "Balance before withdrawal doesn't correspond the sum of donations");
result = funding.call(bytes4(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");
}
function testWithdrawalByNotAnOwner() public {
// Make sure to check what goal is set in the migration (here also 100 Finney)
funding = Funding(DeployedAddresses.Funding());
funding.donate.value(100 finney)();
bool result = funding.call(bytes4(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.
Each transaction on Ethereum costs some amount of gas and represents resources required to change contract state. Due to how EVM works, by default, 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, 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 () => {
await funding.donate({ from: secondAccount, value: 30 * FINNEY });
await funding.donate({ from: thirdAccount, value: 70 * FINNEY });
const initBalance = web3.eth.getBalance(firstAccount);
assert.equal(web3.eth.getBalance(funding.address), 100 * FINNEY);
await funding.withdraw();
const finalBalance = 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 = await Funding.new(DAY, 100 * FINNEY, { from: secondAccount });
await funding.donate({ from: firstAccount, value: 100 * FINNEY });
try {
await funding.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.
contract Funding {
[...]
uint public goal;
modifier onlyOwner() {
require(owner == msg.sender);
_;
}
modifier onlyFunded() {
require(isFunded());
_;
}
function () public payable {}
constructor(uint _duration, uint _goal) public {
owner = msg.sender;
finishesAt = now + _duration;
goal = _goal;
}
function isFunded() public view returns (bool) {
return raised >= goal;
}
function withdraw() public onlyOwner onlyFunded {
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)
✓ testTrackingDonorsBalance (46ms)
✓ testDonatingAfterTimeIsUp (39ms)
✓ testWithdrawalByAnOwner (73ms)
✓ testWithdrawalByNotAnOwner (54ms)
Contract: Funding
✓ sets an owner
✓ accepts donations (53ms)
✓ keeps track of donator balance (87ms)
✓ finishes fundraising 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 donors 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 () => {
await funding.donate({ from: secondAccount, value: 50 * FINNEY });
const initBalance = web3.eth.getBalance(secondAccount);
assert.equal((await funding.balances.call(secondAccount)), 50 * FINNEY);
await increaseTime(DAY);
await funding.refund({ from: secondAccount });
const finalBalance = 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 () => {
await funding.donate({ from: secondAccount, value: 100 * FINNEY });
assert.equal((await funding.balances.call(secondAccount)), 100 * FINNEY);
await increaseTime(DAY);
try {
await funding.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 () => {
await funding.donate({ from: secondAccount, value: 50 * FINNEY });
assert.equal((await funding.balances.call(secondAccount)), 50 * FINNEY);
try {
await funding.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 donors and transfer them their funds. Problem with this solution is that the more donors 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.
contract Funding {
[...]
modifier onlyFinished() {
require(isFinished());
_;
}
modifier onlyNotFunded() {
require(!isFunded());
_;
}
modifier onlyFunded() {
require(isFunded());
_;
}
function refund() public onlyFinished onlyNotFunded {
uint amount = 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)
✓ testTrackingDonorsBalance (64ms)
✓ testDonatingAfterTimeIsUp (52ms)
✓ testWithdrawalByAnOwner (98ms)
✓ testWithdrawalByNotAnOwner (54ms)
Contract: Funding
✓ sets an owner
✓ accepts donations (60ms)
✓ keeps track of donator balance (91ms)
✓ finishes fundraising 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 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.
// contracts/Ownable.sol
pragma solidity ^0.4.17;
contract Ownable {
address public owner;
modifier onlyOwner() {
require(owner == msg.sender);
_;
}
function Ownable() 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";
contract Funding is Ownable {
constructor(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.
npm install --save-exact zeppelin-solidity
import "zeppelin-solidity/contracts/ownership/Ownable.sol";
contract Funding is Ownable {}
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 not 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: mul
, div
, sub
, and add
.
import "zeppelin-solidity/contracts/math/SafeMath.sol";
contract Funding is Ownable {
using SafeMath for uint;
function donate() public onlyNotFinished payable {
balances[msg.sender] = balances[msg.sender].add(msg.value);
raised = raised.add(msg.value);
}
}
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, it is just easier. 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 (which I intentionally skipped in this post), deployment to the publicly accessible network and 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).