Advanced Solidity | Gas Golfing | Yul | EVM | Audit | Zero Knowledge Proofs | Puzzles
Advanced Solidity | Gas Golfing | Yul | EVM | Audit | Zero Knowledge Proofs | Puzzles
Share Dialog
Share Dialog

Subscribe to aṇuha

Subscribe to aṇuha
<100 subscribers
<100 subscribers
This is a continuation of the previous two articles on gas optimization. While it does not matter and one can start from here. There are some references to previous articles.
In Gas Golfing #1, We visited the ReentrancyGuard.sol from OpenZeppelin and discussed why a uint256 variable is used to mark the entry and exit of functions. They could have in fact used a bool or a uint8 . One of the reasons was covered in the article. We will deep dive into major reasoning in this section.
Beginning with the ReentrancyGaurd from OpenZeppelin. It is below here.
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* <https://blog.openzeppelin.com/reentrancy-after-istanbul/>[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// <https://eips.ethereum.org/EIPS/eip-2200>)
_status = _NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
}
I am interested in the below section of the code.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
Why is OpenZeppelin using 1 and 2 integer constants and not 0 and 1 constant integers or false and true boolean constants to mark the entry and exit of execution of the code block?
The simple answer here is again Gas Optimization.
The best way to understand this is by
Creating a sample contract and checking the gas utilization of certain functions.
Learning about the EVM and opcodes in a certain depth
We will also visit certain snippets (The Uncharted Territory by most Solidity Developers) in the yellow paper.
Hence we will do this over multiple articles.
The default value of a variable is also called Zero Value. Examples are
0 --> for intergers and unsigned integers
false --> for booleans
0x0000000000000000000000000000000000000000 --> for address
Any value other than zero values are non-zero values for that variable. Examples are
permitted numbers other than ZERO -> for integers and non-integers
true -> for boolean
other addresses -> for address
So what is the importance of this distinction?
Zero Values do not occupy any storage in the blockchain. Irrespective of what storage slot or variable type is associated with a slot, the blockchain does not need to manage the zero values as they are default. At the same time, any non-zero value that is to be stored on the chain needs to be updated on all the nodes of that chain.
Let’s take the below code sample deployed with no optimization
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract Example {
uint256 val;
// Input : 100
// Transaction Cost: 43702 gas
// Execution Cost = 43702 - 21000 = ~22500+ gas
// SSTORE = 20000 units
// Input : 0
// Transaction Cost: 21709 gas
// Execution Cost = 21709 - 21000 = ~709 gas?
// But the actual execution cost is 5398 units
function setval(uint256 _val) external {
val = _val;
}
}
val is initialized to zero on contract deployment And can be set to a desired unsigned integer using setVal.
Points to note:
Setting the uint256 val to 100 consumes more than 20000 units of gas towards storing the value on the chain. This is zero to non-zero transition. They are costly.
Setting the uint256 val to 0 consumes just 709 units of gas for storing the zero-value on the chain. This is non-zero to zero transition. They are gas-saving because the blockchain does not need to store the data on the chain. Hence Ethereum rewards the removal of value from the chain. Only 709 gas units of the sender are used while the execution cost is 5398 units.
A total of 23000+ units of gas for set and unset of val.
Now lets the try the sample with slight changes. val is initialized to 1.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract Example {
uint256 val = 1 ;
// Input : 100
// Transaction Cost: 26602 gas
// Execution Cost = 26602 - 21000 = 5398 gas
// Input : 1
// Transaction Cost: 26602 gas
// Execution Cost = 26602 - 21000 = 5398 gas
function setval(uint256 _val) external {
val = _val;
}
}
Points to note:
Setting the uint256 val to 100 consumes 5398 units of gas for storing the value on the chain. This is a non-zero to non-zero transition. And the same for setting val to 1.
A total of 10600+ units of gas for set and unset of val.
This is why ReentrancyGuard does not use either bool or zero-values for setting and unsettling the variable values in places where the values of a variable toggle frequently.
Just pick up a token and check its transactions and most likely you will see varying gas units consumed for the same token transfer and this is most likely the reason for the varying gas units consumed.
In conclusion, choose not only the variable type but also the initial variable values carefully.
Such excellent learning from just one contract of the OpenZeppelin. Kudos to the OZ team.
This is a continuation of the previous two articles on gas optimization. While it does not matter and one can start from here. There are some references to previous articles.
In Gas Golfing #1, We visited the ReentrancyGuard.sol from OpenZeppelin and discussed why a uint256 variable is used to mark the entry and exit of functions. They could have in fact used a bool or a uint8 . One of the reasons was covered in the article. We will deep dive into major reasoning in this section.
Beginning with the ReentrancyGaurd from OpenZeppelin. It is below here.
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* <https://blog.openzeppelin.com/reentrancy-after-istanbul/>[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// <https://eips.ethereum.org/EIPS/eip-2200>)
_status = _NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
}
I am interested in the below section of the code.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
Why is OpenZeppelin using 1 and 2 integer constants and not 0 and 1 constant integers or false and true boolean constants to mark the entry and exit of execution of the code block?
The simple answer here is again Gas Optimization.
The best way to understand this is by
Creating a sample contract and checking the gas utilization of certain functions.
Learning about the EVM and opcodes in a certain depth
We will also visit certain snippets (The Uncharted Territory by most Solidity Developers) in the yellow paper.
Hence we will do this over multiple articles.
The default value of a variable is also called Zero Value. Examples are
0 --> for intergers and unsigned integers
false --> for booleans
0x0000000000000000000000000000000000000000 --> for address
Any value other than zero values are non-zero values for that variable. Examples are
permitted numbers other than ZERO -> for integers and non-integers
true -> for boolean
other addresses -> for address
So what is the importance of this distinction?
Zero Values do not occupy any storage in the blockchain. Irrespective of what storage slot or variable type is associated with a slot, the blockchain does not need to manage the zero values as they are default. At the same time, any non-zero value that is to be stored on the chain needs to be updated on all the nodes of that chain.
Let’s take the below code sample deployed with no optimization
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract Example {
uint256 val;
// Input : 100
// Transaction Cost: 43702 gas
// Execution Cost = 43702 - 21000 = ~22500+ gas
// SSTORE = 20000 units
// Input : 0
// Transaction Cost: 21709 gas
// Execution Cost = 21709 - 21000 = ~709 gas?
// But the actual execution cost is 5398 units
function setval(uint256 _val) external {
val = _val;
}
}
val is initialized to zero on contract deployment And can be set to a desired unsigned integer using setVal.
Points to note:
Setting the uint256 val to 100 consumes more than 20000 units of gas towards storing the value on the chain. This is zero to non-zero transition. They are costly.
Setting the uint256 val to 0 consumes just 709 units of gas for storing the zero-value on the chain. This is non-zero to zero transition. They are gas-saving because the blockchain does not need to store the data on the chain. Hence Ethereum rewards the removal of value from the chain. Only 709 gas units of the sender are used while the execution cost is 5398 units.
A total of 23000+ units of gas for set and unset of val.
Now lets the try the sample with slight changes. val is initialized to 1.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract Example {
uint256 val = 1 ;
// Input : 100
// Transaction Cost: 26602 gas
// Execution Cost = 26602 - 21000 = 5398 gas
// Input : 1
// Transaction Cost: 26602 gas
// Execution Cost = 26602 - 21000 = 5398 gas
function setval(uint256 _val) external {
val = _val;
}
}
Points to note:
Setting the uint256 val to 100 consumes 5398 units of gas for storing the value on the chain. This is a non-zero to non-zero transition. And the same for setting val to 1.
A total of 10600+ units of gas for set and unset of val.
This is why ReentrancyGuard does not use either bool or zero-values for setting and unsettling the variable values in places where the values of a variable toggle frequently.
Just pick up a token and check its transactions and most likely you will see varying gas units consumed for the same token transfer and this is most likely the reason for the varying gas units consumed.
In conclusion, choose not only the variable type but also the initial variable values carefully.
Such excellent learning from just one contract of the OpenZeppelin. Kudos to the OZ team.
No activity yet