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

chore: updates deps and refactor for evm #25

Merged
merged 9 commits into from
Nov 14, 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
22 changes: 11 additions & 11 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ jobs:
runs-on: ${{ matrix.platform }}

steps:
- name: Checkout Code
uses: actions/checkout@v3

- name: Run Era Test Node
uses: dutterbutter/era-test-node-action@latest
- name: Checkout Code
uses: actions/checkout@v3

- name: Install Dependencies
run: yarn install

- name: Run Tests
run: |
yarn ci:tests
- name: Run Era Test Node
uses: dutterbutter/era-test-node-action@latest

- name: Install Dependencies
run: yarn install

- name: Run Tests
run: |
yarn ci:tests
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ cache
# zksync artifacts
artifacts-zk
cache-zk
deployments-zk

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
Expand Down
225 changes: 115 additions & 110 deletions contracts/contracts/paymasters/SignatureBasedPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,126 +12,131 @@ import {TransactionHelper, Transaction} from "@matterlabs/zksync-contracts/l2/sy
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.
/// @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)"
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;
// 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.
_;
}
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") {
require(_signer != address(0), "Signer cannot be address(0)");
// Owner can be signer too.
signer = _signer;
}
/// @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") {
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"
);
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");

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

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

// 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");
}
}
// 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;

function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override onlyBootloader {
// Refunds are not supported yet.
// 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 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 {}
function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override onlyBootloader {}

/// @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 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 {}

function domainSeparator() public view returns(bytes32) {
return _domainSeparatorV4();
}
}
/// @dev Only owner should be able to change signer.
/// @param _signer New signer address
function changeSigner(address _signer) public onlyOwner {
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) public onlyOwner {
nonces[_userAddress]++;
}

function domainSeparator() public view returns (bytes32) {
return _domainSeparatorV4();
}
}
80 changes: 38 additions & 42 deletions contracts/deploy/allowListPaymaster.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { Provider, Wallet } from "zksync-web3";
import * as ethers from "ethers";
import { HardhatRuntimeEnvironment, HttpNetworkUserConfig } from "hardhat/types";
import { Deployer } from "@matterlabs/hardhat-zksync-deploy";
import { fundAccount, deployContract } from "./utils";
import * as hre from "hardhat";

// load env file
import dotenv from "dotenv";

dotenv.config();

// load wallet private key from env file
Expand All @@ -14,51 +11,50 @@ 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 AllowlistPaymaster contract...`);
const provider = new Provider((hre.network.config as HttpNetworkUserConfig).url);
async function main() {
const contract = "AllowlistPaymaster";
const artifact = await hre.ethers.loadArtifact(contract);

console.log(
`Running script to deploy ${artifact.contractName} contract on ${hre.network.name}`,
);

// The wallet that will deploy the token and 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);
// Retrieve signers
const [deployer] = await hre.ethers.getSigners();

uF4No marked this conversation as resolved.
Show resolved Hide resolved
// Deploying the paymaster
const paymasterArtifact = await deployer.loadArtifact("AllowlistPaymaster");
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(`Contract owner added to allow list: ${wallet.address}`);
const paymaster = await deployContract(artifact.contractName, []);
const paymasterAddress = await paymaster.getAddress();
console.log(`Paymaster address: ${paymasterAddress}`);
console.log(`Contract owner added to allow list: ${deployer.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);
console.log(`Paymaster ETH balance is now ${paymasterBalance.toString()}`);
await fundAccount(deployer, paymasterAddress, "0.005");

// Verify contract programmatically
//
// Contract MUST be fully qualified name (e.g. path/sourceName:contractName)
const contractFullyQualifedName =
"contracts/paymasters/AllowlistPaymaster.sol:AllowlistPaymaster";
const verificationId = await hre.run("verify:verify", {
address: paymaster.address,
contract: contractFullyQualifedName,
constructorArguments: [],
bytecode: paymasterArtifact.bytecode,
});
console.log(
`${contractFullyQualifedName} verified! VerificationId: ${verificationId}`,
const paymasterBalance = await hre.ethers.provider.getBalance(
paymasterAddress,
);
console.log(`Paymaster ETH balance is now ${paymasterBalance.toString()}`);
// only verify on testnet and mainnet
if (hre.network.name.includes("ZKsyncEra")) {
const verificationId = await hre.run("verify:verify", {
uF4No marked this conversation as resolved.
Show resolved Hide resolved
address: paymasterAddress,
// Contract MUST be fully qualified name (e.g. path/sourceName:contractName)
contract: `${artifact.sourceName}:${artifact.contractName}`,
constructorArguments: [],
});
console.log(
`${artifact.contractName} verified! VerificationId: ${verificationId}`,
);
}

console.log(`Done!`);
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Loading
Loading