Solidity variables — storage, type conversions and accessing private variables

Ankit Goyal
Coinmonks
Published in
5 min readJul 28, 2021

--

This post offers a beginners introduction to how solidity stores variables, does type conversion and how you can check value of private variables. It is to gain basic understand how things are happening at byte level in solidity.

Here are the variable types that I’ll be looking at — uint, string, bytes

How are variables stored in EVM

In the Ethereum Virtual Machine, there are 3 areas where it can store data [1] -

  • Storage — Each contract has a data area called storage, which is persistent between function calls and transactions. State variables (variables declared outside of functions) are by default storage and written permanently to the blockchain. Storage is expensive to use and should be used judiciously.
  • Memory — The second data area is called memory, of which a contract obtains a freshly cleared instance for each message call. Variables declared inside functions are memory and disappear when the function call ends
  • Stack — all computations are performed on a data area called the stack

We are going to look at Storage variables that are written to the blockchain. All variables are stored as hexadecimals in storage.

  • 0x — Prefix represents that the rest of the message is in hexadecimal

Ethereum uses two formats depending on the variable type -

  • Big endian format for strings and bytes
    For example, this is how the string “abcd” is stored in hex format (32 bytes):
    0x6162636400000000000000000000000000000000000000000000000000000000

The hex value is stored on the left in Big endian format followed by zeroes, while it is stored on the right in Little endian format.

  • Little endian format is used for other types (bool, numbers, addresses, etc…).
    The number “10” will be stored as
    0x000000000000000000000000000000000000000000000000000000000000000a

Solidity stores the variables in storage slots. Each storage slot in solidity is 32 bytes long. [2]
1 byte = 8 bits
This means that 1 byte can store a maximum value as (28–1) = 255.
The hexadecimal representation of 255 is ff. To store a higher number, another byte will be needed. This implies that 1 byte can store a 2 digit hexadecimal number. So the length of hexadecimal number is 32*2 = 64 digits long.

Storage of uint

Let’s take a look at how uint is stored. When we declare a variable as uint, it is uint256 by default. uint is an unsigned type, which means it can take only positive values.

A uint256 is full 32 bytes long. The number ‘256’ represents the number of bits. The maximum no that can be stored in a uint256 is
2**256–1 (as numbers start from 0)

For uint8, the maximum number is 2**8–1 = 255. This will take only 1 byte to store.

For types that occupy lower than 32 bytes, solidity automatically packs them with other lower bytes variables depending on how they are defined in the contract.
For example, if we define our variables as

contract A {uint8 a; # goes to storage slot 0
uint8 b; # storage slot 0
uint c; # storage slot 1
}

They will be stored in 2 storage slots only, with `a` and `b` packed into the same slot.

However, if we define them as

contract A {uint8 a; # goes to storage slot 0
uint c; # storage slot 1
uint8 b; # storage slot 2
}

The variables will now take 3 storage slots. As the slots are assigned in seriality in this example.

For more complex types such as mappings, dynamic arrays, or big strings, the storage slot positions are calculated using predetermined formulas. You may refer to Solidity docs link given in the footnotes.

Solidity Type Conversions

uint conversions explained using mod

Let’s take the example of following contract

pragma solidity ^0.8.6;contract A {uint32 a = 100000;
uint16 public b = uint16(a); //b = a % 65536
uint8 public c = uint8(a); //c = a % 256
}

Any lower order number can be converted into a higher order number by adding zeroes to it on the left. However, when we convert a higher order number to a lower order, solidity wraps around the smaller number and gives the result.

In the above example, uint16 can be calculated by wrapping the uint32 100000 around max(uint16) or 65536.
So b = a % 65536 = 34464
Similarly, c = a % 256 = 160

bytes32 to bytes16

pragma solidity ^0.8.6;contract C {bytes32 public b32 = bytes32(“This is a big string”);
bytes16 public b16 = bytes16(b32);
}

The value of b32 will be 0x5468697320697320612062696720737472696e67000000000000000000000000

When you convert this to bytes16, solidity will remove the 16 bytes (or 32 digits in hexadecimal) from the right.

When bytesX is converted to bytesY where y < x then x is truncated from the right hand side till the length in bytes is equal to y.

The value of b16 will be 0x54686973206973206120626967207374

Example with all types

Bytes can be declared directly in solidity by prefixing with 0x.
bytes8 example = 0x11030330f020D5C5;

bytes8 example = 0x11030330f020D5C5;

//uint conversion — truncated from left when converting to lower //bytes; adds 0 to the left when coverting to higher bytes
uint64 public v1 = uint64(example);
//uint64 conversion is allowed for bytes8
uint32 public v2 = uint32(v1);
//v2 == uint32(0xf020D5C5)
uint96 public v3 = uint96(v1);
bytes12 public b = bytes12(v3);
//bytes16(uint32(uint64(example))) = 0x0000000011030330f020d5c5
bytes4 public b1 = bytes4(v2);
//bytes4(uint32(uint64(example))) = 0xf020D5C5
//bytes to bytes conversion — truncates from rightbytes4 public b2 = bytes4(example);
//bytes4(example) 0x11030330

//string conversion — truncates from right when converting to lower //bytes; adds 0 to the right when coverting to higher bytes
string public s = “abcd”;bytes public b3 = bytes(s);
//b3 = 0x61626364
bytes2 public b4 = bytes2(b3);
//b4 = bytes2(bytes(s)) = 0x6162
bytes16 public b5 = bytes16(b3);
//b5 = bytes16(bytes(s)) = 0x61626364000000000000000000000000
string public s1 = string(b3);
//s1 = “abcd”
//address conversionaddress a1 = 0x2014a9707099DFcbA3Bb91D23b31cF7Be61941d9;
bytes20 public b6 = bytes20(a1); // works
bytes32 public b7 = bytes32(abi.encode(a1)); //works

//bool conversion
bytes32 public b8 = bytes32(abi.encode(true)); //works

How to access private variables

The visibility of variables can be public, private or internal. Variables are internal by default. Public variables can be accessed from anywhere. The internal variables can be called by functions of the same solidity contract or the ones that inherit this contract. And private means that the variable can only be accessed within the same contract.

However, since the value of private variables is stored in the EVM on blockchain, it can be _seen_ by anyone. The value of the private variable can be extracted from the EVM.

For example, let us take the following contract —


pragma solidity ^0.8.6;
contract A {
uint private secretValue;
constructor(uint _value) {
secretValue = _value; }
}

We can define the secretValue at the time of deploying the contract. This variable cannot be queried through another program. However, its value will be stored at storage slot 0 of the contract. If we use a simple web3js function, we’ll get the value of private variable in bytecode. [3]
web3.eth.getStorageAt(contractAddress, 0)

Let’s say we defined the secretValue as 10. Then I will see the value of this variable as
0x000000000000000000000000000000000000000000000000000000000000000a

And we can easily convert this back into the decimal value of 10.

References

--

--