5/5
## Reducing Gas Costs in Vyper: Using `constant` and `immutable` When writing Vyper smart contracts, efficiency is key. One major area for optimization lies in how you handle contract variables. By default, variables declared at the contract level are **Storage Variables**. While flexible, they come with a significant drawback: interacting with them costs considerable gas. ### The Cost of Storage Variables Storage variables, like `minimum_usd`, `price_feed`, or `owner` in our example `buy_me_a_coffee.vy` contract, persist on the blockchain's state. Every time your contract reads from (`SLOAD`) or writes to (`SSTORE`) these variables, it incurs gas fees. These operations are among the most expensive in the Ethereum Virtual Machine (EVM). If a variable's value doesn't need to change after being initially set, using a storage variable is often unnecessarily expensive. Vyper provides two more gas-efficient alternatives: `constant` and `immutable` variables. Our goal is to refactor our contract to use these where appropriate, thereby reducing deployment and runtime gas costs. ### Constant Variables: Known at Compile Time A `constant` variable is a value that is fixed and known *before* the contract is even compiled. Think of it as a hardcoded value baked directly into the contract's bytecode. **Key Characteristics:** * **Value:** Determined at compile time. * **Mutability:** Cannot *ever* be changed after declaration. * **Gas Efficiency:** Extremely gas-efficient, as reading the value doesn't require accessing storage. * **Naming Convention:** By convention, constants are named using `ALL_CAPS_WITH_UNDERSCORES`. * **Access:** Accessed directly by their name, *without* the `self.` prefix. **Example: Refactoring `minimum_usd`** In our contract, `minimum_usd` is set to the equivalent of $5 in wei within the constructor and never changes. Since this value (5 ether) is known before we deploy, it's ideal for a `constant`. * **Before (Storage Variable):** ```vyper # Storage variables minimum_usd: public(uint256) # ... other variables ... @deploy def __init__(price_feed: address): self.minimum_usd = as_wei_value(5, "ether") # ... rest of constructor ... # Accessed later as: # assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" ``` * **After (Constant Variable):** ```vyper # Constants & Immutables MINIMUM_USD: public(constant(uint256)) = as_wei_value(5, "ether") # Value set in declaration # ... other variables ... @deploy def __init__(price_feed: address): # self.minimum_usd line is REMOVED from constructor # ... rest of constructor ... # Accessed later as: assert usd_value_of_eth >= MINIMUM_USD, "You must spend more ETH!" # No 'self.' ``` Notice how the value is assigned directly in the declaration line using `constant(uint256)) = ...`. The assignment is removed from the `__init__` function, and subsequent references use `MINIMUM_USD` directly. ### Immutable Variables: Set Once at Deployment An `immutable` variable is a value that is set exactly *once*, during contract deployment within the constructor (`__init__` function), and cannot be changed thereafter. Like constants, their values are embedded in the contract's deployed code rather than storage slots, making them much cheaper to read than storage variables. **Key Characteristics:** * **Value:** Determined at deployment time (within the `__init__` function). * **Mutability:** Cannot be changed *after* the constructor finishes execution. * **Gas Efficiency:** Significantly more gas-efficient than storage variables, though slightly less so than constants because the value isn't fixed until deployment. * **Naming Convention:** Also uses `ALL_CAPS_WITH_UNDERSCORES`. * **Access:** Assigned in the constructor *without* `self.` and accessed later directly by name, also *without* the `self.` prefix. **Example 1: Refactoring `price_feed`** The `price_feed` address is provided as an argument to the constructor and set once. It needs to remain fixed for the contract's lifetime. * **Why not `constant`?** The specific price feed address might differ depending on the network (e.g., Sepolia testnet vs. Ethereum mainnet). Since the value isn't universally known at compile time, it can't be `constant`. * **Why `immutable`?** It's set once based on a deployment parameter, making it a perfect fit for `immutable`. * **Before (Storage Variable):** ```vyper price_feed: public(AggregatorV3Interface) # ... @deploy def __init__(price_feed: address): # ... self.price_feed = AggregatorV3Interface(price_feed) # ... # Accessed later as (e.g., in _get_eth_to_usd_rate): # price: int256 = staticcall self.price_feed.latestAnswer() ``` * **After (Immutable Variable):** ```vyper # Constants & Immutables # ... PRICE_FEED: public(immutable(AggregatorV3Interface)) # ... @deploy def __init__(price_feed_address: address): # Parameter renamed for clarity # ... PRICE_FEED = AggregatorV3Interface(price_feed_address) # Set WITHOUT 'self.' # ... # Accessed later as: price: int256 = staticcall PRICE_FEED.latestAnswer() # No 'self.' ``` Here, the variable is declared with `immutable(AggregatorV3Interface)`. Inside `__init__`, it's assigned directly (`PRICE_FEED = ...`), and later accessed directly (`PRICE_FEED`). **Example 2: Refactoring `owner`** The contract `owner` is set to the deployer's address (`msg.sender`) in the constructor. If the design intends for the owner to be fixed permanently, `immutable` is appropriate. * **Before (Storage Variable):** ```vyper owner: public(address) # ... @deploy def __init__(price_feed: address): # ... self.owner = msg.sender # ... # Accessed later as: # assert msg.sender == self.owner, "Not the contract owner!" # send(self.owner, self.balance) ``` * **After (Immutable Variable):** ```vyper # Constants & Immutables # ... OWNER: public(immutable(address)) # ... @deploy def __init__(price_feed_address: address): # ... OWNER = msg.sender # Set WITHOUT 'self.' # ... # Accessed later as: assert msg.sender == OWNER, "Not the contract owner!" # No 'self.' send(OWNER, self.balance) # No 'self.' for owner ``` The pattern is the same: declare with `immutable`, assign in `__init__` without `self.`, access later without `self.`. ### When to Stick with Storage Variables Not all variables can be made `constant` or `immutable`. Variables whose values *must* change during the lifetime of the contract *after* deployment need to remain as standard storage variables. In our example, `funders` (a dynamic array tracking who sent funds) and `funder_to_amount_funded` (a hash map tracking amounts) are modified each time someone sends coffee money and potentially when funds are withdrawn. They require mutability post-deployment. * **Storage Variables (Necessary):** ```vyper # Storage funders: public(DynArray[address, 1000]) funder_to_amount_funded: public(HashMap[address, uint256]) # Accessed using 'self.': # self.funders.append(msg.sender) # self.funder_to_amount_funded[msg.sender] += msg.value ``` These variables must remain as storage variables, declared without `constant` or `immutable`, and accessed using the `self.` prefix. ### Key Takeaways for Optimization * **Identify Fixed Values:** Analyze your contract variables. If a value is known at compile time and never changes, use `constant`. * **Identify Set-Once Values:** If a value is determined during deployment (in `__init__`) and never changes afterward, use `immutable`. * **Use Storage Sparingly:** Reserve standard storage variables for data that truly needs to be mutable after the contract is deployed. * **Follow Conventions:** Use `ALL_CAPS_WITH_UNDERSCORES` for `constant` and `immutable` variable names. * **Access Correctly:** Remember that `constant` and `immutable` variables are accessed directly by name, while storage variables and contract functions require the `self.` prefix. By carefully choosing between `constant`, `immutable`, and standard storage variables, you can significantly reduce the gas costs associated with deploying and interacting with your Vyper smart contracts, making them more efficient and cost-effective for users.
constant
and immutable
When writing Vyper smart contracts, efficiency is key. One major area for optimization lies in how you handle contract variables. By default, variables declared at the contract level are Storage Variables. While flexible, they come with a significant drawback: interacting with them costs considerable gas.
Storage variables, like minimum_usd
, price_feed
, or owner
in our example buy_me_a_coffee.vy
contract, persist on the blockchain's state. Every time your contract reads from (SLOAD
) or writes to (SSTORE
) these variables, it incurs gas fees. These operations are among the most expensive in the Ethereum Virtual Machine (EVM).
If a variable's value doesn't need to change after being initially set, using a storage variable is often unnecessarily expensive. Vyper provides two more gas-efficient alternatives: constant
and immutable
variables. Our goal is to refactor our contract to use these where appropriate, thereby reducing deployment and runtime gas costs.
A constant
variable is a value that is fixed and known before the contract is even compiled. Think of it as a hardcoded value baked directly into the contract's bytecode.
Key Characteristics:
Value: Determined at compile time.
Mutability: Cannot ever be changed after declaration.
Gas Efficiency: Extremely gas-efficient, as reading the value doesn't require accessing storage.
Naming Convention: By convention, constants are named using ALL_CAPS_WITH_UNDERSCORES
.
Access: Accessed directly by their name, without the self.
prefix.
Example: Refactoring minimum_usd
In our contract, minimum_usd
is set to the equivalent of $5 in wei within the constructor and never changes. Since this value (5 ether) is known before we deploy, it's ideal for a constant
.
Before (Storage Variable):
After (Constant Variable):
Notice how the value is assigned directly in the declaration line using constant(uint256)) = ...
. The assignment is removed from the __init__
function, and subsequent references use MINIMUM_USD
directly.
An immutable
variable is a value that is set exactly once, during contract deployment within the constructor (__init__
function), and cannot be changed thereafter. Like constants, their values are embedded in the contract's deployed code rather than storage slots, making them much cheaper to read than storage variables.
Key Characteristics:
Value: Determined at deployment time (within the __init__
function).
Mutability: Cannot be changed after the constructor finishes execution.
Gas Efficiency: Significantly more gas-efficient than storage variables, though slightly less so than constants because the value isn't fixed until deployment.
Naming Convention: Also uses ALL_CAPS_WITH_UNDERSCORES
.
Access: Assigned in the constructor without self.
and accessed later directly by name, also without the self.
prefix.
Example 1: Refactoring price_feed
The price_feed
address is provided as an argument to the constructor and set once. It needs to remain fixed for the contract's lifetime.
Why not constant
? The specific price feed address might differ depending on the network (e.g., Sepolia testnet vs. Ethereum mainnet). Since the value isn't universally known at compile time, it can't be constant
.
Why immutable
? It's set once based on a deployment parameter, making it a perfect fit for immutable
.
Before (Storage Variable):
After (Immutable Variable):
Here, the variable is declared with immutable(AggregatorV3Interface)
. Inside __init__
, it's assigned directly (PRICE_FEED = ...
), and later accessed directly (PRICE_FEED
).
Example 2: Refactoring owner
The contract owner
is set to the deployer's address (msg.sender
) in the constructor. If the design intends for the owner to be fixed permanently, immutable
is appropriate.
Before (Storage Variable):
After (Immutable Variable):
The pattern is the same: declare with immutable
, assign in __init__
without self.
, access later without self.
.
Not all variables can be made constant
or immutable
. Variables whose values must change during the lifetime of the contract after deployment need to remain as standard storage variables.
In our example, funders
(a dynamic array tracking who sent funds) and funder_to_amount_funded
(a hash map tracking amounts) are modified each time someone sends coffee money and potentially when funds are withdrawn. They require mutability post-deployment.
Storage Variables (Necessary):
These variables must remain as storage variables, declared without constant
or immutable
, and accessed using the self.
prefix.
Identify Fixed Values: Analyze your contract variables. If a value is known at compile time and never changes, use constant
.
Identify Set-Once Values: If a value is determined during deployment (in __init__
) and never changes afterward, use immutable
.
Use Storage Sparingly: Reserve standard storage variables for data that truly needs to be mutable after the contract is deployed.
Follow Conventions: Use ALL_CAPS_WITH_UNDERSCORES
for constant
and immutable
variable names.
Access Correctly: Remember that constant
and immutable
variables are accessed directly by name, while storage variables and contract functions require the self.
prefix.
By carefully choosing between constant
, immutable
, and standard storage variables, you can significantly reduce the gas costs associated with deploying and interacting with your Vyper smart contracts, making them more efficient and cost-effective for users.
A practical exploration to Reducing Gas Costs in Vyper: Using `constant` and `immutable` - Discover the gas-saving benefits of using `constant` (compile-time) and `immutable` (deploy-time) variables over standard storage in Vyper. Learn to refactor your smart contracts for improved efficiency and lower deployment/runtime costs.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
The basics of blockchain transactions, how to send and receive money on a blockchain network.
How to write Python based smart contracts using Vyper.
How to read and understand Vyper smart contracts.
Vyper data structures, arrays, structs, hash maps.
How to build a smart contract application and deploy on ZKsync with Moccasin.
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
On-chain Data Analyst
$59,000 - $139,000 (avg. salary)
DeFi Developer
$75,000 - $200,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Last updated on June 11, 2025
Duration: 2h 08min
Duration: 2h 32min
Duration: 24min
Course Overview
About the course
The basics of blockchain transactions, how to send and receive money on a blockchain network.
How to write Python based smart contracts using Vyper.
How to read and understand Vyper smart contracts.
Vyper data structures, arrays, structs, hash maps.
How to build a smart contract application and deploy on ZKsync with Moccasin.
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
On-chain Data Analyst
$59,000 - $139,000 (avg. salary)
DeFi Developer
$75,000 - $200,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Last updated on June 11, 2025