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

feat: add time based paymaster #16

Merged
merged 7 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This repository contains several example Paymaster Smart Contracts that cover mo
- 📜 **[Allowlist Paymaster](./contracts/paymasters/AllowlistPaymaster.sol)**: Pays fees for accounts present in a predefined list (the "allow list").
- 🎫 **[ERC20 Fixed Paymaster](./contracts/paymasters/ERC20fixedPaymaster.sol)**: Accepts a fixed amount of a specific ERC20 token in exchange for covering gas fees. It only services accounts that have a balance of the specified token.
- 🎨 **[ERC721 Gated Paymaster](./contracts/paymasters/ERC721gatedPaymaster.sol)**: Pays fees for accounts that hold a specific ERC721 token (NFT).
- 🎨 **[TimeBased Paymaster](./contracts/paymasters/TimeBasedPaymaster.sol)**: Pays fees for accounts that interact with contract at specific times.

Stay tuned! More Paymaster examples will be added over time. This project was scaffolded with [zksync-cli](https://github.com/matter-labs/zksync-cli).

Expand Down
79 changes: 79 additions & 0 deletions contracts/contracts/paymasters/TimeBasedPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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 contract validates based on the time of the transaction.
contract TimeBasedPaymaster is IPaymaster, Ownable {
modifier onlyBootloader() {
require(
msg.sender == BOOTLOADER_FORMAL_ADDRESS,
"Only bootloader can call this method"
);
_;
}

function validateAndPayForPaymasterTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
)
external
payable
onlyBootloader
returns (bytes4 magic, bytes memory context)
{
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.general.selector) {
uint256 startTime = (block.timestamp / 86400) * 86400 + 14 hours;
uint256 endTime = startTime + 10 minutes;

require(
block.timestamp >= startTime && block.timestamp <= endTime,
"Transactions can only be processed between 14:00 - 14:10"
);

uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas;

(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 in paymasterParams.");
}
}

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 {
uint256 balance = address(this).balance;
(bool success, ) = _to.call{value: balance}("");
require(success, "Failed to withdraw funds from paymaster.");
}

receive() external payable {}
}
59 changes: 59 additions & 0 deletions contracts/deploy/timeBasedPaymaster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Provider, Wallet } from "zksync-web3";
import * as ethers from "ethers";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";

// load env file
import dotenv from "dotenv";
dotenv.config();

// load wallet private key from env file
const PRIVATE_KEY = process.env.WALLET_PRIVATE_KEY || "";

if (!PRIVATE_KEY)
throw "⛔️ Private key not detected! Add it to the .env file!";

export default async function (hre: HardhatRuntimeEnvironment) {
console.log(`Running deploy script for the TimeBasedPaymaster contract...`);
const provider = new Provider("https://testnet.era.zksync.dev");

const wallet = new Wallet(PRIVATE_KEY);
const deployer = new Deployer(hre, wallet);

const paymasterArtifact = await deployer.loadArtifact("TimeBasedPaymaster");
const deploymentFee = await deployer.estimateDeployFee(paymasterArtifact, []);
const parsedFee = ethers.utils.formatEther(deploymentFee.toString());
console.log(`The deployment is estimated to cost ${parsedFee} ETH`);
// Deploy the contract
const paymaster = await deployer.deploy(paymasterArtifact, []);
console.log(`Paymaster address: ${paymaster.address}`);
console.log("constructor args:" + paymaster.interface.encodeDeploy([]));

console.log("Funding paymaster with ETH");
// Supplying paymaster with ETH
await (
await deployer.zkWallet.sendTransaction({
to: paymaster.address,
value: ethers.utils.parseEther("0.001"),
})
).wait();

let paymasterBalance = await provider.getBalance(paymaster.address);
console.log(`Paymaster ETH balance is now ${paymasterBalance.toString()}`);

// Verify contract programmatically
//
// Contract MUST be fully qualified name (e.g. path/sourceName:contractName)
const contractFullyQualifedName =
"contracts/paymasters/TimeBasedPaymaster.sol:TimeBasedPaymaster";
const verificationId = await hre.run("verify:verify", {
address: paymaster.address,
contract: contractFullyQualifedName,
constructorArguments: [],
bytecode: paymasterArtifact.bytecode,
});
console.log(
`${contractFullyQualifedName} verified! VerificationId: ${verificationId}`,
);
console.log(`Done!`);
}
1 change: 1 addition & 0 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"allowList": "hardhat deploy-zksync --script allowListPaymaster.ts",
"nftGated": "hardhat deploy-zksync --script erc721gatedPaymaster.ts",
"fixedToken": "hardhat deploy-zksync --script erc20fixedPaymaster.ts",
"timeBased": "hardhat deploy-zksync --script timeBasedPaymaster.ts",
"nft": "hardhat deploy-zksync --script erc721.ts",
"token": "hardhat deploy-zksync --script erc20.ts",
"compile": "hardhat compile",
Expand Down
94 changes: 94 additions & 0 deletions contracts/test/timeBased.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { expect } from "chai";
import { Wallet, Provider, Contract, utils } from "zksync-web3";
import hardhatConfig from "../hardhat.config";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import * as ethers from "ethers";

