loading...
Cover image for How to test Ethereum smart contracts: audit best practices

How to test Ethereum smart contracts: audit best practices

smartym profile image Sergey V. ・7 min read

When developing an Ethereum blockchain-based project, the first thing to do is to ensure smart contract security and code quality. Otherwise, you may lose a huge amount of money.

As you might have heard, the Parity Ethereum wallet lost $30 million because of having code vulnerabilities. For this reason, Satoshi Pie fond lost 32,4% of its assets. In 2016, the attacker hacked the DAO and took away $50 million.

These examples clearly show that even a slight defect or an error can result in bad consequences. Therefore, smart contract testing is a crucial part of creating smart contracts.

The purpose of their audit is to find code errors, check the program logic, and ensure everything is okay. A high-quality audit will enable you to remove risks, anxiety, as well as save your nerves and resources.

How to prepare for testing smart contracts

The preparation has an important role, helping auditors define the project objectives, faster define vulnerabilities, and improve the testing process.

Also, keep in mind that preparation begins from the moment you start creating smart contracts. That's why it's important to know smart contract vulnerabilitites and ways to protect from them.

I recommend adhering to the following tips:

1. Provide a detailed specification

Providing a clear technical specification is always a good idea, as it helps blockchain developers clearly understand smart contract goals, use cases, and working principles.

Although having it is important, many companies don’t create it. However, most engineers say that the document enables to clarify thinking and increase chances for project success.

In the spec, describe the intended behavior of a smart contract: how each function should work, how it shouldn’t, and how all of them should work together. Use diagrams as they also allow you to see possible alternatives and see how people can misinterpret your code.

2. Document the process of deploying smart contracts

Though the code of your smart contract can be great, there are some important steps to make before it’s deployed. To avoid mishaps and save the team’s efforts, provide the documentation of the deployment process.

A well-documented process that includes the order in which contracts will be deployed, which compiler versions will be used for which contracts, etc., will help you prevent smart contract vulnerabilities.

3. Clean the project repository

A quality smart contract code without clutter will facilitate and automate the work of auditors. When preparing the project for the testing process, remove any unused and unnecessary files, contracts, or pieces of code.

