Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Update transaction type for gas estimation if one is not provided for EIP712 transactions, this fixes paymasters for era-test-node. #195

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"ZKSYNC_HOME": "${workspaceFolder}"
},
"args": [
"--dev-use-local-contracts",
"--dev-system-contracts=local",
"run"
],
"preLaunchTask": "rebuild-contracts",
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "era_test_node"
version = "0.1.0-alpha.5"
version = "0.1.0-alpha.8"
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
edition = "2018"
authors = ["The Matter Labs Team <hello@matterlabs.dev>"]
homepage = "https://zksync.io/"
Expand Down
33 changes: 33 additions & 0 deletions e2e-tests/contracts/ERC721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Importing OpenZeppelin's ERC721 Implementation
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
// Importing OpenZeppelin's Ownable contract to control ownership
import "@openzeppelin/contracts/access/Ownable.sol";

/**
* @dev This contract is for basic demonstration purposes only. It should not be used in production.
* It is for the convenience of the ERC721gatedPaymaster.sol contract and its corresponding test file.
*/
contract MyNFT is ERC721, Ownable {
// Maintains a counter of token IDs for uniqueness
uint256 public tokenCounter;

// A constructor that gives my NFT a name and a symbol
constructor () ERC721 ("MyNFT", "MNFT"){
// Initializes the tokenCounter to 0. Every new token has a unique ID starting from 1
tokenCounter = 0;
}

// Creates an NFT collection, with a unique token ID
function mint(address recipient) public onlyOwner returns (uint256) {
// Increases the tokenCounter by 1 and then mints the token with this new ID
_safeMint(recipient, tokenCounter);

// Increments the token counter for the next token to be minted
tokenCounter = tokenCounter + 1;

return tokenCounter;
}
}
97 changes: 97 additions & 0 deletions e2e-tests/contracts/ERC721GatedPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol";
import {IPaymasterFlow} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol";
import {TransactionHelper, Transaction} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";

import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";

import "@openzeppelin/contracts/access/Ownable.sol";

/// @author Matter Labs
/// @notice This smart contract pays the gas fees on behalf of users that are the owner of a specific NFT asset
contract ERC721GatedPaymaster is IPaymaster, Ownable {
IERC721 private immutable nft_asset;

modifier onlyBootloader() {
require(
msg.sender == BOOTLOADER_FORMAL_ADDRESS,
"Only bootloader can call this method"
);
// Continue execution if called from the bootloader.
_;
}

// The constructor takes the address of the ERC721 contract as an argument.
// The ERC721 contract is the asset that the user must hold in order to use the paymaster.
constructor(address _erc721) {
nft_asset = IERC721(_erc721); // Initialize the ERC721 contract
}

// The gas fees will be paid for by the paymaster if the user is the owner of the required NFT asset.
function validateAndPayForPaymasterTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
)
external
payable
onlyBootloader
returns (bytes4 magic, bytes memory context)
{
// By default we consider the transaction as accepted.
magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;
require(
_transaction.paymasterInput.length >= 4,
"The standard paymaster input must be at least 4 bytes long"
);

bytes4 paymasterInputSelector = bytes4(
_transaction.paymasterInput[0:4]
);

// Use the general paymaster flow
if (paymasterInputSelector == IPaymasterFlow.general.selector) {
address userAddress = address(uint160(_transaction.from));
// Verify if user has the required NFT asset in order to use paymaster
require(
nft_asset.balanceOf(userAddress) > 0,
"User does not hold the required NFT asset and therefore must for their own gas!"
);
// Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit,
// neither paymaster nor account are allowed to access this context variable.
uint256 requiredETH = _transaction.gasLimit *
_transaction.maxFeePerGas;

// The bootloader never returns any data, so it can safely be ignored here.
(bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{
value: requiredETH
}("");
} else {
revert("Invalid paymaster flow");
}
}

function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override onlyBootloader {
// Refunds are not supported yet.
}

function withdraw(address payable _to) external onlyOwner {
// send paymaster funds to the owner
uint256 balance = address(this).balance;
(bool success, ) = _to.call{value: balance}("");
require(success, "Failed to withdraw funds from paymaster.");
}

receive() external payable {}
}
9 changes: 8 additions & 1 deletion e2e-tests/contracts/Greeter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ contract Greeter is Ownable {
return greeting;
}

