All articles
Web3 FoundationsSeptember 14, 20268 min read

address, uint256, bool: Solidity Types in the First Ten Lines

The handful of Solidity types you meet immediately: what address really holds, why uint256 is everywhere, what happens at the ceiling, and why decimals do not exist.

By Carlos (Bloqarl)

TL;DR

  • You meet about five Solidity types in the first ten lines of almost any contract, and they cover most of what you will read: address, uint256, bool, string, bytes.
  • address holds an account's identity and cannot tell you whether that account is a person's wallet or another contract. Code that assumes one or the other is making an assumption the type does not support.
  • uint256 is an unsigned whole number. Unsigned means it cannot go below zero, and subtracting past zero does not produce a negative, it reverts.
  • Solidity has no decimals. Every "1.5 tokens" you have ever seen is an integer with an agreed decimal point applied by something else.
  • The type on a state variable is a ceiling. Reading the type tells you the largest value that line can ever hold.

What Solidity types do you actually need to know?

Five types cover the overwhelming majority of what you will read: address for account identities, uint256 for whole numbers, bool for true and false, and string and bytes for text and raw data. Everything else is a variation on those, a container holding them, or a name someone gave to one of them.

Here are two of them in context, in a complete contract:

contract PiggyBank {
    address public owner;
    uint256 public goal;

A who and a how much. Sorting each line into what a contract remembers and what it can do is the core of how to read a smart contract, and this pairing, an identity and a quantity, is the most common shape in all of Solidity, and once you see it you will see it everywhere: balances, allowances, stakes, deposits, votes.

What does address actually hold?

An address is a 20 byte value that identifies an account on the chain. You see it written as the familiar 42 character hexadecimal string beginning 0x.

The important part for a reader is what it does not contain.

An address does not say whether it belongs to a person's wallet or to another contract. Both kinds of account have addresses, drawn from the same space, and the type cannot distinguish them. It also carries no name, no owner, no history, and no guarantee that anything exists at that address at all. You can send value to an address nobody controls and nobody ever will.

This is why so much real contract code reads defensively around addresses. When you see a check comparing an address against address(0), that is guarding against the zero address, a valid address that nobody holds the keys to. Value sent there is unrecoverable.

There is a second form you will meet immediately:

    payable(owner).transfer(address(this).balance);

payable(owner) converts a plain address into a payable address, the variant Solidity requires before you can send ETH to it. And address(this) is the contract referring to itself, so address(this).balance is the contract reading its own ETH balance. See payable, receive, and fallback.

Why is uint256 everywhere?

uint means unsigned integer, a whole number that cannot be negative. The 256 is the number of bits, which sets the ceiling.

Three facts do most of the work when reading:

It cannot go below zero. If a calculation would take an unsigned value negative, it does not wrap around to a huge number and it does not produce a negative. On any compiler in the 0.8 series or later, the whole call reverts. That connects directly to require and revert: failure in Solidity is usually total, not partial.

The ceiling is enormous but real. A uint256 maxes out around 1.16 times ten to the seventy seventh. For token amounts and balances that is effectively unbounded. For smaller variants it is not: a uint32 stops just above four billion, and a uint8 stops at 255. When you see a small type, the ceiling is a design decision someone made, and it is worth asking whether the values that reach it could ever get close. EVM storage layout covers why anyone would choose the smaller type.

It is the default for anything countable. Balances, timestamps, amounts, indices, and IDs are nearly always uint256 unless there is a packing reason not to be.

Where are the decimals?

There are none. Solidity has no floating point numbers and no decimal type at all.

This surprises people, because every token interface they have ever seen displays fractional amounts. The resolution is that those fractions are a presentation layer. The chain stores integers, and something outside the contract agrees where to put the decimal point.

ETH is the clearest case. On chain, ETH amounts are counted in wei, the smallest unit, and one ETH is 10 to the power of 18 wei. So a balance that displays as 1.5 ETH is stored as the integer 1500000000000000000. The contract does arithmetic on that integer. Your wallet divides by 10 to the 18 before showing you a number with a dot in it.

Most tokens follow the same convention and publish a decimals value saying where their point goes. Eighteen is the common choice, but it is a choice, not a rule, and some widely used tokens use six. Nothing in the type system enforces any of this. A uint256 holding a token amount looks exactly like a uint256 holding a block number.

For a reader, the practical habit is to ask of any amount: what unit is this in? The type will not tell you. The variable name, the comments, and the rest of the code are the only clues, which is one reason fixed-point arithmetic shows up so often in serious contract code.

What about bool, string, and bytes?

bool is true or false, and it is the simplest type in the language. You meet it mostly as a flag on a contract, bool public paused, or as the result of a comparison.

string holds text. In our example it appears in an unexpected place:

    require(msg.sender == owner, "only the owner");

That quoted text is a string, the message attached to a failure. It is there for humans reading the result of a failed call.

bytes holds raw data of arbitrary length, and bytes32 holds exactly 32 bytes. You see bytes32 constantly for hashes and identifiers, because a hash is exactly 32 bytes and fits one storage slot perfectly.

Two container types round out what you will meet early. An array, written uint256[], holds a list. A mapping, written mapping(address => uint256), is a lookup table, and it is how almost every token contract stores balances: give it an address, get back that address's number.

Delete the type and ask what breaks

  • Change uint256 goal to uint32 goal and the largest goal the pig can hold drops from astronomically large to about four billion wei, which is a fraction of a cent. The contract still compiles and is now useless for its stated job.
  • Change address owner to string owner and the contract can store a name but can no longer send anything to it, because you cannot transfer value to text.
  • Remove the payable(...) conversion on the transfer line and the code will not compile, because Solidity refuses to send ETH to a plain address.
  • Swap uint256 for int256 and the value can now go negative. Every check in the contract that assumed a non-negative amount needs re-reading.

Related questions

What is the difference between uint and uint256? Nothing. uint is an alias for uint256. Both are unsigned 256 bit integers, and you will see the two spellings used interchangeably in real code.

Can a Solidity address tell me if it is a wallet or a contract? Not from the type itself. Both kinds of account share the same address space. Code can check at runtime whether there is code deployed at an address, but that check has known limitations and the address type carries no such information on its own.

Why do token amounts have 18 decimals? By convention, matching ETH's smallest unit, wei. The chain stores plain integers, and the token's published decimals value tells interfaces where to place the point when displaying. Some widely used tokens publish 6 instead, so the convention is not a guarantee.

What happens if a uint256 overflows? Since Solidity 0.8, arithmetic that would exceed the maximum or fall below zero reverts the whole transaction by default. Earlier versions wrapped around silently, which is why contracts written before 0.8 often use a safe-math library.

What is bytes32 used for? Fixed size raw data, most commonly hashes and identifiers. It is exactly one storage slot wide, which makes it efficient, and hashes are exactly 32 bytes, which makes it the natural fit.

Is a mapping the same as a dictionary? Close enough for reading. It maps keys to values, so mapping(address => uint256) balances gives each address a number. Unlike a dictionary, you cannot list its contents or count its entries, because unset keys simply return the zero value.

Where to go next

Types are the smallest unit of meaning in a contract and they carry more than they look like they do. An address is an identity with no guarantees attached. A uint256 is a whole number with a hard floor at zero. A decimal point is somebody else's idea, applied after the fact.

Next, the first function in almost every contract, and the only one that runs exactly once: the Solidity constructor, which is where those two state variables get their values in the first place.

Tagged

SoliditySmart ContractsLearn to Code