From f83fd656e61a6b88214a49eb6d77f3109c1e8a03 Mon Sep 17 00:00:00 2001 From: Jrigada Date: Tue, 24 Sep 2024 15:27:43 -0300 Subject: [PATCH 1/5] initial support for custom paymaster --- crates/cheatcodes/spec/src/vm.rs | 4 + crates/cheatcodes/src/inspector.rs | 9 +- crates/cheatcodes/src/test.rs | 10 ++ crates/forge/tests/it/zk/mod.rs | 1 + crates/forge/tests/it/zk/paymaster.rs | 37 +++++++ crates/zksync/core/src/lib.rs | 9 ++ crates/zksync/core/src/vm/runner.rs | 23 ++++- crates/zksync/core/src/vm/tracer.rs | 4 +- testdata/zk/MyPaymaster.sol | 138 ++++++++++++++++++++++++++ testdata/zk/Paymaster.t.sol | 104 +++++++++++++++++++ 10 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 crates/forge/tests/it/zk/paymaster.rs create mode 100644 testdata/zk/MyPaymaster.sol create mode 100644 testdata/zk/Paymaster.t.sol diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 5e4d567f2..a7997690d 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -700,6 +700,10 @@ interface Vm { #[cheatcode(group = Testing, safety = Safe)] function zkVmSkip() external pure; + /// Enables/Disables use of a paymaster for ZK transactions. + #[cheatcode(group = Testing, safety = Safe)] + function zkUsePaymaster(address paymaster_address, bytes calldata paymaster_input) external pure; + /// Registers bytecodes for ZK-VM for transact/call and create instructions. #[cheatcode(group = Testing, safety = Safe)] function zkRegisterContract(string calldata name, bytes32 evmBytecodeHash, bytes calldata evmDeployedBytecode, bytes calldata evmBytecode, bytes32 zkBytecodeHash, bytes calldata zkDeployedBytecode) external pure; diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 974c3c8c0..9ba15ab11 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -37,8 +37,8 @@ use foundry_evm_core::{ use foundry_zksync_compiler::{DualCompiledContract, DualCompiledContracts}; use foundry_zksync_core::{ convert::{ConvertH160, ConvertH256, ConvertRU256, ConvertU256}, - get_account_code_key, get_balance_key, get_nonce_key, Call, ZkTransactionMetadata, - DEFAULT_CREATE2_DEPLOYER_ZKSYNC, + get_account_code_key, get_balance_key, get_nonce_key, Call, ZkPaymasterData, + ZkTransactionMetadata, DEFAULT_CREATE2_DEPLOYER_ZKSYNC, }; use itertools::Itertools; use revm::{ @@ -360,6 +360,8 @@ pub struct Cheatcodes { /// to EVM. Alternatively, we'd need to add `vm.zkVmSkip()` to these calls manually. pub skip_zk_vm_addresses: HashSet
, + pub paymaster_params: Option, + /// Records the next create address for `skip_zk_vm_addresses`. pub record_next_create_address: bool, @@ -463,6 +465,7 @@ impl Cheatcodes { skip_zk_vm_addresses: Default::default(), record_next_create_address: Default::default(), persisted_factory_deps: Default::default(), + paymaster_params: None, } } @@ -959,6 +962,7 @@ impl Cheatcodes { expected_calls: Some(&mut self.expected_calls), accesses: self.accesses.as_mut(), persisted_factory_deps: Some(&mut self.persisted_factory_deps), + paymaster_data: self.paymaster_params.take(), }; let create_inputs = CreateInputs { scheme: input.scheme().unwrap_or(CreateScheme::Create), @@ -1550,6 +1554,7 @@ impl Cheatcodes { expected_calls: Some(&mut self.expected_calls), accesses: self.accesses.as_mut(), persisted_factory_deps: Some(&mut self.persisted_factory_deps), + paymaster_data: self.paymaster_params.take(), }; // We currently exhaust the entire gas for the call as zkEVM returns a very high amount diff --git a/crates/cheatcodes/src/test.rs b/crates/cheatcodes/src/test.rs index deaaa71ba..064d2daba 100644 --- a/crates/cheatcodes/src/test.rs +++ b/crates/cheatcodes/src/test.rs @@ -5,6 +5,7 @@ use alloy_primitives::Address; use alloy_sol_types::SolValue; use foundry_evm_core::constants::{MAGIC_ASSUME, MAGIC_SKIP}; use foundry_zksync_compiler::DualCompiledContract; +use foundry_zksync_core::ZkPaymasterData; pub(crate) mod assert; pub(crate) mod expect; @@ -31,6 +32,15 @@ impl Cheatcode for zkVmSkipCall { } } +impl Cheatcode for zkUsePaymasterCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { paymaster_address, paymaster_input } = self; + ccx.state.paymaster_params = + Some(ZkPaymasterData { address: *paymaster_address, input: paymaster_input.clone() }); + Ok(Default::default()) + } +} + impl Cheatcode for zkRegisterContractCall { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self { diff --git a/crates/forge/tests/it/zk/mod.rs b/crates/forge/tests/it/zk/mod.rs index d7337efb0..bbe6cda52 100644 --- a/crates/forge/tests/it/zk/mod.rs +++ b/crates/forge/tests/it/zk/mod.rs @@ -13,6 +13,7 @@ mod invariant; mod logs; mod nft; mod ownership; +mod paymaster; mod proxy; mod repros; mod traces; diff --git a/crates/forge/tests/it/zk/paymaster.rs b/crates/forge/tests/it/zk/paymaster.rs new file mode 100644 index 000000000..7d3493ae7 --- /dev/null +++ b/crates/forge/tests/it/zk/paymaster.rs @@ -0,0 +1,37 @@ +//! Forge tests for zksync contracts. + +use foundry_config::fs_permissions::PathPermission; +use foundry_test_utils::util; + +#[tokio::test(flavor = "multi_thread")] +async fn test_zk_contract_paymaster() { + let (prj, mut cmd) = util::setup_forge( + "test_zk_contract_paymaster", + foundry_test_utils::foundry_compilers::PathStyle::Dapptools, + ); + util::initialize(prj.root()); + + cmd.args([ + "install", + "OpenZeppelin/openzeppelin-contracts", + "cyfrin/zksync-contracts", + "--no-commit", + "--shallow", + ]) + .ensure_execute_success() + .expect("able to install dependencies"); + + cmd.forge_fuse(); + + let mut config = cmd.config(); + config.fs_permissions.add(PathPermission::read("./zkout")); + prj.write_config(config); + + prj.add_source("MyPaymaster.sol", include_str!("../../../../../testdata/zk/MyPaymaster.sol")) + .unwrap(); + prj.add_source("Paymaster.t.sol", include_str!("../../../../../testdata/zk/Paymaster.t.sol")) + .unwrap(); + + cmd.args(["test", "--zk-startup", "--evm-version", "shanghai", "--via-ir"]); + assert!(cmd.stdout_lossy().contains("Suite result: ok")); +} diff --git a/crates/zksync/core/src/lib.rs b/crates/zksync/core/src/lib.rs index b93dc4013..b9fdd7432 100644 --- a/crates/zksync/core/src/lib.rs +++ b/crates/zksync/core/src/lib.rs @@ -74,6 +74,15 @@ pub fn get_nonce_key(address: Address) -> rU256 { zksync_types::get_nonce_key(&address.to_h160()).key().to_ru256() } +/// Represents additional data for ZK transactions that require a paymaster. +#[derive(Clone, Debug, Default)] +pub struct ZkPaymasterData { + /// Paymaster address. + pub address: Address, + /// Paymaster input. + pub input: Bytes, +} + /// Represents additional data for ZK transactions. #[derive(Clone, Debug, Default)] pub struct ZkTransactionMetadata { diff --git a/crates/zksync/core/src/vm/runner.rs b/crates/zksync/core/src/vm/runner.rs index 520f65f1f..3996107be 100644 --- a/crates/zksync/core/src/vm/runner.rs +++ b/crates/zksync/core/src/vm/runner.rs @@ -137,6 +137,15 @@ where let (gas_limit, max_fee_per_gas) = gas_params(ecx, caller); info!(?gas_limit, ?max_fee_per_gas, "tx gas parameters"); + let paymaster_params = if let Some(paymaster_data) = &ccx.paymaster_data { + PaymasterParams { + paymaster: paymaster_data.address.to_h160(), + paymaster_input: paymaster_data.input.to_vec(), + } + } else { + PaymasterParams::default() + }; + let tx = L2Tx::new( CONTRACT_DEPLOYER_ADDRESS, calldata, @@ -150,7 +159,7 @@ where caller.to_h160(), call.value.to_u256(), factory_deps, - PaymasterParams::default(), + paymaster_params, ); let call_ctx = CallContext { @@ -186,6 +195,16 @@ where let (gas_limit, max_fee_per_gas) = gas_params(ecx, caller); info!(?gas_limit, ?max_fee_per_gas, "tx gas parameters"); + + let paymaster_params = if let Some(paymaster_data) = &ccx.paymaster_data { + PaymasterParams { + paymaster: paymaster_data.address.to_h160(), + paymaster_input: paymaster_data.input.to_vec(), + } + } else { + PaymasterParams::default() + }; + let tx = L2Tx::new( call.bytecode_address.to_h160(), call.input.to_vec(), @@ -202,7 +221,7 @@ where _ => U256::zero(), }, factory_deps, - PaymasterParams::default(), + paymaster_params, ); // address and caller are specific to the type of call: diff --git a/crates/zksync/core/src/vm/tracer.rs b/crates/zksync/core/src/vm/tracer.rs index 0b765a539..661f1000f 100644 --- a/crates/zksync/core/src/vm/tracer.rs +++ b/crates/zksync/core/src/vm/tracer.rs @@ -28,7 +28,7 @@ use zksync_utils::bytecode::hash_bytecode; use crate::{ convert::{ConvertAddress, ConvertH160, ConvertH256, ConvertU256}, vm::farcall::{CallAction, CallDepth}, - EMPTY_CODE, + ZkPaymasterData, EMPTY_CODE, }; use super::farcall::FarCallHandler; @@ -82,6 +82,8 @@ pub struct CheatcodeTracerContext<'a> { pub accesses: Option<&'a mut RecordAccess>, /// Factory deps that were persisted across calls pub persisted_factory_deps: Option<&'a mut HashMap>>, + /// Paymaster data + pub paymaster_data: Option, } /// Tracer result to return back to foundry. diff --git a/testdata/zk/MyPaymaster.sol b/testdata/zk/MyPaymaster.sol new file mode 100644 index 000000000..3cc330f88 --- /dev/null +++ b/testdata/zk/MyPaymaster.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "forge-std/console2.sol"; + +import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; +import {IPaymasterFlow} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; +import {TransactionHelper, Transaction} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; +import "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/Constants.sol"; + +contract MyPaymaster is IPaymaster { + 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: 1 ether + }(""); + 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 { + } + + receive() external payable {} +} + +contract MyERC20 is ERC20 { + uint8 private _decimals; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_ + ) 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; + } +} diff --git a/testdata/zk/Paymaster.t.sol b/testdata/zk/Paymaster.t.sol new file mode 100644 index 000000000..2150061a4 --- /dev/null +++ b/testdata/zk/Paymaster.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/Constants.sol"; +import {MyPaymaster, MyERC20} from "./MyPaymaster.sol"; + +contract TestPaymasterFlow is Test { + MyERC20 private erc20; + MyPaymaster private paymaster; + DoStuff private do_stuff; + address private alice; + bytes private paymaster_encoded_input; + + function setUp() public { + alice = makeAddr("Alice"); + do_stuff = new DoStuff(); + erc20 = new MyERC20("Test", "JR", 1); + paymaster = new MyPaymaster(address(erc20)); + + // Initial funding + vm.deal(address(do_stuff), 1 ether); + vm.deal(alice, 1 ether); + vm.deal(address(paymaster), 10 ether); + + // Mint and approve ERC20 tokens + erc20.mint(alice, 1); + vm.prank(alice, alice); + erc20.approve(address(paymaster), 1); + + // Encode paymaster input + paymaster_encoded_input = abi.encodeWithSelector( + bytes4(keccak256("approvalBased(address,uint256,bytes)")), + address(erc20), + uint256(1), + bytes("0x") + ); + } + + function testCallWithPaymaster() public { + require(address(do_stuff).balance == 1 ether, "Balance is not 1 ether"); + + uint256 alice_balance = address(alice).balance; + (bool success, ) = address(vm).call( + abi.encodeWithSignature( + "zkUsePaymaster(address,bytes)", + address(paymaster), + paymaster_encoded_input + ) + ); + require(success, "zkUsePaymaster call failed"); + + vm.prank(alice, alice); + do_stuff.do_stuff(); + + require(address(do_stuff).balance == 0, "Balance is not 0 ether"); + require(address(alice).balance == alice_balance, "Balance is not the same"); + } + + function testCreateWithPaymaster() public { + uint256 alice_balance = address(alice).balance; + (bool success, ) = address(vm).call( + abi.encodeWithSignature( + "zkUsePaymaster(address,bytes)", + address(paymaster), + paymaster_encoded_input + ) + ); + require(success, "zkUsePaymaster call failed"); + + vm.prank(alice, alice); + DoStuff new_do_stuff = new DoStuff(); + + require(address(alice).balance == alice_balance, "Balance is not the same"); + } + + function testFailPaymasterBalanceDoesNotUpdate() public { + uint256 alice_balance = address(alice).balance; + uint256 paymaster_balance = address(paymaster).balance; + (bool success, ) = address(vm).call( + abi.encodeWithSignature( + "zkUsePaymaster(address,bytes)", + address(paymaster), + paymaster_encoded_input + ) + ); + require(success, "zkUsePaymaster call failed"); + + vm.prank(alice, alice); + do_stuff.do_stuff(); + + require(address(alice).balance == alice_balance, "Balance is not the same"); + require(address(paymaster).balance < paymaster_balance, "Paymaster balance is not less"); + } +} + +contract DoStuff { + function do_stuff() public { + (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ + value: address(this).balance + }(""); + require(success, "Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough."); + } +} From 146b9bacce42f366e86e20270e0a616cfc2a0bab Mon Sep 17 00:00:00 2001 From: Jrigada Date: Tue, 24 Sep 2024 16:10:44 -0300 Subject: [PATCH 2/5] Move test files --- .../forge/tests/fixtures}/zk/MyPaymaster.sol | 78 +++++++------------ .../forge/tests/fixtures}/zk/Paymaster.t.sol | 33 ++------ crates/forge/tests/it/zk/paymaster.rs | 4 +- 3 files changed, 37 insertions(+), 78 deletions(-) rename {testdata => crates/forge/tests/fixtures}/zk/MyPaymaster.sol (59%) rename {testdata => crates/forge/tests/fixtures}/zk/Paymaster.t.sol (75%) diff --git a/testdata/zk/MyPaymaster.sol b/crates/forge/tests/fixtures/zk/MyPaymaster.sol similarity index 59% rename from testdata/zk/MyPaymaster.sol rename to crates/forge/tests/fixtures/zk/MyPaymaster.sol index 3cc330f88..509977a34 100644 --- a/testdata/zk/MyPaymaster.sol +++ b/crates/forge/tests/fixtures/zk/MyPaymaster.sol @@ -4,9 +4,17 @@ pragma solidity ^0.8.0; import "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import "forge-std/console2.sol"; -import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; -import {IPaymasterFlow} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; -import {TransactionHelper, Transaction} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; +import { + IPaymaster, + ExecutionResult, + PAYMASTER_VALIDATION_SUCCESS_MAGIC +} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; +import {IPaymasterFlow} from + "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; +import { + TransactionHelper, + Transaction +} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; import "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; import "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/Constants.sol"; @@ -16,10 +24,7 @@ contract MyPaymaster is IPaymaster { address public allowedToken; modifier onlyBootloader() { - require( - msg.sender == BOOTLOADER_FORMAL_ADDRESS, - "Only bootloader can call this method" - ); + require(msg.sender == BOOTLOADER_FORMAL_ADDRESS, "Only bootloader can call this method"); // Continue execution if called from the bootloader. _; } @@ -28,11 +33,7 @@ contract MyPaymaster is IPaymaster { allowedToken = _erc20; } - function validateAndPayForPaymasterTransaction( - bytes32, - bytes32, - Transaction calldata _transaction - ) + function validateAndPayForPaymasterTransaction(bytes32, bytes32, Transaction calldata _transaction) external payable onlyBootloader @@ -40,21 +41,14 @@ contract MyPaymaster is IPaymaster { { // 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] - ); + 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) - ); + (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"); @@ -64,22 +58,14 @@ contract MyPaymaster is IPaymaster { address thisAddress = address(this); - uint256 providedAllowance = IERC20(token).allowance( - userAddress, - thisAddress - ); - require( - providedAllowance >= PRICE_FOR_PAYING_FEES, - "Min allowance too low" - ); + 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) { + 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) { @@ -91,13 +77,8 @@ contract MyPaymaster is IPaymaster { } } // The bootloader never returns any data, so it can safely be ignored here. - (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ - value: 1 ether - }(""); - require( - success, - "Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough." - ); + (bool success,) = payable(BOOTLOADER_FORMAL_ADDRESS).call{value: 1 ether}(""); + require(success, "Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough."); } else { revert("Unsupported paymaster flow"); } @@ -110,8 +91,7 @@ contract MyPaymaster is IPaymaster { bytes32, ExecutionResult _txResult, uint256 _maxRefundedGas - ) external payable override onlyBootloader { - } + ) external payable override onlyBootloader {} receive() external payable {} } @@ -119,11 +99,7 @@ contract MyPaymaster is IPaymaster { contract MyERC20 is ERC20 { uint8 private _decimals; - constructor( - string memory name_, - string memory symbol_, - uint8 decimals_ - ) ERC20(name_, symbol_) { + constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) { _decimals = decimals_; } diff --git a/testdata/zk/Paymaster.t.sol b/crates/forge/tests/fixtures/zk/Paymaster.t.sol similarity index 75% rename from testdata/zk/Paymaster.t.sol rename to crates/forge/tests/fixtures/zk/Paymaster.t.sol index 2150061a4..abdd46ff8 100644 --- a/testdata/zk/Paymaster.t.sol +++ b/crates/forge/tests/fixtures/zk/Paymaster.t.sol @@ -30,10 +30,7 @@ contract TestPaymasterFlow is Test { // Encode paymaster input paymaster_encoded_input = abi.encodeWithSelector( - bytes4(keccak256("approvalBased(address,uint256,bytes)")), - address(erc20), - uint256(1), - bytes("0x") + bytes4(keccak256("approvalBased(address,uint256,bytes)")), address(erc20), uint256(1), bytes("0x") ); } @@ -41,12 +38,8 @@ contract TestPaymasterFlow is Test { require(address(do_stuff).balance == 1 ether, "Balance is not 1 ether"); uint256 alice_balance = address(alice).balance; - (bool success, ) = address(vm).call( - abi.encodeWithSignature( - "zkUsePaymaster(address,bytes)", - address(paymaster), - paymaster_encoded_input - ) + (bool success,) = address(vm).call( + abi.encodeWithSignature("zkUsePaymaster(address,bytes)", address(paymaster), paymaster_encoded_input) ); require(success, "zkUsePaymaster call failed"); @@ -59,12 +52,8 @@ contract TestPaymasterFlow is Test { function testCreateWithPaymaster() public { uint256 alice_balance = address(alice).balance; - (bool success, ) = address(vm).call( - abi.encodeWithSignature( - "zkUsePaymaster(address,bytes)", - address(paymaster), - paymaster_encoded_input - ) + (bool success,) = address(vm).call( + abi.encodeWithSignature("zkUsePaymaster(address,bytes)", address(paymaster), paymaster_encoded_input) ); require(success, "zkUsePaymaster call failed"); @@ -77,12 +66,8 @@ contract TestPaymasterFlow is Test { function testFailPaymasterBalanceDoesNotUpdate() public { uint256 alice_balance = address(alice).balance; uint256 paymaster_balance = address(paymaster).balance; - (bool success, ) = address(vm).call( - abi.encodeWithSignature( - "zkUsePaymaster(address,bytes)", - address(paymaster), - paymaster_encoded_input - ) + (bool success,) = address(vm).call( + abi.encodeWithSignature("zkUsePaymaster(address,bytes)", address(paymaster), paymaster_encoded_input) ); require(success, "zkUsePaymaster call failed"); @@ -96,9 +81,7 @@ contract TestPaymasterFlow is Test { contract DoStuff { function do_stuff() public { - (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ - value: address(this).balance - }(""); + (bool success,) = payable(BOOTLOADER_FORMAL_ADDRESS).call{value: address(this).balance}(""); require(success, "Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough."); } } diff --git a/crates/forge/tests/it/zk/paymaster.rs b/crates/forge/tests/it/zk/paymaster.rs index 7d3493ae7..506fec6d1 100644 --- a/crates/forge/tests/it/zk/paymaster.rs +++ b/crates/forge/tests/it/zk/paymaster.rs @@ -27,9 +27,9 @@ async fn test_zk_contract_paymaster() { config.fs_permissions.add(PathPermission::read("./zkout")); prj.write_config(config); - prj.add_source("MyPaymaster.sol", include_str!("../../../../../testdata/zk/MyPaymaster.sol")) + prj.add_source("MyPaymaster.sol", include_str!("../../fixtures/zk/MyPaymaster.sol")) .unwrap(); - prj.add_source("Paymaster.t.sol", include_str!("../../../../../testdata/zk/Paymaster.t.sol")) + prj.add_source("Paymaster.t.sol", include_str!("../../fixtures/zk/Paymaster.t.sol")) .unwrap(); cmd.args(["test", "--zk-startup", "--evm-version", "shanghai", "--via-ir"]); From e9f47dd3be844b10a60781027f254dbe90f64612 Mon Sep 17 00:00:00 2001 From: Jrigada Date: Tue, 24 Sep 2024 16:14:54 -0300 Subject: [PATCH 3/5] Cargo fmt --- crates/forge/tests/it/zk/paymaster.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/forge/tests/it/zk/paymaster.rs b/crates/forge/tests/it/zk/paymaster.rs index 506fec6d1..6f168d098 100644 --- a/crates/forge/tests/it/zk/paymaster.rs +++ b/crates/forge/tests/it/zk/paymaster.rs @@ -27,10 +27,8 @@ async fn test_zk_contract_paymaster() { config.fs_permissions.add(PathPermission::read("./zkout")); prj.write_config(config); - prj.add_source("MyPaymaster.sol", include_str!("../../fixtures/zk/MyPaymaster.sol")) - .unwrap(); - prj.add_source("Paymaster.t.sol", include_str!("../../fixtures/zk/Paymaster.t.sol")) - .unwrap(); + prj.add_source("MyPaymaster.sol", include_str!("../../fixtures/zk/MyPaymaster.sol")).unwrap(); + prj.add_source("Paymaster.t.sol", include_str!("../../fixtures/zk/Paymaster.t.sol")).unwrap(); cmd.args(["test", "--zk-startup", "--evm-version", "shanghai", "--via-ir"]); assert!(cmd.stdout_lossy().contains("Suite result: ok")); From a20203fc182a72b637a04297cfdea1eb39127443 Mon Sep 17 00:00:00 2001 From: Jrigada Date: Wed, 25 Sep 2024 15:18:13 -0300 Subject: [PATCH 4/5] Simplify paymaster implementation, move order of struct attributes and simplify imports --- crates/cheatcodes/src/inspector.rs | 5 +- .../forge/tests/fixtures/zk/MyPaymaster.sol | 85 +++---------------- .../forge/tests/fixtures/zk/Paymaster.t.sol | 35 ++++---- crates/forge/tests/it/zk/paymaster.rs | 14 ++- 4 files changed, 38 insertions(+), 101 deletions(-) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 7e9b6c973..ce7dc31ae 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -360,11 +360,12 @@ pub struct Cheatcodes { /// to EVM. Alternatively, we'd need to add `vm.zkVmSkip()` to these calls manually. pub skip_zk_vm_addresses: HashSet
, - pub paymaster_params: Option, - /// Records the next create address for `skip_zk_vm_addresses`. pub record_next_create_address: bool, + /// Paymaster params + pub paymaster_params: Option, + /// Dual compiled contracts pub dual_compiled_contracts: DualCompiledContracts, diff --git a/crates/forge/tests/fixtures/zk/MyPaymaster.sol b/crates/forge/tests/fixtures/zk/MyPaymaster.sol index 509977a34..225547d35 100644 --- a/crates/forge/tests/fixtures/zk/MyPaymaster.sol +++ b/crates/forge/tests/fixtures/zk/MyPaymaster.sol @@ -1,37 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; -import "../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "forge-std/console2.sol"; - import { IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC -} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; -import {IPaymasterFlow} from - "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; +} from "zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; +import {IPaymasterFlow} from "zksync-contracts/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; import { TransactionHelper, Transaction -} from "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; -import "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; -import "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/Constants.sol"; +} from "zksync-contracts/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; +import "zksync-contracts/zksync-contracts/l2/system-contracts/Constants.sol"; contract MyPaymaster is IPaymaster { - 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; - } + constructor() payable {} function validateAndPayForPaymasterTransaction(bytes32, bytes32, Transaction calldata _transaction) external @@ -39,49 +27,13 @@ contract MyPaymaster is IPaymaster { onlyBootloader returns (bytes4 magic, bytes memory context) { - // By default we consider the transaction as accepted. + // Always accept the transaction 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: 1 ether}(""); - require(success, "Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough."); - } else { - revert("Unsupported paymaster flow"); - } + // Pay for the transaction fee + 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"); } function postTransaction( @@ -95,20 +47,3 @@ contract MyPaymaster is IPaymaster { receive() external payable {} } - -contract MyERC20 is ERC20 { - uint8 private _decimals; - - constructor(string memory name_, string memory symbol_, uint8 decimals_) 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; - } -} diff --git a/crates/forge/tests/fixtures/zk/Paymaster.t.sol b/crates/forge/tests/fixtures/zk/Paymaster.t.sol index abdd46ff8..8b661e1f4 100644 --- a/crates/forge/tests/fixtures/zk/Paymaster.t.sol +++ b/crates/forge/tests/fixtures/zk/Paymaster.t.sol @@ -2,39 +2,32 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; -import "../lib/zksync-contracts/zksync-contracts/l2/system-contracts/Constants.sol"; -import {MyPaymaster, MyERC20} from "./MyPaymaster.sol"; +import "zksync-contracts/zksync-contracts/l2/system-contracts/Constants.sol"; +import {MyPaymaster} from "./MyPaymaster.sol"; contract TestPaymasterFlow is Test { - MyERC20 private erc20; MyPaymaster private paymaster; DoStuff private do_stuff; address private alice; + address private bob; bytes private paymaster_encoded_input; function setUp() public { alice = makeAddr("Alice"); + bob = makeAddr("Bob"); do_stuff = new DoStuff(); - erc20 = new MyERC20("Test", "JR", 1); - paymaster = new MyPaymaster(address(erc20)); + paymaster = new MyPaymaster(); - // Initial funding - vm.deal(address(do_stuff), 1 ether); + // A small amount is needed for initial tx processing vm.deal(alice, 1 ether); vm.deal(address(paymaster), 10 ether); - // Mint and approve ERC20 tokens - erc20.mint(alice, 1); - vm.prank(alice, alice); - erc20.approve(address(paymaster), 1); - // Encode paymaster input - paymaster_encoded_input = abi.encodeWithSelector( - bytes4(keccak256("approvalBased(address,uint256,bytes)")), address(erc20), uint256(1), bytes("0x") - ); + paymaster_encoded_input = abi.encodeWithSelector(bytes4(keccak256("general(bytes)")), bytes("0x")); } function testCallWithPaymaster() public { + vm.deal(address(do_stuff), 1 ether); require(address(do_stuff).balance == 1 ether, "Balance is not 1 ether"); uint256 alice_balance = address(alice).balance; @@ -44,10 +37,11 @@ contract TestPaymasterFlow is Test { require(success, "zkUsePaymaster call failed"); vm.prank(alice, alice); - do_stuff.do_stuff(); + do_stuff.do_stuff(bob); require(address(do_stuff).balance == 0, "Balance is not 0 ether"); require(address(alice).balance == alice_balance, "Balance is not the same"); + require(address(bob).balance == 1 ether, "Balance is not 1 ether"); } function testCreateWithPaymaster() public { @@ -72,7 +66,7 @@ contract TestPaymasterFlow is Test { require(success, "zkUsePaymaster call failed"); vm.prank(alice, alice); - do_stuff.do_stuff(); + do_stuff.do_stuff(bob); require(address(alice).balance == alice_balance, "Balance is not the same"); require(address(paymaster).balance < paymaster_balance, "Paymaster balance is not less"); @@ -80,8 +74,9 @@ contract TestPaymasterFlow is Test { } contract DoStuff { - function do_stuff() public { - (bool success,) = payable(BOOTLOADER_FORMAL_ADDRESS).call{value: address(this).balance}(""); - require(success, "Failed to transfer tx fee to the bootloader. Paymaster balance might not be enough."); + function do_stuff(address recipient) public { + uint256 amount = address(this).balance; + (bool success,) = payable(recipient).call{value: amount}(""); + require(success, "Failed to transfer balance to the recipient."); } } diff --git a/crates/forge/tests/it/zk/paymaster.rs b/crates/forge/tests/it/zk/paymaster.rs index 6f168d098..5104350ad 100644 --- a/crates/forge/tests/it/zk/paymaster.rs +++ b/crates/forge/tests/it/zk/paymaster.rs @@ -1,6 +1,5 @@ //! Forge tests for zksync contracts. -use foundry_config::fs_permissions::PathPermission; use foundry_test_utils::util; #[tokio::test(flavor = "multi_thread")] @@ -23,13 +22,20 @@ async fn test_zk_contract_paymaster() { cmd.forge_fuse(); - let mut config = cmd.config(); - config.fs_permissions.add(PathPermission::read("./zkout")); + let config = cmd.config(); prj.write_config(config); prj.add_source("MyPaymaster.sol", include_str!("../../fixtures/zk/MyPaymaster.sol")).unwrap(); prj.add_source("Paymaster.t.sol", include_str!("../../fixtures/zk/Paymaster.t.sol")).unwrap(); - cmd.args(["test", "--zk-startup", "--evm-version", "shanghai", "--via-ir"]); + cmd.args([ + "test", + "--zk-startup", + "--evm-version", + "shanghai", + "--via-ir", + "--match-contract", + "TestPaymasterFlow", + ]); assert!(cmd.stdout_lossy().contains("Suite result: ok")); } From a5b10cfa275a572d6341d35ba1e0059cc97d8ab8 Mon Sep 17 00:00:00 2001 From: Jrigada Date: Thu, 26 Sep 2024 09:46:53 -0300 Subject: [PATCH 5/5] Remove evm version from cmd --- crates/forge/tests/it/zk/paymaster.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/forge/tests/it/zk/paymaster.rs b/crates/forge/tests/it/zk/paymaster.rs index 5104350ad..e21a48daf 100644 --- a/crates/forge/tests/it/zk/paymaster.rs +++ b/crates/forge/tests/it/zk/paymaster.rs @@ -28,14 +28,6 @@ async fn test_zk_contract_paymaster() { prj.add_source("MyPaymaster.sol", include_str!("../../fixtures/zk/MyPaymaster.sol")).unwrap(); prj.add_source("Paymaster.t.sol", include_str!("../../fixtures/zk/Paymaster.t.sol")).unwrap(); - cmd.args([ - "test", - "--zk-startup", - "--evm-version", - "shanghai", - "--via-ir", - "--match-contract", - "TestPaymasterFlow", - ]); + cmd.args(["test", "--zk-startup", "--via-ir", "--match-contract", "TestPaymasterFlow"]); assert!(cmd.stdout_lossy().contains("Suite result: ok")); }