function setGreeting(string memory _greeting) public onlyOwner {
function setGreeting(string memory _greeting) public {
console.log("setGreeting called");
console.log(_greeting);
emit LogString(string.concat("Greeting is being updated to ", _greeting));
Expand All @@ -27,4 +27,11 @@ contract Greeter is Ownable {
);
greeting = _greeting;
}

function setGreetingByOwner(string memory _greeting) public onlyOwner {
console.log("setGreetingByOwner called");
console.log(_greeting);
emit LogString(string.concat("Greeting is being updated to ", _greeting));
greeting = _greeting;
}
}
1 change: 1 addition & 0 deletions e2e-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@matterlabs/hardhat-zksync-deploy": "^0.6.5",
"@matterlabs/hardhat-zksync-solc": "^0.4.2",
"@matterlabs/prettier-config": "^1.0.3",
"@matterlabs/zksync-contracts": "0.6.1",
"@nomiclabs/hardhat-ethers": "^2.2.3",
"@openzeppelin/contracts": "^4.9.3",
"@types/chai": "^4.3.4",
Expand Down
118 changes: 118 additions & 0 deletions e2e-tests/test/erc721gated.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { expect } from "chai";
import { Wallet, Provider, Contract, utils } from "zksync-web3";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import * as ethers from "ethers";
import * as hre from "hardhat";

import { expectThrowsAsync, getTestProvider } from "../helpers/utils";
import { RichAccounts } from "../helpers/constants";

describe("ERC721GatedPaymaster", function () {
let provider: Provider;
let wallet: Wallet;
let deployer: Deployer;
let nftUserWallet: Wallet;
let paymaster: Contract;
let greeter: Contract;
let erc721: Contract;

before(async function () {
provider = getTestProvider();
wallet = new Wallet(RichAccounts[0].PrivateKey, provider);
deployer = new Deployer(hre, wallet);

// Setup new wallets
nftUserWallet = Wallet.createRandom();
nftUserWallet = new Wallet(nftUserWallet.privateKey, provider);

// Deploy NFT and Paymaster
let artifact = await deployer.loadArtifact("MyNFT");
erc721 = await deployer.deploy(artifact, []);
artifact = await deployer.loadArtifact("ERC721GatedPaymaster");
paymaster = await deployer.deploy(artifact, [erc721.address]);
artifact = await deployer.loadArtifact("Greeter");
greeter = await deployer.deploy(artifact, ["Hi"]);

// Fund Paymaster
await provider.send("hardhat_setBalance", [paymaster.address, ethers.utils.parseEther("10")._hex]);

// Assign NFT to nftUserWallet
const tx = await erc721.mint(nftUserWallet.address);
await tx.wait();
});

async function executeGreetingTransaction(user: Wallet, greeting: string) {
const gasPrice = await provider.getGasPrice();
const paymasterParams = utils.getPaymasterParams(paymaster.address, {
type: "General",
// empty bytes as paymaster does not use innerInput
innerInput: new Uint8Array(),
});

// estimate gasLimit via paymaster
const gasLimit = await greeter.connect(user).estimateGas.setGreeting(greeting, {
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams: paymasterParams,
},
});

const setGreetingTx = await greeter.connect(user).setGreeting(greeting, {
maxPriorityFeePerGas: ethers.BigNumber.from(0),
maxFeePerGas: gasPrice,
gasLimit,
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
paymasterParams,
},
});

await setGreetingTx.wait();
}

it("should not pay for gas fees when user has NFT", async function () {
// Arrange
const initialBalance = await nftUserWallet.getBalance();

// Act
await executeGreetingTransaction(nftUserWallet, "Hello World");

// Assert
expect(await greeter.greet()).to.equal("Hello World");
const newBalance = await nftUserWallet.getBalance();
expect(newBalance).to.eql(initialBalance);
});

it("should require the user to have the NFT", async function () {
// Arrange
let normalUserWallet = Wallet.createRandom();
normalUserWallet = new Wallet(normalUserWallet.privateKey, provider);

// Act
const action = async () => {
await executeGreetingTransaction(normalUserWallet, "Hello World");
};

// Assert
await expectThrowsAsync(action, "User does not hold the required NFT asset and therefore must for their own gas!");
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
});

it("should allow owner to withdraw all funds", async function () {
// Arrange
// Act
const tx = await paymaster.connect(wallet).withdraw(nftUserWallet.address);
await tx.wait();

// Assert
const finalContractBalance = await provider.getBalance(paymaster.address);
expect(finalContractBalance).to.eql(ethers.BigNumber.from(0));
});

