Skip to content

Commit

Permalink
fix: Update transaction type for gas estimation if one is not provide…
Browse files Browse the repository at this point in the history
…d for EIP712 transactions, this fixes paymasters for era-test-node. (#195)
  • Loading branch information
MexicanAce authored Oct 25, 2023
1 parent c8fa0b1 commit 0b4a200
Show file tree
Hide file tree
Showing 14 changed files with 569 additions and 12 deletions.
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.9"
edition = "2018"
authors = ["The Matter Labs Team <[email protected]>"]
homepage = "https://zksync.io/"
Expand Down
33 changes: 33 additions & 0 deletions e2e-tests/contracts/ERC20.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
* @dev This contract is for basic demonstration purposes only. It should not be used in production.
* It is for the convenience of the ERC20FixedPaymaster.sol contract and its corresponding test file.
*/
contract MyERC20 is ERC20 {
uint8 private _decimals;

constructor(
string memory name,
string memory symbol,
uint8 decimals_
) payable ERC20(name, symbol) {
_decimals = decimals_;
}

function mint(address _to, uint256 _amount) public returns (bool) {
_mint(_to, _amount);
return true;
}

function decimals() public view override returns (uint8) {
return _decimals;
}

function burn(address from, uint256 amount) public {
_burn(from, amount);
}
}
129 changes: 129 additions & 0 deletions e2e-tests/contracts/ERC20FixedPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.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 for accounts with balance of a specific ERC20 token. It makes use of the approval-based flow paymaster.
contract ERC20FixedPaymaster is IPaymaster, Ownable {
uint256 constant PRICE_FOR_PAYING_FEES = 1;

address public allowedToken;

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

constructor(address _erc20) {
allowedToken = _erc20;
}

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]
);
if (paymasterInputSelector == IPaymasterFlow.approvalBased.selector) {
// While the transaction data consists of address, uint256 and bytes data,
// the data is not needed for this paymaster
(address token, uint256 amount, bytes memory data) = abi.decode(
_transaction.paymasterInput[4:],
(address, uint256, bytes)
);

// Verify if token is the correct one
require(token == allowedToken, "Invalid token");

// We verify that the user has provided enough allowance
address userAddress = address(uint160(_transaction.from));

address thisAddress = address(this);

uint256 providedAllowance = IERC20(token).allowance(
userAddress,
thisAddress
);
require(
providedAllowance >= PRICE_FOR_PAYING_FEES,
"Min allowance too low"
);

// 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;

try
IERC20(token).transferFrom(userAddress, thisAddress, amount)
{} catch (bytes memory revertReason) {
// If the revert reason is empty or represented by just a function selector,
// we replace the error with a more user-friendly message
if (revertReason.length <= 4) {
revert("Failed to transferFrom from users' account");
} else {
assembly {
revert(add(0x20, revertReason), mload(revertReason))
}
}
}

// The bootloader never returns any data, so it can safely be ignored here.
(bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{
value: requiredETH
}("");
require(
success,
"Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough."
);
} else {
revert("Unsupported paymaster flow");
}
}

function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override onlyBootloader {
}

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

receive() external payable {}

}
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;
}
}
96 changes: 96 additions & 0 deletions e2e-tests/contracts/ERC721GatedPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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 {
}

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
Loading

0 comments on commit 0b4a200

Please sign in to comment.