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 SignatureBasedPaymaster contract #22

Merged
merged 5 commits into from
Mar 13, 2024
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 @@ -11,6 +11,7 @@ This repository contains several example Paymaster Smart Contracts that cover mo
- 🎫 **[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.
- ✍🏻 **[SignatureBased Paymaster](./contracts/paymasters/SignatureBasedPaymaster.sol)**: Pays fees for accounts that provides valid signatures.

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
137 changes: 137 additions & 0 deletions contracts/contracts/paymasters/SignatureBasedPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.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";

/// @notice This smart contract pays the gas fees on behalf of users that provide valid signature from the signer.
/// @dev This contract is controlled by an owner, who can update the signer, cancel a user's nonce and withdraw funds from contract.
contract SignatureBasedPaymaster is IPaymaster, Ownable, EIP712 {
using ECDSA for bytes32;
// Note - EIP712 Domain compliance typehash. TYPES should exactly match while signing signature to avoid signature failure.
bytes32 public constant SIGNATURE_TYPEHASH = keccak256(
"SignatureBasedPaymaster(address userAddress,uint256 lastTimestamp,uint256 nonces)"
);
// All signatures should be validated based on signer
address public signer;
// Mapping user => nonce to guard against signature re-play attack.
mapping(address => uint256) public nonces;

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

/// @param _signer Sets the signer to validate against signatures
/// @dev Changes in EIP712 constructor arguments - "name","version" would update domainSeparator which should be taken into considertion while signing.
constructor(address _signer) EIP712("SignatureBasedPaymaster","1") {
hoshiyari420 marked this conversation as resolved.
Show resolved Hide resolved
require(_signer != address(0), "Signer cannot be address(0)");
// Owner can be signer too.
signer = _signer;
}

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.general.selector) {
// Note - We first need to decode innerInputs data to bytes.
(bytes memory innerInputs) = abi.decode(
_transaction.paymasterInput[4:],
(bytes)
);
// Note - Decode the innerInputs as per encoding. Here, we have encoded lastTimestamp and signature in innerInputs
(uint lastTimestamp, bytes memory sig) = abi.decode(innerInputs,(uint256,bytes));

// Verify signature expiry based on timestamp.
// lastTimestamp is used in signature hash, hence cannot be faked.
require(block.timestamp <= lastTimestamp, "Paymaster: Signature expired");
// Get user address from transaction.from
address userAddress = address(uint160(_transaction.from));
// Generate hash
bytes32 hash = keccak256(abi.encode(SIGNATURE_TYPEHASH, userAddress,lastTimestamp, nonces[userAddress]++));
// EIP712._hashTypedDataV4 hashes with domain separator that includes chain id. Hence prevention to signature replay atttacks.
bytes32 digest = _hashTypedDataV4(hash);
// Revert if signer not matched with recovered address. Reverts on address(0) as well.
require(signer == digest.recover(sig),"Paymaster: Invalid signer");


// 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
}("");
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 {
// Refunds are not supported yet.
}
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 {}

/// @dev Only owner should be able to change signer.
/// @param _signer New signer address
function changeSigner(address _signer) onlyOwner public {
signer = _signer;
}
/// @dev Only owner should be able to update user nonce.
/// @dev There could be a scenario where owner needs to cancel paying gas for a certain user transaction.
/// @param _userAddress user address to update the nonce.
function cancelNonce(address _userAddress) onlyOwner public {
nonces[_userAddress]++;
}

function domainSeparator() public view returns(bytes32) {
return _domainSeparatorV4();
}
}
76 changes: 76 additions & 0 deletions contracts/deploy/signatureBasedPaymaster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Provider, Wallet } from "zksync-web3";
import * as ethers from "ethers";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import { HttpNetworkUserConfig } from "hardhat/types";

// 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 SignatureBasedPaymaster contract...`,
);
// Currently targeting the Sepolia zkSync testnet
const network = hre.userConfig.networks?.zkSyncTestnet;
const provider = new Provider((network as HttpNetworkUserConfig).url);

// The wallet that will deploy the paymaster
// It is assumed that this wallet already has sufficient funds on zkSync
const wallet = new Wallet(PRIVATE_KEY);
const deployer = new Deployer(hre, wallet);

// Deploying the paymaster
const paymasterArtifact = await deployer.loadArtifact(
"SignatureBasedPaymaster",
);
const deploymentFee = await deployer.estimateDeployFee(paymasterArtifact, [wallet.address]);
const parsedFee = ethers.utils.formatEther(deploymentFee.toString());
console.log(`The deployment is estimated to cost ${parsedFee} ETH`);
// Deploy the contract with owner as signer
const paymaster = await deployer.deploy(paymasterArtifact, [wallet.address]);
console.log(`Paymaster address: ${paymaster.address}`);
console.log(`Signer of the contract: ${wallet.address}`);

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

let paymasterBalance = await provider.getBalance(paymaster.address);
// Only verify on live networks
if (
hre.network.name == "zkSyncTestnet" ||
hre.network.name == "zkSyncMainnet"
) {
// Verify contract programmatically
//
// Contract MUST be fully qualified name (e.g. path/sourceName:contractName)
const contractFullyQualifedName =
"contracts/paymasters/SignatureBasedPaymaster.sol:SignatureBasedPaymaster";
const verificationId = await hre.run("verify:verify", {
address: paymaster.address,
contract: contractFullyQualifedName,
constructorArguments: [wallet.address],
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 @@ -34,6 +34,7 @@
"nftGated": "hardhat deploy-zksync --script erc721gatedPaymaster.ts",
"fixedToken": "hardhat deploy-zksync --script erc20fixedPaymaster.ts",
"timeBased": "hardhat deploy-zksync --script timeBasedPaymaster.ts",
"signatureBased":"hardhat deploy-zksync --script signatureBasedPaymaster.ts",
"nft": "hardhat deploy-zksync --script erc721.ts",
"token": "hardhat deploy-zksync --script erc20.ts",
"compile": "hardhat compile",
Expand Down
Loading
Loading