diff --git a/content/tutorials/max-gas-pub-data/10.index.md b/content/tutorials/max-gas-pub-data/10.index.md
new file mode 100644
index 00000000..ebf72935
--- /dev/null
+++ b/content/tutorials/max-gas-pub-data/10.index.md
@@ -0,0 +1,425 @@
+# Understanding Pubdata Costs in the ZKsync Era
+
+ZKsync Era, as a state diff-based ZK rollup, introduces a unique approach to charging for pubdata.
+
+Unlike traditional rollups that publish transaction data (calldata), ZKsync Era publishes state changes as pubdata. This includes:
+
+- Modified storage slots
+- Smart contract bytecodes
+- L2 → L1 messages and logs
+
+The charge is calculated as `pubdata_bytes_published * gas_per_pubdata` directly from the context of the execution.
+This approach allows for more efficient handling of applications that frequently modify the same storage slot, such as oracles.
+These applications can update storage slots multiple times while maintaining a constant footprint on L1 pubdata.
+
+## Key Differences from Ethereum
+
+Before diving into ZKsync Era's model, it's important to understand key differences from Ethereum's gas model:
+
+1. **Volatile L1 Gas Prices**: L2 transaction prices depend on fluctuating L1 gas prices, making it impossible to use hardcoded gas costs.
+2. **Zero-Knowledge Proofs**: As a zkRollup, ZKsync Era must prove every operation with zero-knowledge proofs, adding complexity to the fee structure.
+3. **Separation of Computation and Pubdata Costs**: Unlike Ethereum, where all costs are represented by gas,
+ZKsync Era distinguishes between computational costs and pubdata costs.
+
+## ZKsync Era's Approach to Pubdata Costs
+
+ZKsync Era has developed a unique solution for charging pubdata costs in a state diff rollup:
+
+1. **Dynamic Pricing**: Pubdata costs are calculated dynamically based on current L1 gas prices.
+2. **Post-Execution Pubdata Charging**: Users are charged for pubdata based on a counter that tracks pubdata usage during transaction execution.
+This includes storage writes, bytecode deployment, and L2->L1 messages. The final cost depends on the amount of pubdata used and the gas price
+per pubdata unit, which is determined at the time of execution.
+3. **Efficient for Repeated Operations**: Applications that repeatedly modify the same storage slots benefit from this model,
+as they're not charged for each individual update.
+
+## How ZKsync Era Charges for Pubdata (Process)
+
+ZKsync Era employs a sophisticated method for charging pubdata costs, addressing several challenges inherent to state diff-based rollups:
+
+- **Post-Charging Approach**: Instead of pre-charging for pubdata (which is impossible due to the nature of state diffs),
+ZKsync Era uses a post-charging mechanism.
+- **Pubdata Counter**: ZKsync Era uses a pubdata counter that tracks potential pubdata usage during transaction execution.
+This counter is modified by the operator for storage writes (which can be positive or negative) and incremented by system contracts for L1 data publication.
+The counter can revert along with other state changes. The final value of this counter, combined with the gas price per pubdata unit, determines the
+pubdata cost of the transaction.
+ - The system maintains a counter that tracks how much pubdata has been spent during a transaction.
+- **Execution Process**:
+ 1. The current pubdata spent is recorded as basePubdataSpent.
+ 2. Transaction validation is executed.
+ 3. The system checks if (getPubdataSpent() - basePubdataSpent) * gasPerPubdata <= gasLeftAfterValidation.
+ 4. If the check fails, the transaction is rejected (not included in the block). → Note this one.
+ 5. The main transaction is executed.
+ 6. The pubdata check is repeated. If it fails at this stage, the transaction is reverted (user pays for computation but no state changes occur).
+ 7. If a paymaster is used, steps d-f are repeated for the paymaster's postTransaction method.
+- **Pubdata Counter Modifications**:
+ - **Storage writes:** The operator specifies the increment for the pubdata counter. Note that this value can be negative if, for example,
+ the storage diff is being reversed, such as in the case of a reentrancy guard.
+ - **Publishing bytes to L1:** The counter is incremented by the number of bytes published.
+ - **Transaction revert:** The pubdata counter value reverts along with storage and events.
+- **Advantages of Post-Charging**:
+ - Removes unnecessary overhead.
+ - Decouples execution gas from data availability gas.
+ - Eliminates caps on `gasPerPubdata.`
+
+## Implications for Users and Developers
+
+1. **Cost Predictability**: While costs may vary with L1 gas prices, users can estimate costs based on the state changes their transactions will cause.
+2. **Optimization Opportunities**: Developers can optimize their applications to minimize state changes, potentially reducing users' costs.
+3. **Efficient for Certain Use Cases**: Applications like oracles or high-frequency trading platforms may find this model particularly cost-effective.
+4. **Transaction Behavior**: Users should be aware that transactions may be rejected or reverted based on
+pubdata costs, even if they seem to have sufficient gas for execution.
+5. **Flexible Pricing**: The absence of hard caps on `gasPerPubdata` allows for more flexible pricing models.
+
+The following section provides practical insights into measuring gas costs and setting the `DEFAULT_GAS_PER_PUBDATA_LIMIT` in the ZKsync era.
+
+## Measuring Gas Costs and Setting Different Values for Pubdata Gas Limit
+
+This guide demonstrates how to measure gas costs and set the `DEFAULT_GAS_PER_PUBDATA_LIMIT` in the ZKsync Era using a practical example:
+a ZKFest Voting Contract. **What is Max Gas Per Pubdata?** Max gas per pubdata is a value attached to each transaction on ZKsync Era,
+representing the maximum amount of gas a user is willing to pay for each byte of pubdata (public data) published on Ethereum.
+
+**Key points:**
+
+1. Default value: 50,000 gas per pubdata byte
+2. Can be customized per transaction; in this case, we're testing three cases
+3. Affects transaction success and cost
+
+## Objective
+
+We'll deploy and interact with a smart contract using various gas settings to understand how different parameters affect transaction execution and
+costs on ZKsync Era.
+
+## Running the Experiment
+
+1. Set up your environment by creating a new project with ZKsync CLI
+2. Create the ZKFestVoting contract and the deploy script based on the code below
+3. Analyze the output for each scenario, paying attention to:
+ - Successful transactions and their gas usage
+ - Rejected transactions and their error messages
+
+## Step 1: Smart Contract (ZKFestVoting.sol)
+
+```solidity [ZKFestVoting.sol]
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.17;
+
+contract ZKFestVoting {
+ enum Stage { Culture, DeFi, ElasticChain }
+ mapping(address => uint8) public participation;
+ event Voted(address indexed voter, Stage stage);
+
+ function vote(string memory stageName) external {
+ uint256 stageIndex = getStageIndex(stageName);
+ require(stageIndex < 3, "Invalid stage");
+ uint256 stageBit = 1 << stageIndex;
+ require((participation[msg.sender] & stageBit) == 0, "Already voted for this stage");
+ participation[msg.sender] |= uint8(stageBit);
+ emit Voted(msg.sender, Stage(stageIndex));
+ }
+
+ // Helper functions omitted for brevity
+}
+```
+
+
+Full code for ZKFestVoting.sol
+
+
+ ```solidity
+ // SPDX-License-Identifier: MIT
+ pragma solidity ^0.8.17;
+
+ contract ZKFestVoting {
+ // Enum: A user-defined type that consts of a set of named constants, we're defining the stages of ZKFest
+ enum Stage { Culture, DeFi, ElasticChain }
+
+ // Mapping: A key-value data structure that allows for efficient data lookup
+ mapping(address => uint8) public participation;
+
+ // Event: A way to emit logs on the blockchain, useful for off-chain applications
+ event Voted(address indexed voter, Stage stage);
+
+ /*
+ Vote function: allows a user to vote for a stage
+ - converts stage name to index
+ - checks for valid state and prevents double voting
+ - uses bit manipulation (1 << stageIndex) to efficiently store votes in single uint8
+ - updates participation using bitwise OR (|=)
+ */
+ function vote(string memory stageName) external {
+ uint256 stageIndex = getStageIndex(stageName);
+ require(stageIndex < 3, "Invalid stage");
+ uint256 stageBit = 1 << stageIndex; // 1 left shifted by stageIndex
+
+ // Check if the user has already voted for the stage, the participation is checking the transaction sender and the stageBit is the stage the user is voting for
+ require((participation[msg.sender] & stageBit) == 0, "Already voted for this stage");
+
+ // we're updating the participation mapping with the stageBit, the |= is the bitwise OR assignment operator, it's used to update the participation mapping with the stageBit
+ participation[msg.sender] |= uint8(stageBit);
+
+ // emit does not store data, it only logs the event, in this case it's logging the voter and the stage
+ emit Voted(msg.sender, Stage(stageIndex));
+ }
+
+ // Helper function to get the index of the stage
+ function getStageIndex(string memory stageName) internal pure returns (uint256) {
+ // the goal of the if statements is to convert the stageName to an index, the keccak256 is a hash function that converts the stageName to a hash,
+ // and we're checking if the hash of the stageName is equal to the hash of the string "Culture"
+ // we're using bytes to convert the stageName to a byte array, because keccak256 expects a byte array
+ if (keccak256(bytes(stageName)) == keccak256(bytes("Culture"))) return 0;
+ if (keccak256(bytes(stageName)) == keccak256(bytes("DeFi"))) return 1;
+ if (keccak256(bytes(stageName)) == keccak256((bytes("ElasticChain")))) return 2;
+ revert("Invalid stage name");
+ }
+
+ function hasVoted(address voter, Stage stage) external view returns (bool) {
+ return (participation[voter] & (1 << uint256(stage))) != 0;
+ // explain in detail what the above line does:
+ // 1. participation[voter] is the bitmask of the voter's votes, bitmask in solidity is a way to store multiple boolean values in a single variable
+ // 2. (1 << uint256(stage)) is the bitmask of the stage, it's a 1 left shifted by the stage index
+ // 3. & is the bitwise AND operator, it's used to check if the voter has voted for the stage
+ // 4. != 0 is used to check if the voter has voted for the stage
+ }
+
+ function voterStages(address voter) external view returns (bool[3] memory) {
+ uint8 participationBits = participation[voter];
+
+ return [
+ participationBits & (1 << 0) != 0, // Check Culture stage
+ participationBits & (1 << 1) != 0, // Check DeFi stage
+ participationBits & (1 << 2) != 0 // Check ElsticChain stage
+ ];
+ }
+ }
+ ```
+
+
+
+
+This contract uses bit manipulation to store voting data efficiently, minimizing storage costs and pubdata usage.
+
+The `ZKFestVoting` contract allows participants to vote for different stages of ZKFest.
+
+**Key features:**
+
+- Supports voting for three stages: Culture, DeFi, and ElasticChain.
+- Prevents double voting for the same stage.
+- Tracks voter participation across all stages.
+
+## Step 2: Deployment Script (deploy.ts)
+
+```tsx
+import { deployContract, getProvider, getWallet } from "./utils";
+import { Deployer } from "@matterlabs/hardhat-zksync";
+import * as hre from "hardhat";
+import { ethers } from "ethers";
+import { utils } from "zksync-ethers";
+
+export default async function () {
+ const wallet = getWallet();
+ const provider = getProvider();
+ const deployer = new Deployer(hre, wallet);
+
+ // Deploy the contract
+ const artifact = await deployer.loadArtifact("ZKFestVoting");
+ const deploymentFee = await deployer.estimateDeployFee(artifact, []);
+ console.log(`Estimated deployment fee: ${ethers.formatEther(deploymentFee)} ETH`);
+
+ const contract = await deployContract("ZKFestVoting", [], {
+ wallet: wallet,
+ noVerify: false
+ });
+
+ const contractAddress = await contract.getAddress();
+ console.log(`ZKFestVoting deployed to: ${contractAddress}`);
+
+ // Test different scenarios
+ const stageNames = ["Culture", "DeFi", "ElasticChain"];
+ let currentStageIndex = 0;
+
+ const sendAndExplainTx = async (gasPerPubdata: string | number, gasLimit: string | number) => {
+ // Implementation details omitted for brevity
+ }
+
+ const createCustomTx = async (gasPerPubdata: string | number, gasLimit: string | number, stageName: string) => {
+ // Implementation details omitted for brevity
+ }
+
+ // Test different scenarios
+ await sendAndExplainTx(utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, "2000000");
+ await sendAndExplainTx("100", "2000000"); // Very low gasPerPubdata
+ await sendAndExplainTx(utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, "100000000"); // Very high gasLimit
+}
+```
+
+
+Full code for deploy.ts script
+
+
+ ```tsx
+ import { deployContract, getProvider, getWallet } from "./utils";
+ import { Deployer } from "@matterlabs/hardhat-zksync";
+
+ import * as hre from "hardhat";
+ import { ethers } from "ethers";
+ import { utils } from "zksync-ethers";
+
+ export default async function () {
+ console.log("Deploying ZKFestVoting contract...");
+
+ // Get the wallet to deploy from
+ const wallet = getWallet();
+ const provider = getProvider();
+ console.log(`Deploying from address: ${wallet.address}`);
+
+ const deployer = new Deployer(hre, wallet);
+
+ try {
+ // Deploy the contract
+ // Note: We're not passing any constructor arguments here
+ const artifact = await deployer.loadArtifact("ZKFestVoting");
+ const deploymentFee = await deployer.estimateDeployFee(artifact, []);
+ console.log(`Estimated deployment fee: ${ethers.formatEther(deploymentFee)} ETH`);
+
+ const contract = await deployContract("ZKFestVoting", [], {
+ wallet: wallet,
+ // Set to false if you want the contract to be verified automatically
+ noVerify: false
+ });
+
+ const contractAddress = await contract.getAddress();
+ console.log(`ZKFestVoting deployed to: ${contractAddress}`);
+
+ // Test different scenarios
+ const stageNames = ["Culture", "DeFi", "ElasticChain"];
+ let currentStageIndex = 0;
+
+ const sendAndExplainTx = async (gasPerPubdata: string | number, gasLimit: string | number) => {
+ console.log(`\nTesting with gasPerPubdata: ${gasPerPubdata}, gasLimit: ${gasLimit}`);
+ try {
+ const stageName = stageNames[currentStageIndex];
+ currentStageIndex++;
+
+ const customTx = await createCustomTx(gasPerPubdata, gasLimit, stageName);
+ console.log(`Voting for stage: ${stageName}`);
+
+ console.log("Custom transaction created, attempting to send...");
+
+ const txResponse = await wallet.sendTransaction(customTx);
+ console.log(`Transaction sent. Hash: ${txResponse.hash}`);
+ console.log("Transaction sent, waiting for confirmation...");
+
+ const timeoutPromise = new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("Transaction confirmation timeout")), 60000) // 60 second timeout
+ );
+
+ // const receipt = await txResponse.wait();
+ const receiptPromise = txResponse.wait();
+
+ // const receipt = await Promise.race([receiptPromise, timeoutPromise]);
+ const receipt = await Promise.race([receiptPromise, timeoutPromise]) as ethers.TransactionReceipt;
+
+ console.log("Transaction successful!");
+ console.log(`Gas used: ${receipt.gasUsed}`);
+ } catch (error) {
+ console.log("Transaction failed!");
+ console.error("Error details:", error);
+ if (error instanceof Error) {
+ console.error("Error message:", error.message);
+ }
+ }
+ }
+
+ const createCustomTx = async (gasPerPubdata: string | number, gasLimit: string | number, stageName: string) => {
+
+ // const voteFunctionData = contract.interface.encodeFunctionData("vote", [0]); // Vote for Culture stage
+ const voteFunctionData = contract.interface.encodeFunctionData("vote", [stageName]); // Vote for Culture stage
+ // we add stageName as a string to the array because the encodeFunctionData expects an array
+
+ const gasPrice = await provider.getGasPrice();
+
+ let customTx = {
+ to: contractAddress,
+ from: wallet.address,
+ data: voteFunctionData,
+ gasLimit: ethers.getBigInt(gasLimit),
+ gasPrice: gasPrice,
+ chainId: (await provider.getNetwork()).chainId,
+ nonce: await provider.getTransactionCount(wallet.address),
+ type: 113,
+ customData: {
+ gasPerPubdata: ethers.getBigInt(gasPerPubdata)
+ },
+ value: ethers.getBigInt(0),
+ };
+
+ return customTx;
+ }
+
+ // Test different scenarios
+ await sendAndExplainTx(utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, "2000000");
+ await sendAndExplainTx("100", "2000000"); // Very low gasPerPubdata,
+ await sendAndExplainTx(utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, "100000000"); // Very high gasLimit
+
+ // // Get and log the transaction receipt for the vote
+ // const receipt = await deployContract
+ } catch (error) {
+ console.error("Deployment or interaction failed: ", error);
+ process.exitCode = 1;
+ }
+ }
+ ```
+
+
+
+
+This deployment script serves as a practical example of how to interact with ZKsync Era, showcasing the impact
+of different gas settings on transaction execution and costs.
+
+This script deploys the contract and tests scenarios with varying `gasPerPubdata` and `gasLimit` values.
+
+**The script includes:**
+
+1. Deploy the `ZKFestVoting` contract.
+2. Execute a series of test votes with different `gasPerPubdata` and `gasLimit` settings.
+3. Log the results of each transaction, including gas usage and any errors encountered.
+
+## How It Works
+
+1. **Transaction Submission**: When sending a transaction, specify the max gas per pubdata.
+
+2. **Execution**: ZKsync Era executes the transaction and calculates the actual pubdata cost.
+
+3. **Comparison**: The actual cost is compared against your specified max value.
+
+4. **Outcome**:
+
+- If actual cost ≤ specified max: Transaction succeeds
+- If actual cost > specified max: Transaction is rejected
+ - Which in this case, it's happening with the very low gasPerPubdata example `await sendAndExplainTx("100", "2000000");`
+
+## Key Aspects of Gas Measurement and DEFAULT_GAS_PER_PUBDATA_LIMIT
+
+- **Deployment Fee Estimation**:
+The script estimates the deployment fee before deploying the contract, giving insight into the initial cost.
+- **Custom Transaction Creation**:
+The `createCustomTx` function allows custom `gasPerPubdata` and `gasLimit` values to be set for each transaction.
+- **Testing Different Scenarios**:
+ - failsDefault `DEFAULT_GAS_PER_PUBDATA_LIMIT` with a moderate `gasLimit`
+ - Very low `gasPerPubdata` to see how it affects transaction execution
+ - Default `DEFAULT_GAS_PER_PUBDATA_LIMIT` with a very high `gasLimit`
+- **Transaction Monitoring**:
+The script logs transaction details, including gas used and any errors encountered.
+- **Error Handling**:
+Comprehensive error handling and logging provide insights into transaction failures.
+
+## Key Takeaways
+
+1. The `DEFAULT_GAS_PER_PUBDATA_LIMIT` (accessed via zksync-ethers library on `utils.DEFAULT_GAS_PER_PUBDATA_LIMIT`)
+serves for setting default pubdata gas limits.
+2. Very low gas per pubdata values may lead to transaction rejection due to insufficient pubdata gas.
+ - In this case, the transaction won’t be included in the blockchain, and the user is not charged anything.
+3. Extremely high `gasLimit` values may not necessarily improve transaction success rates but could lead to higher upfront costs.
+4. The optimal values depend on the specific operation and current network conditions.
+
+By experimenting with these parameters, developers can find the right balance between transaction success rate cost efficiency for their ZKsync Era applications.
diff --git a/content/tutorials/max-gas-pub-data/_dir.yml b/content/tutorials/max-gas-pub-data/_dir.yml
new file mode 100644
index 00000000..7d5527f3
--- /dev/null
+++ b/content/tutorials/max-gas-pub-data/_dir.yml
@@ -0,0 +1,23 @@
+title: How Max Gas Per Pubdata Works on ZKsync Era
+featured: false
+authors:
+ - name: MatterLabs
+ url: https://matter-labs.io
+ avatar: https://avatars.githubusercontent.com/u/42489169?s=200&v=4
+github_repo: https://github.com/ZKsync-Community-Hub
+tags:
+ - ZKsync Era Pubdata
+ - Fee Model
+summary: ZKsync Era Pubdata Cost Model Explained
+description:
+ This guide aims to explain how ZKSync Era handles pubdata costs, highlighting the differences from Ethereum's gas
+ model and exploring the implications for users and developers.
+what_you_will_learn:
+ - Understand ZKSync Era's pubdata cost model.
+ - Deploy a voting contract on ZKSync Era.
+ - Experiment with different gas settings on transactions.
+updated: 2024-11-25
+tools:
+ - zksync-cli
+ - hardhat
+ - zksync-ethers