Smart Contract Security Part III: Advanced Techniques for Unbeatable Security and Peace of Mind

Kaan Kaçar
CoinsBench
Published in
5 min readMar 17, 2023

--

Smart contracts are a critical component of decentralized applications, enabling developers to create trustless and secure systems without intermediaries. However, the complexity of smart contracts and the high stakes involved in their execution make them a prime target for attackers. As such, smart contract security is an essential aspect of decentralized applications, and developers need to rigorously test and audit their smart contracts to ensure their reliability and safety. In this article, we will explore advanced techniques for auditing and testing smart contracts, including property-based testing, differential fuzzing, and symbolic execution, to help developers enhance the security of their smart contracts and minimize the risk of vulnerabilities.

1. Property-Based Testing

Property-based testing is a testing technique that involves generating random inputs and checking if the output satisfies a set of predefined properties. This approach is particularly useful for testing complex systems with a large number of inputs and outputs.

To demonstrate this technique, we will use the QuickCheck library for Solidity. QuickCheck generates random inputs based on a set of constraints and tests the smart contract’s properties. For example, suppose we have a simple smart contract that calculates the sum of two integers:

pragma solidity ^0.8.0;
contract Adder {
function add(uint x, uint y) public pure returns (uint) {
return x + y;
}
}

We can use QuickCheck to test this contract by defining the following properties:

pragma solidity ^0.8.0;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "fuzzysol/QuickCheck.sol";
contract TestAdder is QuickCheck {
Adder adder = Adder(DeployedAddresses.Adder());
function checkAddition(uint x, uint y) public returns (bool) {
uint sum = adder.add(x, y);
return sum == x + y;
}
}

In this example, we define a QuickCheck test case that generates random values for x and y and checks if the sum calculated by the smart contract matches the expected value. If the test fails, QuickCheck will generate a minimal input that reproduces the failure.

2. Differential Fuzzing

Differential fuzzing is a technique that involves comparing the output of two implementations of a smart contract with the same inputs. This approach can detect subtle differences between two implementations that may indicate a vulnerability.

To demonstrate this technique, we will use the Echidna tool, which is a smart contract fuzzer that supports differential testing. Echidna can generate random inputs and compare the output of two implementations of a smart contract, looking for discrepancies. For example, suppose we have a smart contract that implements a simple voting system:

pragma solidity ^0.8.0;

contract Voting {
mapping(address => bool) public votes;
function vote(bool choice) public {
votes[msg.sender] = choice;
}
function result() public view returns (bool) {
uint yesVotes = 0;
uint noVotes = 0;
for (address voter : votes) {
if (votes[voter]) {
yesVotes++;
} else {
noVotes++;
}
}
return yesVotes > noVotes;
}
}

We can use Echidna to test this contract by defining two implementations of the same contract with different logic:

pragma solidity ^0.8.0;

import "./Voting.sol";
contract ModifiedVoting is Voting {
function result() public view returns (bool) {
uint yesVotes = 0;
uint noVotes = 0;
for (address voter : votes) {
if (votes[voter]) {
yesVotes++;
}
}
return yesVotes >= noVotes;
}
}

In this example, the ModifiedVoting contract is a modified version of the original Voting contract that changes the result calculation logic. We can use Echidna to compare the output of the original contract with the output of the modified contract and look for discrepancies:

import "ds-test/test.sol";
import "./Voting.sol";
import "./ModifiedVoting.sol";
import "./echidna/Echidna.sol";

contract TestVoting is DSTest, EchidnaTest {
Voting voting = new Voting();
ModifiedVoting modifiedVoting = new ModifiedVoting();
function testVoting() public {
assertTrue(voting.result() == modifiedVoting.result());
}
}

In this example, we define a test case that compares the output of the original Voting contract with the output of the ModifiedVoting contract. If the test fails, Echidna will generate a minimal input that reproduces the failure.

3. Symbolic Execution:

Symbolic execution is a technique that involves exploring all possible paths through a smart contract by systematically solving a set of constraints. This approach can identify vulnerabilities that are difficult to detect with other techniques, such as integer overflows and underflows.

To demonstrate this technique, we will use the Mythril tool, which is a symbolic execution framework for Ethereum smart contracts. Mythril can analyze smart contracts and generate a report that highlights potential vulnerabilities. For example, suppose we have a smart contract that implements a simple token:

pragma solidity ^0.8.0;

contract Token {
mapping(address => uint) balances;
function transfer(address to, uint amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
balances[to] += amount;
}
function balanceOf(address account) public view returns (uint) {
return balances[account];
}
}

We can use Mythril to analyze this contract and identify potential vulnerabilities:

myth analyze Token.sol

In this example, Mythril will generate a report that highlights potential vulnerabilities, such as integer overflows and underflows.

Symbolic execution is a powerful technique that involves exploring all possible paths through a smart contract by systematically solving a set of constraints. This approach can identify vulnerabilities that are difficult to detect with other techniques, such as integer overflows and underflows. While symbolic execution can be time-consuming and computationally expensive, it is a valuable tool for advanced developers who want to ensure that their smart contracts are secure and free of vulnerabilities.

However, it’s worth noting that like any tool or technique, Symbolic execution and Mithril have their limitations. For example, symbolic execution may not be effective in identifying vulnerabilities that arise from complex interactions between multiple contracts. Similarly, while Mithril provides a valuable starting point for identifying potential vulnerabilities in smart contracts, developers should also conduct manual code reviews and other testing and auditing techniques to ensure that their smart contracts are secure and free of vulnerabilities.

Smart contract security is a critical concern for developers working on decentralized applications. The techniques we have explored in this article, including property-based testing, differential fuzzing, and symbolic execution, are powerful tools for identifying and mitigating potential vulnerabilities in smart contracts. However, it is important to note that these techniques are not a panacea and should be used in conjunction with other security best practices, such as code reviews, formal verification, and continuous monitoring. By combining these techniques and best practices, developers can enhance the security of their smart contracts and create trustless and secure decentralized applications that meet the needs of their users.

--

--