Here are 3 tips which could be considered the low hanging fruit of gas efficient solidity smart contracts.
Use Correct Modifiers & Declarations
This is the simplest way to get some small wins with gas optimisation. Go through your contract and define anything that shouldn’t change as constant (or immutable if it is set after declaration).
uint constant x = 42069;
Next we can check the function parameters are set correctly. Anything that doesn’t modify state should be set as view, or pure if it doesn’t read state either.
function loop(uint _x) external pure returns (uint) {
return _x * 2;
}
When consuming input data such as arrays and strings we need to set the data storage to somewhere. Check to see if changing memory storage to calldata will save gas if you aren’t manipulating that input data.
function doSomething(uint[] calldata _arr) external {
// calldata is read only
}
Plan & Pack State Storage Variables
Persistent state storage is the most gas expensive part of our contracts. We pay a significant amount to take up the valuable storage capacity of the Ethereum virtual machine. This is where we can save the most through careful planning and packing of our state variables.
Ethereum has a standard storage slot of 32 bytes (256 bits). This is why we use uint256 so widely, because it’s a massive number that fits conveniently into a single storage slot.
If we don’t need a massive whole number we can actually break that storage slot down and pack multiple values in there. We want to place smaller sized variables next to each other to ensure they are packed efficiently.
uint128 x = 1;
uint128 y = 2;
We can also pack variables efficiently into structs, such as this one below which still only takes up a single slot.
struct User {
bool verified;
bool confirmed;
uint8 age;
uint32 birthYear;
}
Note that variable types such as uint8 have a maximum value (0-255) due to the limitations of the reduced data storage size.
Optimise For Runtime Cost
When we compile a contract we make compromises based on if we want to optimise for deployment costs or runtime costs. It costs significant amounts to store a smart contract on-chain which is paid for during the deployment transaction. On Ethereum, during peak periods, this can stretch into thousands of dollars. However for contracts where we expect continued and frequent usage it is efficient to optimise for runtime costs which means you will pay more for deployment but users will pay less for transactions.
One simple way to do this is to change the settings in your compiler. For example if you are using Hardhat you can adjust them directly in the hardhat.config.js file.
module.exports = {
solidity: {
version: "0.8.9",
settings: {
optimizer: {
enabled: true,
runs: 1000,
},
},
},
};
In foundry we would set it in the foundry.toml config file:
optimizer = true
optimizer_runs = 1000
And of course in Remix you can do this too by going into the compile tab and editing the advanced configurations settings.
Set the runs to a rough estimate for how many transactions the contract will receive over it’s life time. This doesn’t need to be anywhere near exact but will help inform the solidity compiler as to what you want to optimise for. Setting the runs value to a higher amount will push it towards optimising for runtime costs over deployment costs making tx fees lower for users.