it("should prevent non-owners from withdrawing funds", async function () {
const action = async () => {
await paymaster.connect(nftUserWallet).withdraw(nftUserWallet.address);
};

await expectThrowsAsync(action, "Ownable: caller is not the owner");
});
});
2 changes: 1 addition & 1 deletion e2e-tests/test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("Greeter Smart Contract", function () {
const greeter = await deployer.deploy(artifact, ["Hello, world!"]);

// should revert
const tx = await greeter.connect(userWallet).setGreeting("Hola, mundo!");
const tx = await greeter.connect(userWallet).setGreetingByOwner("Hola, mundo!");
await tx.wait();
};

Expand Down
5 changes: 5 additions & 0 deletions e2e-tests/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,11 @@
resolved "https://registry.yarnpkg.com/@matterlabs/prettier-config/-/prettier-config-1.0.3.tgz#3e2eb559c0112bbe9671895f935700dad2a15d38"
integrity sha512-JW7nHREPqEtjBWz3EfxLarkmJBD8vi7Kx/1AQ6eBZnz12eHc1VkOyrc6mpR5ogTf0dOUNXFAfZut+cDe2dn4kQ==

"@matterlabs/zksync-contracts@0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@matterlabs/zksync-contracts/-/zksync-contracts-0.6.1.tgz#39f061959d5890fd0043a2f1ae710f764b172230"
integrity sha512-+hucLw4DhGmTmQlXOTEtpboYCaOm/X2VJcWmnW4abNcOgQXEHX+mTxQrxEfPjIZT0ZE6z5FTUrOK9+RgUZwBMQ==

"@metamask/eth-sig-util@^4.0.0":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz#3ad61f6ea9ad73ba5b19db780d40d9aae5157088"
Expand Down
34 changes: 27 additions & 7 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,19 @@ use zksync_core::api_server::web3::backend_jsonrpc::{
error::into_jsrpc_error, namespaces::eth::EthNamespaceT,
};
use zksync_state::{ReadStorage, StoragePtr, StorageView, WriteStorage};
use zksync_types::vm_trace::Call;
use zksync_types::{
api::{Block, DebugCall, Log, TransactionReceipt, TransactionVariant},
block::legacy_miniblock_hash,
fee::Fee,
get_code_key, get_nonce_key,
l2::L2Tx,
l2::TransactionType,
transaction_request::TransactionRequest,
utils::{
decompose_full_nonce, nonces_to_full_nonce, storage_key_for_eth_balance,
storage_key_for_standard_token_balance,
},
vm_trace::Call,
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
PackedEthSignature, StorageKey, StorageLogQueryType, StorageValue, Transaction,
ACCOUNT_CODE_STORAGE_ADDRESS, EIP_712_TX_TYPE, L2_ETH_TOKEN_ADDRESS, MAX_GAS_PER_PUBDATA_BYTE,
MAX_L2_TX_GAS_LIMIT,
Expand Down Expand Up @@ -401,13 +402,26 @@ impl<S: std::fmt::Debug + ForkSource> InMemoryNodeInner<S> {
&self,
req: zksync_types::transaction_request::CallRequest,
) -> jsonrpc_core::Result<Fee> {
let mut l2_tx = match L2Tx::from_request(req.into(), MAX_TX_SIZE) {
Ok(tx) => tx,
Err(e) => {
let error = Web3Error::SerializationError(e);
return Err(into_jsrpc_error(error));
let mut request_with_gas_per_pubdata_overridden = req;

if let Some(ref mut eip712_meta) = request_with_gas_per_pubdata_overridden.eip712_meta {
if eip712_meta.gas_per_pubdata == U256::zero() {
eip712_meta.gas_per_pubdata = MAX_GAS_PER_PUBDATA_BYTE.into();
}
};
}

let is_eip712 = request_with_gas_per_pubdata_overridden
.eip712_meta
.is_some();

let mut l2_tx =
match L2Tx::from_request(request_with_gas_per_pubdata_overridden.into(), MAX_TX_SIZE) {
Ok(tx) => tx,
Err(e) => {
let error = Web3Error::SerializationError(e);
return Err(into_jsrpc_error(error));
}
};

let tx: Transaction = l2_tx.clone().into();
let fair_l2_gas_price = L2_GAS_PRICE;
Expand Down Expand Up @@ -435,6 +449,12 @@ impl<S: std::fmt::Debug + ForkSource> InMemoryNodeInner<S> {
l2_tx.common_data.signature[64] = 27;
}

// The user may not include the proper transaction type during the estimation of
// the gas fee. However, it is needed for the bootloader checks to pass properly.
if is_eip712 {
l2_tx.common_data.transaction_type = TransactionType::EIP712Transaction;
}

l2_tx.common_data.fee.gas_per_pubdata_limit = MAX_GAS_PER_PUBDATA_BYTE.into();
l2_tx.common_data.fee.max_fee_per_gas = base_fee.into();
l2_tx.common_data.fee.max_priority_fee_per_gas = base_fee.into();
Expand Down