In addition, you should specify which contracts are utilized for audit only (meaning that they aren't part of the end system) and which of them are reused from the tested code or inherit from the validated one.

How to test Ethereum smart contracts

The process of smart contract audit is similar to testing of any other code: a set of standard method calls is created in the predefined environment, and statements are written for their results.

The audit is a process that consists of:

  • Test development
  • Testing of smart contract state changes
  • Event testing
  • Error testing
  • Checking a message sender

To audit your project, it's convenient to use Behavior Driven Development (BDD) practices, that along with the tests enable you to create documentation and use cases. Now there is a wide range of frameworks, libraries, and other tools for auditing smart contracts.

Although the test development in Solidity is limited to the capabilities of this programming language, you can use JavaScript, Truffle framework, Parity, and other reliable technologies.

Auditing smart contracts with Truffle

Today Truffle is the most popular framework for Ethereum. Truffle is a Node.js framework used to compile, link, and deploy smart contracts. The framework is written in a completely modular way enabling engineers to choose the functionality they need.

While in Truffle v.2 tests are created on JavaScript, in Truffle v.3 developers added the ability to write tests on Solidity, whose syntax is similar to that of JavaScript.

Truffle offers plenty of useful features that include binary management, library linking, custom deployment support, migrations framework, scriptable deployment, access to hundreds of external packages, external script runner, and automated contract audit with Mocha and Chai – everything to avoid failures is smart contracts.

In speaking of the fact that blockchain systems generally don’t work very fast, auditors use blockchain test clients, for instance, TestRPC that almost completely emulates the work of the JSON RPC API of Ethereum clients.

Besides standard methods, TestRPC also implements a number of additional ones, such as evm_increaseTime and evm_mine. A good alternative to applying TestRPC is to use one of the standard clients, for instance, Parity, that runs in dev mode and has transactions instantly confirmed.

To start working with Truffle framework install it via npm:

npm install -g truffle

Then, perform the truffle init command in order to create the project structure:

$ mkdir solidity-test-example
$ cd solidity-test-example/
$ truffle init

Contracts must be located in the contracts/ directory and tests – in the test/ directory. When you compile contracts, Truffle expects that each contract is placed in a separate file, and the contract name is equal to the file name.

Test development with Truffle framework

Tests use JavaScript objects that represent abstractions for working with contracts, making the mapping between operations on objects and calls of JSON RPC methods of the Ethereum client. These objects are created automatically when you compile the source code for * .sol files.

The calls of all methods are asynchronous and return Promise, thus removing the worry about tracking transaction confirmations. To avoid large amounts of unreadable code, use async/await for writing tests.

Also, to simplify the work, it’s better not to mix migrations and the creation of test instances. Instead, create them in the test by using the code that creates a new contract instance before calling each test function. To this end, you can use the beforeEach async function.

const congressInitialParams = {
minimumQuorumForProposals: 3,
minutesForDebate: 5,
marginOfVotesForMajority: 1,
congressLeader: accounts[0]
};
let congress;
beforeEach(async function() {
congress = await Congress.new(…Object.values(congressInitialParams));
});

How to test events

Events in Ethereum can be used for the following purposes:

  • To return values from methods that change the contract state
  • To create a history of changing the state of smart contracts
  • To be used as a cheap and convenient information repository

Let’s take the following example. You need to check that when you call the new proposal method, adding a sentence to the contract, an event record Proposal Added is created.

For that, first, create a member of DAO in the test and create a proposal on its behalf. Then create a subscriber for the ProposalAdded event and check that after the newProposal method was called, the event occurred and its attributes correspond to the transmitted data.

A newProposalfunction code:
/* Function to create a new proposal */
function newProposal(
address beneficiary,
uint etherAmount,
string JobDescription,
bytes transactionBytecode

)
onlyMembers
returns (uint proposalID)

How to test changes of the smart contract state

In Ethereum, the addMember method is responsible for changing the state of smart contracts. To test this method, check if it records the information about the DAO participant to the array of members' structures.

/make member/
function addMember(address targetMember, string memberName) onlyOwner {
uint id;

if (memberId[targetMember] == 0) {
memberId[targetMember] = members.length;
id = members.length++;

members[id] = Member({member: targetMember, memberSince: now, name: memberName});
} else {
id = memberId[targetMember];
Member m = members[id];
}

MembershipChanged(targetMember, true);
}
function removeMember(address targetMember) onlyOwner {
if (memberId[targetMember] == 0) throw;
for (uint i = memberId[targetMember]; i
members[i] = members[i+1];
}
delete members[members.length-1];
Members.length–;
}

Using the array with test accounts, add participants in the contract in the test. Then check that the member's function returns the input data.

It should be noted that every time the addMembermethod is called, a transaction is created and the Blockchain state is changed, that is the information is recorded in the distributed ledger.

How to test errors and checking of the message sender

A general way of stopping the work of the contract method is the exceptions that can be created using the throw instruction. You may need an exception if your task is to restrict access to the method.

To make it implement a modifier that would check an account address which has called a method: if it doesn’t correspond to the conditions (it’s not the contract owner who calls the method), an exception is created.

Some tips to avoid smart contract mistakes

1. Use Solidity test coverage

For smart contract audit, use Solidity-coverage. It will help you measure test coverage and define code pieces that haven’t been tested yet or need more attention. Surely, 100% test coverage isn’t a panacea, so it’s also important to write a quality test code from the beginning.

2. Use linters

Linters enable Ethereum developers to enhance the code quality by making the code easier to read and review. There are many linters you can use. For example, Solcheck is a JavaScript linter for Solidity code.

Solhint is a linter that helps engineers avoid errors and vulnerabilities in Solidity smart contracts. Solhint provides Security and Style Guide validations.

3. Make static analysis

In smart contract testing, static analysis allows finding code vulnerabilities and insecurities.

To make the code analysis, you can use such tools as Oyente that analyzes the code and detects common vulnerabilities, Manticore, that allows a dynamic binary analysis with the EVM support, and Solgraph that enables to visualize smart contract function control flow and identify potential insecurities.

4. Prepare for failure

Despite accurate testing, a smart contract may still have some errors or vulnerabilities, you should always be prepared for failures. So, make sure your code is able to respond to smart contract bugs.

To this end, create an effective upgrade plan for bugfixing and code enhancements. Manage the amount of money at risk by establishing rate limitation and maximum usage. In addition, if something goes wrong, pause your contract.

Posted on by:

smartym profile

Sergey V.

@smartym

I’m the CTO at software development company and a technical expert with more than 10 years of experience. Interested in blockchain, AI, Big Data, and cybersecurity solutions.

Discussion

markdown guide