Solidity’s SMTChecker can automatically find real bugs
By: Leonardo Alt
Tags: Formal Verification, Smart Contracts, Solidity, Ethereum

In my previous post (https://medium.com/@leonardoalt/formal-verification-in-solidity-5cbff7b7ff8) I wrote about the SMTChecker, a formal verification module embedded in Solidity’s compiler that is under development. The goal of the SMTChecker is to be a compile-time automated prover and bug finder, where properties are specified using Solidity’s require/assert .

While developing the SMTChecker we often try examples from other more stable/established formal verification tools, which we use as benchmarks for what should be supported and proved. After reading https://medium.com/ethworks/formal-verification-for-n00bs-part-3-an-attempt-to-prevent-classic-hack-with-klab-8e8d13318086, we were curious to see how the SMTChecker would perform on that example after our recent work on supporting arrays and reporting fewer false positives when SafeMath is used.

The bug analyzed here is the batch overflow vulnerability. The following code snippet is a reduced version of the full contract which contains the original function:

pragma solidity ^0.5.0;

pragma experimental SMTChecker; import "./SafeMath.sol"; contract Bug

{

using SafeMath for uint256; bool paused = false;

mapping (address => uint) balances; modifier whenNotPaused {

require(!paused);

_;

} function batchTransfer(

address[] memory _receivers,

uint256 _value

) public whenNotPaused returns (bool) {

uint cnt = _receivers.length;

uint256 amount = uint256(cnt) * _value;

require(cnt > 0 && cnt <= 20);

require(_value > 0 && balances[msg.sender] >= amount); balances[msg.sender] = balances[msg.sender].sub(amount);

for (uint i = 0; i < cnt; i++) {

balances[_receivers[i]] = balances[_receivers[i]].add(_value);

//Transfer(msg.sender, _receivers[i], _value);

}

return true;

}

}

Briefly, the bug is an overflow in the operation uint256(cnt) * _value . An attacker can choose specific _receivers and a huge _value such that cnt > 0 && cnt <= 20 is true and amount is small, such that they have enough funds. Notice that the gain is in balances[_receivers[i]].add(_value) , where _value might be much higher than it should be.

If we simply run the compiler on the example, the output (among some false positives) contains:

bug.sol:18:20: Warning: Overflow (resulting value larger than 2**256 - 1) happens here

uint256 amount = uint256(cnt) * _value;

^-------------------^

for:

<result> = 2**256

_value = 0x80 * 2**248

amount = 0

balances[msg.sender] = 0

cnt = 2

msg.sender = 2997

The run took 10.24s with 2 backend SMT solvers on my laptop. It basically tells us how to exploit the bug: call batchTransfer(_receivers, 0x80 * 2**248) , where _receivers is an array with two addresses. After the overflow, amount = 0 which satisfies balances[msg.sender] >= amount . The addresses will then receive 0x80 * 2**248 tokens each.

This example shows that the SMTChecker can be used at compile-time to quickly find real bugs, even though it still lacks support to parts of Solidity. We are constantly increasing the power of the SMTChecker, hopefully eventually making it able to fully analyze complex smart contracts automatically.

If you have any questions/ideas please reach out! You can comment here, talk to us on our Gitter channel or start a thread in the Solidity repository.