Building 100+ Tests for HelloSugoi
Solidity code may look simple, but there are a lot of subtle quirks and rules that exist. In building out the ticketing platform for HelloSugoi, I had to build a test suite to stop malicious actors from calling functions or manipulating data. In the end, I have far more testing code then source code.
General Considerations:
One of the best resources for building out Solidity applications is the best practices guide by Consensys. Normal things like checking for integer overflows, making sure to check if address.send()
returns false, and controlling permissions to functions is very important.
Additionally, looking at OpenZepplin is a good move. They have a fair set of wonderful code with a bunch of tests. Plus you should definitely look over some of their audits, or any audits really. They provided suggestions on things to look out for.
Controlling contract function access:
Our platform provides levers for event owners to mange the events. They are able to modify the price and number of tickets available, and assign who the the promoters and artists are. With any multi party contract, there needs to be an access control layer. This just means we have to create functions that only work for specific users. We can use the concept of a “modifier” function that has a simple “if” conditional. Notice below there is a “modifier” function that checks if msg.sender
is equal to owner
. Below is a snippet of a contract called Ownable
.
//...
modifier onlyOwner() {
if (msg.sender != owner) {
throw;
}
_;
}modifier only(address _allowedAddress) {
if (msg.sender != _allowedAddress) {
throw;
}
_;
}
//...
The second function is similar to the first, except the owner
variable is dynamic. I simply added it to the already wonderful Zeppelin-Solidity repo maintained by OpenZepplin. A sample contract could look like:
contract Event is Ownable {
address eventOwner;
bool isOver = false; function Event() {
eventOwner = msg.sender
}
function setIsOver() only(eventOwner) {
isOver = true;
}
}
Given the code snippet above, how many different tests should there be? A sample set of tests would be like:
- Should successfully set isOver if caller is eventOwner
- Should fail to set isOver if caller is not eventOwner
That may seem basic, but we always have to test for failure cases. For complicated functions with multiple function modifiers, there can be many test cases.
SafeMath:
If you go through a few audits, you will notice many contracts fail to use SafeMath. So I’ll save you the trouble and tell you to simply use it.
The purpose of safe math is to protect against number overflows. An overflow is when the number space is used up, and everything starts back at 0. For example, my data type can only store 16 numbers. If I do the operation of 16 + 2, I would normally expect 18. But I can only store 16 numbers, therefore we have an overflow and the result is 2.
Complex Scenarios:
Here at HelloSugoi, we are creating an event ticketing platform. This means we have slightly more complex logic and we have to think through weird scenarios. We end up writing tests like:
- Should allow user x to call Function
- Should fail to allow user y to call Function
- Should fail to allow user z to call Function only if state is true
- Should allow user z to call function if state is false
Making tests is repetitive. I may have to call 5 functions to get into a specific state and then test both for success and failure. I end up with many tests that look the same, but only differ at the end because of who calls the function.
Another thing to consider is the interaction of moving ether around. Consider a scenario where two users purchase a ticket. You should check the contract has received the funds. Check the users have the correct amount of ether after calling the contracts. Please take the time to think through a bunch of scenarios. It may be annoying, but is a good practice.
Moving Ether:
When creating functions that move ether around, you must check it works properly. And not just that the function returned successfully. But that the expected change in ether for both parties.
Lets image an escrow contract that accepts ether on a function called fund
. When user A with 10 ether calls the fund
with 2 ether, we need to check the contract has 2 ether. We must also check the user has a little less than 8 ether because they paid the gas price. These values must be checked EVEN for the FAIL cases.
Async Await:
We use truffle as a framework, and they have done good work on providing a promise based API. This means that we can use Javascript’s async/await for free!
it('some test', async function() {
let beforeBalance = web3.eth.getBalance(mainAccount)
let Contract = await TicketManager.new()
let { logs } = await Contract.createEvent({ from: mainAccount })
let status = await Contract.SomeFunction({from: userB })
//...
})
The above snippet is much nicer then the snippet below:
it('some test', async function() {
let beforeBalance = web3.eth.getBalance(mainAccount)
let Contract = null
TicketManager.new()
.then(instance => {
Contract = instance
return Contract.createEvent({ from: mainAccount })
})
.then(data => {
return Contract.SomeFunction({ from: userB })
})
.then(status => {
//..
})
})
Async/await has made my life easy. I don’t have to write so much boilerplate. I write code that looks synchronous but is asynchronous. I highly recommend understanding how to use async/await! I have a more detailed blog post coming soon, so please subscribe! (و ˃̵ᴗ˂̵)و
Our Tests Roll out:
If you want to see how much testing a contract may need, just look below:
That is a fair amount of testing. (^_^;)
Tl;dr
- Test for the true and false cases for any function
- Test that a function uses a safe amount of gas under good and bad cases
- Check that ether has been properly moved between function calls
- Check ether does not move if a function fails to execute
- Always change/set state before sending ether
- Check Getter methods too!
Conclusion:
This is the amount of effort needed to safeguard a contract. Could there be more tests? Of course there could be. I may have missed a complex scenario that puts things in a weird state. But I wanted to demonstrate the amount of effort it takes to handle Solidity. If there is something you notice, please speak up! The test suite is not done, it can always grow.