The Smart Contract Weakness Classification and Test Cases (SWC) Registry is a set of Web3 vulnerabilities to avoid when writing smart contracts. It may seem daunting to understand every issue so I’ll do my best to demystify every issue and explain each vulnerability with real-world examples. This write-up is intended for those who are just starting out in solidity development and smart contract auditing.
SWC-108: State Variable Default Visibility
Variables can be specified as being public, internal, or private. When the visibility is not specified, the default visibility of a state variable is internal. Variables declared with the internal keyword are only accessible within the contract in which they were declared and derived contracts.
Remember, nothing in the blockchain is a secret! Everything can be accessed, so having private visibility on the state variable does not mean that it is a secret! Do not store important passwords/key values on the blockchain. In this write-up, I will be discussing these points:
How do I access private variables?
How is storage packed?
Summary
References
How do you access private variables?
With web3.eth.getStorageAt
you can read the complete storage of any contract externally (off-chain).
For example, you have these variables in your contract
address private a;
address private b;
mapping (bytes32 => uint) public people;
mapping (bytes32 => mapping(address => uint)) public listOfEmp;
bytes32[] public list;
bytes32 private z;
The address ‘a’, although private, can be accessed with the getStorageAt function that passes in two variables, the contract address and the storage slot. Storage slots in solidity always start from 0.
web3.eth.getStorageAt("0x501...", 0)
‘z’ can be accessed with
web3.eth.getStorageAt("0x501...", 5)
We know how to extract data from storage now, but how do we know which storage slots to call?
How is storage packed?
getStorageAt
returns a storage slot, which is 32 bytes. Once 32 bytes is reached, the next storage slot is called. Different data types have different amounts of bytes. For example, an address has 20 bytes. A boolean has 1 byte. A uint256 has 32 bytes. (uint8 has 1 byte). With this information in mind, realize that the variables of the fixed size could be stored in a single storage slot if they occupy <= 32 bytes.
For example, if your contract would be:
address private a;
bool private b;
Reading the first slot web3.eth.getStorageAt("0x501...", 0)
would return not just the address a
but also the value for b
.
This means, you need to know the slot number for a variable, but also the size and offset.
address private a; // slot 0, size 20, offset 0
bool private b; // slot 0, size 1, offset 20
Next, take a look at the 3 types of variables, a,b, and c.
bool private a;
uint256 private b;
bool private c;
What is the storage slot of ‘c’? Is it the second or third slot? The answer is the third slot. If the next variable exceeds the length of the storage slot, the previous variable gets the full storage slot even though it is not packed fully.
bool private a; // 1 byte, storage slot 0
uint256 private b; // 32 bytes, storage slot 1
bool private c; // 1 byte, storage slot 2
Things get more complicated with arrays, mappings, strings and dynamically sized types.
For example, for arrays:
Fixed length: each item as a single variable:
// slot 0 for item0
// slot 1 for item1
// slot 2 for item2
address[3] private a;
// slot 3
address private b;
Dynamic length:
// slot 0 for current array length
// each item at specific index is accessed under the slot: keccak256(encodePacked(0)) + index
address[] private a;
// slot 1
address private b;
Multiple slots per item in dynamic arrays
struct Data {
address user;
uint256 balance;
}
// slot 0 for current array length
// 2 slots per item
// a[0].user would be : keccak256(encodePacked(0)) + 0 * 2 + 0
// a[0].balance would be: keccak256(encodePacked(0)) + 0 * 2 + 1
// a[1].user would be : keccak256(encodePacked(0)) + 1 * 2 + 0
// a[1].balance would be: keccak256(encodePacked(0)) + 1 * 2 + 1
Data[] private a;
// slot 1
address private b;
Another caveat could be inheritance:
contract A {
// slot 0
address private a;
}
contract B is A {
// slot 1
address private b;
}
Now we know how storage is packed in solidity! Storage slot packing is important to save gas from deployment costs! More on this next time.
Summary
Don’t use private visibility to hide important values
A storage slot has 32 bytes
An address has 20 bytes, a bool value has 1 byte, and a uint value has 32 bytes
Dynamic arrays and inherited contracts have special storage slots packing
References:
https://www.developer.com/languages/variable-function-visibility-solidity/#:~:text=State%20Variable%20Visibility%20in%20Solidity&text=There%20are%20three%20visibility%20modifiers,variable%20name%20(e.g%20var).
https://stackoverflow.com/questions/50493197/solidity-accessing-private-variable