# Eliminating Smart Contract Special Cases

In a few projects I’ve audited recently, I noticed that special cases were causing significant code complexity. Complexity is the enemy of security, so I’m always looking for ways to simplify things.

In this post, I’ll share some examples of how eliminating special cases can reduce code complexity and improve maintainability.

## Special Maximums

A common special case is using 0 to mean “no maximum”. This special case is usually easy to eliminate.

### Special Expirations

Consider this code:

``````uint256 expiration;

// Use 0 to mean "no expiration".
function setExpiration(uint256 newExpiration) external {
expiration = newExpiration;
}

function doSomething() external {
require(expiration == 0 || now < expiration, "Error: expired");
...
}``````

In this code, 0 is a special case that means “there is no expiration”. This special case is unintuitive, and it’s adding complexity to that `require` statement.

The real danger, though, is when a new developer on the team misses this subtlety and fails to handle the special case of `expiration == 0`. That could easily lead to lost funds or other serious issues.

The code is simpler and more obvious this way:

``````// Default to 2**256-1 instead.
uint256 expiration = 2**256-1;

// Use 2**256-1 to mean "no expiration".
function setExpiration(uint256 newExpiration) external {
expiration = newExpiration;
}

function doSomething() external {
require(now <= expiration, "Error: expired");
...
}``````

Here, instead of 0, I’ve used an `expiration` of the maximum allowable `uint256`, which is effectively infinite when it comes to timestamps.1 Now `expiration` always means exactly what it says.

### Special Maximum Ether Amounts

Here’s a very similar example, but this time involving ether:

``````uint256 maxWithdrawal;

// Use 0 to mean "no maximum".
function setMaxWithdrawal(uint256 newMax) external {
maxWithdrawal = newMax;
}

function withdraw(uint256 amount) external {
require(maxWithdrawal == 0 || amount <= maxWithdrawal, "Error: too much");
...
}``````

Again, we have an unintuitive special case, which we can do away with by using an effectively infinite value:2

``````// Default to 2**256-1 instead.
uint256 maxWithdrawal = 2**256-1;

// Use 2**256-1 to mean "no maximum".
function setMaxWithdrawal(uint256 newMax) external {
maxWithdrawal = newMax;
}

function withdraw(uint256 amount) external {
require(amount <= maxWithdrawal, "Error: too much");
...
}``````

### 2256-1 Is a Great Maximum

Note that this same trick generalizes to token amounts or any value at all. Because Solidity can’t represent values greater than 2256-1, it always works as an “effectively infinite” value to compare with a `uint256`.3

### Working Around Gas Costs

As is often the case, there’s a tradeoff here with regard to gas costs. A typical reason people end up using 0 as a default is that storing non-zero values costs gas.

If storage costs are significant for your use case, consider a trick like this:

``````uint256 _expiration; // 0 still means "no expiration"

...

// Properly handle the special cases in one place.
function expiration() internal view returns (uint256) {
return _expiration > 0 ? _expiration : 2**256-1;
}

function doSomething() external {
require(now < expiration(), "Error: expired");
}``````

In this code, the `_expiration` value written to storage is 0 by default, with the same special meaning as before. But I’ve introduced a helper function `expiration()` that translates a 0 into the less special value of 2256-1. This means the rest of my code doesn’t need to deal with that special case.

Consider pairing this technique with a custom linter rule that makes sure you don’t read `_expiration` directly anywhere except in the `expiration()` function.

When it comes to addresses, there are two types of special cases I see frequently:

1. Address 0 is often disallowed.
2. Specific addresses, often privileged roles, are disallowed.

Here’s a familiar bit of code that uses 0 as a special case:

``````function transfer(address to, uint256 amount) external {
require(to != address(0), "Error: can't send to 0x0");
...
}
``````

Disallowing address 0 is typically an attempt to protect users from mistakes. Sending tokens to address 0 is usually no more disastrous than sending them to address 1, but 0 is a default value and thus much more likely to get passed in accidentally due to a buggy tool or library.

I personally dislike this type of check for address 0, but it’s rarely problematic. Unlike in the previous examples, nothing will break if a developer forgets about this special case when maintaining the code.

This code snippet is much more troubling than the last:

``````address owner;

constructor() public {
owner = msg.sender;
}

function transfer(address to, uint256 amount) external {
require(to != owner, "Error: can't send to owner.");
...
}
``````

When I see code like this, my immediate question is why the `owner` address can’t receive tokens. A check like this is often an effort to put security controls in place, but it usually fails to account for Sybil attacks, where multiple addresses in the system are controlled by the same person.

In this particular example, the owner could simply receive tokens with a different address. If that violates some security assumption of the contract, then there’s a problem.

Special cases like this are a code smell, but that doesn’t mean they should always be eliminated. The important thing to do is document why this special case is needed and consider alternatives.

## Summary

• When possible, eliminate special cases altogether.
• 2256-1 is a good replacement for maximum values.
• Special cases for address 0 are usually okay.
• Special cases for other specific addresses are a code smell.
• If you decide to use values with special meaning in your code, try to isolate the code for handling them.