import { deployContract, fundAccount, setupDeployer } from "./utils";

import dotenv from "dotenv";
dotenv.config();

const PRIVATE_KEY =
process.env.WALLET_PRIVATE_KEY ||
"0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110";

describe("TimeBasedPaymaster", function () {
let provider: Provider;
let wallet: Wallet;
let deployer: Deployer;
let userWallet: Wallet;
let paymaster: Contract;
let greeter: Contract;

before(async function () {
const deployUrl = hardhatConfig.networks.zkSyncTestnet.url;
[provider, wallet, deployer] = setupDeployer(deployUrl, PRIVATE_KEY);
userWallet = Wallet.createRandom();
console.log(`User wallet's address: ${userWallet.address}`);
userWallet = new Wallet(userWallet.privateKey, provider);
paymaster = await deployContract(deployer, "TimeBasedPaymaster", []);
greeter = await deployContract(deployer, "Greeter", ["Hi"]);
await fundAccount(wallet, paymaster.address, "3");
});

async function executeGreetingTransaction(user: Wallet) {
const gasPrice = await provider.getGasPrice();

const paymasterParams = utils.getPaymasterParams(paymaster.address, {
type: "General",
innerInput: new Uint8Array(),
});

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

await setGreetingTx.wait();
}

it("should cost the user no gas during the time window", async function () {
// Arrange
const currentDate = new Date();
currentDate.setUTCHours(14);
currentDate.setUTCMinutes(1);
currentDate.setUTCSeconds(0);
currentDate.setUTCMilliseconds(0);
const targetTime = Math.floor(currentDate.getTime() / 1000);
await provider.send("evm_setNextBlockTimestamp", [targetTime]);

// Act
const initialBalance = await userWallet.getBalance();
await executeGreetingTransaction(userWallet);
await provider.send("evm_mine", []);
dutterbutter marked this conversation as resolved.
Show resolved Hide resolved
const newBalance = await userWallet.getBalance();

// Assert
expect(newBalance.toString()).to.equal(initialBalance.toString());
expect(await greeter.greet()).to.equal("Hola, mundo!");
});

it("should fail due to Paymaster validation error outside the time window", async function () {
// Arrange
let errorOccurred = false;

// Act
try {
await executeGreetingTransaction(wallet);
} catch (error) {
errorOccurred = true;
expect(error.message).to.include("Paymaster validation error");
}

// Assert
expect(errorOccurred).to.be.true;
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"deploy:allowList": "yarn workspace contracts allowList",
"deploy:nftGated": "yarn workspace contracts nftGated",
"deploy:fixedToken": "yarn workspace contracts fixedToken",
"deploy:timeBased": "yarn workspace contracts timeBased",
"deploy:nft": "yarn workspace contracts nft",
"deploy:token": "yarn workspace contracts token",
"serve:ui": "yarn workspace frontend dev",
Expand Down