├── testfile ├── tests ├── test_p2p.py ├── rip7560 │ ├── __init__.py │ ├── devnet │ │ └── test_devnet.py │ ├── test_rip7712.py │ ├── test_gas_usage.py │ ├── types.py │ ├── conftest.py │ ├── test_env_opcodes.py │ ├── test_validation_rules.py │ └── test_send_failed.py ├── single │ ├── rpc │ │ ├── __init__.py │ │ ├── test_eth_chainId.py │ │ ├── test_eth_supportedEntryPoints.py │ │ ├── test_eth_getUserOperationReceipt.py │ │ ├── conftest.py │ │ ├── test_eth_getUserOperationByHash.py │ │ ├── test_eth_estimateUserOperationGas.py │ │ └── test_eth_sendUserOperation.py │ ├── bundle │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_paymaster.py │ │ └── test_codehash.py │ ├── opbanning │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_op_banning.py │ │ └── test_create.py │ ├── reputation │ │ ├── __init__.py │ │ ├── test_replace.py │ │ ├── test_erep.py │ │ └── test_reputation.py │ ├── gas │ │ ├── test_eip_7623.py │ │ └── test_pre_verification_gas_calculation.py │ └── eip7702 │ │ └── test_eip7702_tuple_userop.py ├── __init__.py ├── contracts │ ├── TestRulesAggregator.sol │ ├── rip7560 │ │ ├── TestCounter.sol │ │ ├── OpcodesTestAccountFactory.sol │ │ ├── TestAccountFactory.sol │ │ ├── RIP7560TransactionType4.sol │ │ ├── TestPaymaster.sol │ │ ├── RIP7560TestTimeRangeAccount.sol │ │ ├── OpcodesTestAccount.sol │ │ ├── gaswaste │ │ │ ├── GasWasteAccount.sol │ │ │ └── GasWastePaymaster.sol │ │ ├── OpcodesTestPaymaster.sol │ │ ├── RIP7560TestTimeRangePaymaster.sol │ │ ├── RIP7560NonceManager.sol │ │ ├── RIP7560TestRulesAccountDeployer.sol │ │ ├── TestPostOpPaymaster.sol │ │ ├── RIP7560TestRulesAccount.sol │ │ ├── TestAccount.sol │ │ ├── RIP7560Paymaster.sol │ │ ├── RIP7560Deployer.sol │ │ └── utils │ │ │ └── TestUtils.sol │ ├── State.sol │ ├── ITestAccount.sol │ ├── Stakable.sol │ ├── TestRulesAccountFactory.sol │ ├── UserOpGetters.sol │ ├── TestReputationAccountFactory.sol │ ├── TestReputationPaymaster.sol │ ├── TestRulesTarget.sol │ ├── TestSimplePaymaster.sol │ ├── Helper.sol │ ├── TestReputationAccount.sol │ ├── ValidationRulesStorage.sol │ ├── TestCoin.sol │ ├── TestFakeWalletPaymaster.sol │ ├── TestCodeHashFactory.sol │ ├── SimpleWallet.sol │ ├── TestRulesAccount.sol │ ├── TestFakeWalletToken.sol │ ├── TestRulesPaymaster.sol │ ├── TestRulesFactory.sol │ ├── Create2.sol │ └── ValidationRules.sol ├── tests.iml ├── p2p │ └── test_p2p.py ├── types.py ├── user_operation_erc4337.py ├── transaction_eip_7702.py ├── find_min.py ├── conftest.py └── utils.py ├── bundler_spec_tests ├── __init__.py └── bundler_spec_tests.iml ├── .gitignore ├── .gitmodules ├── docker └── Dockerfile ├── scripts ├── clone-helper └── aabundler-launcher ├── README.md ├── pyproject.toml └── .circleci └── config.yml /testfile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_p2p.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/rip7560/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/single/rpc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bundler_spec_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/single/bundle/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/single/bundle/conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/single/opbanning/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/single/opbanning/conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/single/reputation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.register_assert_rewrite("tests.utils") 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | .idea/ 4 | dist/ 5 | /.pdm.toml 6 | .DS_Store 7 | .pdm-python 8 | -------------------------------------------------------------------------------- /tests/contracts/TestRulesAggregator.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.25; 2 | 3 | contract TestRulesAggregator { 4 | } 5 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/TestCounter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | contract TestCounter { 5 | uint256 public counter = 0; 6 | 7 | function increment() external { 8 | counter++; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/contracts/State.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | contract State { 5 | mapping(address => uint) state; 6 | 7 | function getState(address addr) public returns (uint) { 8 | return state[addr]; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/tests.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/contracts/ITestAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "@account-abstraction/contracts/interfaces/IAccount.sol"; 5 | 6 | interface IState { 7 | function state() external returns (uint256); 8 | 9 | function funTSTORE() external returns(uint256); 10 | } 11 | 12 | interface ITestAccount is IState, IAccount { 13 | } 14 | -------------------------------------------------------------------------------- /bundler_spec_tests/bundler_spec_tests.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/OpcodesTestAccountFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.25; 2 | 3 | import "./OpcodesTestAccount.sol"; 4 | 5 | contract OpcodesTestAccountFactory { 6 | event TestFactoryEvent(uint salt); 7 | function createAccount(uint salt) external returns (address) { 8 | TestUtils.emitEvmData("factory-validation"); 9 | return address(new OpcodesTestAccount{salt: bytes32(salt)}()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/TestAccountFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.25; 2 | 3 | import "../Stakable.sol"; 4 | import "./TestAccount.sol"; 5 | 6 | contract TestAccountFactory is Stakable { 7 | event TestFactoryEvent(uint salt); 8 | function createAccount(uint salt) external returns (address) { 9 | emit TestFactoryEvent(salt); 10 | return address(new TestAccount{salt: bytes32(salt)}()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "account-abstraction"] 2 | path = @account-abstraction 3 | url = https://github.com/eth-infinitism/account-abstraction.git 4 | branch = develop 5 | [submodule "spec"] 6 | path = spec 7 | url = https://github.com/eth-infinitism/bundler-spec.git 8 | branch = main 9 | [submodule "./@account-abstraction/"] 10 | branch = develop 11 | [submodule "@rip7560"] 12 | path = @rip7560 13 | url = git@github.com:eth-infinitism/rip7560_contracts.git 14 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/RIP7560TransactionType4.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | struct TransactionType4 { 5 | address sender; 6 | uint256 nonce; 7 | uint256 validationGasLimit; 8 | uint256 paymasterValidationGasLimit; 9 | uint256 postOpGasLimit; 10 | uint256 callGasLimit; 11 | uint256 maxFeePerGas; 12 | uint256 maxPriorityFeePerGas; 13 | uint256 builderFee; 14 | address paymaster; 15 | bytes paymasterData; 16 | address deployer; 17 | bytes deployerData; 18 | bytes callData; 19 | bytes signature; 20 | } 21 | -------------------------------------------------------------------------------- /tests/single/rpc/test_eth_chainId.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from jsonschema import validate, Validator 3 | from tests.types import RPCRequest, CommandLineArgs 4 | 5 | 6 | @pytest.mark.parametrize("schema_method", ["eth_chainId"], ids=[""]) 7 | def test_eth_chainId(schema): 8 | request = RPCRequest(method="eth_chainId") 9 | bundler_response = request.send(CommandLineArgs.url) 10 | node_response = request.send(CommandLineArgs.ethereum_node) 11 | assert int(bundler_response.result, 16) == int(node_response.result, 16) 12 | Validator.check_schema(schema) 13 | validate(instance=bundler_response.result, schema=schema) 14 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cimg/python:3.13.2-node 2 | 3 | USER root 4 | 5 | # Install dependencies and Go 6 | RUN apt-get update && \ 7 | apt-get install -y wget && \ 8 | wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz && \ 9 | tar -C /usr/local -xzf go1.22.5.linux-amd64.tar.gz && \ 10 | rm go1.22.5.linux-amd64.tar.gz 11 | 12 | # Set Go environment variables 13 | ENV PATH="/root/.local/bin:/usr/local/go/bin:${PATH}" 14 | 15 | # Verify installations 16 | RUN python --version && go version 17 | 18 | RUN curl -L https://foundry.paradigm.xyz | bash 19 | RUN source ~/.bashrc 20 | ENV PATH="/root/.foundry/bin:${PATH}" 21 | RUN foundryup 22 | 23 | USER $USERNAME 24 | -------------------------------------------------------------------------------- /tests/contracts/Stakable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "@account-abstraction/contracts/interfaces/IStakeManager.sol"; 5 | 6 | abstract contract Stakable { 7 | 8 | function addStake(IStakeManager stakeManager, uint32 _unstakeDelaySec) external payable { 9 | stakeManager.addStake{value: msg.value}(_unstakeDelaySec); 10 | } 11 | function unlockStake(IStakeManager stakeManager) external { 12 | stakeManager.unlockStake(); 13 | } 14 | function withdrawStake(IStakeManager stakeManager, address payable withdrawAddress) external { 15 | stakeManager.withdrawStake(withdrawAddress); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/contracts/TestRulesAccountFactory.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.25; 2 | // SPDX-License-Identifier: MIT 3 | 4 | import "./TestRulesAccount.sol"; 5 | import "./ValidationRules.sol"; 6 | 7 | contract TestRulesAccountFactory is Stakable, ValidationRulesStorage { 8 | TestCoin public immutable coin = new TestCoin(); 9 | constructor(address _ep) { 10 | entryPoint = IEntryPoint(_ep); 11 | } 12 | 13 | function create(uint nonce, string memory rule, address _ep) public returns (TestRulesAccount) { 14 | TestRulesAccount account = new TestRulesAccount{salt : bytes32(nonce)}(_ep); 15 | account.setCoin(coin); 16 | return account; 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /scripts/clone-helper: -------------------------------------------------------------------------------- 1 | branch=$1 2 | url=$2 3 | 4 | if [ "$url" == "" ]; then 5 | echo usage: "$0 {branch} {giturl}" 6 | exit 1 7 | fi 8 | 9 | dir=`echo $url | perl -pe 's!.*/!!; s/\.git$//' ` 10 | ( test $CIRCLE_BRANCH != "master" && git clone --quiet $url --depth 1 --branch $CIRCLE_BRANCH ) || git clone --quiet $url --depth 1 --branch $branch 11 | cd $dir 12 | echo "== checked out '$dir' branch '`git branch --show-current`'" 13 | 14 | if [ "$3" == "--no-submodules" ]; then 15 | echo "Skipping submodule cloning as per request." 16 | else 17 | git submodule update --recursive --init 18 | fi 19 | 20 | git rev-parse HEAD > commit-hash.txt 21 | echo == commit-hash.txt = $(cat commit-hash.txt) 22 | -------------------------------------------------------------------------------- /tests/single/rpc/test_eth_supportedEntryPoints.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from jsonschema import validate, Validator 3 | from tests.types import RPCRequest, CommandLineArgs 4 | from eth_utils import to_checksum_address 5 | 6 | 7 | @pytest.mark.parametrize("schema_method", ["eth_supportedEntryPoints"], ids=[""]) 8 | def test_eth_supportedEntryPoints(schema): 9 | response = RPCRequest(method="eth_supportedEntryPoints").send(CommandLineArgs.url) 10 | supported_entrypoints = response.result 11 | assert len(supported_entrypoints) == 1 12 | assert to_checksum_address(supported_entrypoints[0]) == CommandLineArgs.entrypoint 13 | Validator.check_schema(schema) 14 | validate(instance=response.result, schema=schema) 15 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/TestPaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "../Stakable.sol"; 5 | import "./TestAccount.sol"; 6 | import {IRip7560Paymaster} from "@rip7560/contracts/interfaces/IRip7560Paymaster.sol"; 7 | 8 | contract TestPaymaster is IRip7560Paymaster { 9 | 10 | constructor() payable {} 11 | 12 | function validatePaymasterTransaction( 13 | uint256 version, 14 | bytes32 txHash, 15 | bytes calldata transaction) 16 | external 17 | { 18 | RIP7560Utils.paymasterAcceptTransaction("!hello hello hello!", 1, type(uint48).max - 1); 19 | } 20 | 21 | function postPaymasterTransaction( 22 | bool success, 23 | uint256 actualGasCost, 24 | bytes calldata context 25 | ) external {} 26 | 27 | } 28 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/RIP7560TestTimeRangeAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.15; 3 | 4 | import "@rip7560/contracts/interfaces/IRip7560Account.sol"; 5 | import "@rip7560/contracts/utils/RIP7560Utils.sol"; 6 | 7 | contract RIP7560TestTimeRangeAccount is IRip7560Account { 8 | 9 | constructor() payable {} 10 | 11 | function validateTransaction( 12 | uint256 version, 13 | bytes32 txHash, 14 | bytes calldata transaction 15 | ) external { 16 | RIP7560Transaction memory txStruct = RIP7560Utils.decodeTransaction(version, transaction); 17 | (uint48 validAfter, uint48 validUntil) = 18 | abi.decode(txStruct.authorizationData, (uint48, uint48)); 19 | RIP7560Utils.accountAcceptTransaction(validAfter, validUntil); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/contracts/UserOpGetters.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 5 | 6 | library UserOpGetters { 7 | function getFactory(PackedUserOperation memory op) internal pure returns (address) { 8 | if (op.initCode.length < 20) { 9 | return address(0); 10 | } 11 | return bytesToAddress(op.initCode); 12 | } 13 | 14 | function getPaymaster(PackedUserOperation memory op) internal pure returns (address) { 15 | if (op.paymasterAndData.length < 20) { 16 | return address(0); 17 | } 18 | return bytesToAddress(op.paymasterAndData); 19 | } 20 | 21 | function bytesToAddress(bytes memory data) private pure returns (address) { 22 | return address(bytes20(data)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/OpcodesTestAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@rip7560/contracts/interfaces/IRip7560Account.sol"; 5 | import "@rip7560/contracts/interfaces/IRip7560Transaction.sol"; 6 | import "@rip7560/contracts/utils/RIP7560Utils.sol"; 7 | 8 | import "../utils/TestUtils.sol"; 9 | 10 | contract OpcodesTestAccount is IRip7560Account { 11 | 12 | constructor() payable {} 13 | 14 | function validateTransaction( 15 | uint256 version, 16 | bytes32 txHash, 17 | bytes calldata transaction 18 | ) public override { 19 | TestUtils.emitEvmData("account-validation"); 20 | RIP7560Utils.accountAcceptTransaction(1, type(uint48).max - 1); 21 | } 22 | 23 | function saveEventOpcodes() external { 24 | TestUtils.emitEvmData("account-execution"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/rip7560/devnet/test_devnet.py: -------------------------------------------------------------------------------- 1 | def test_devnet(w3, tx_7560): 2 | # account: Account = w3.eth.account.from_key(private_key) 3 | # print(w3.middleware_onion.middlewares[0]) 4 | nonce = w3.eth.get_transaction_count(w3.eth.default_account) 5 | transaction = { 6 | "from": w3.eth.default_account, 7 | "to": "0xF0109fC8DF283027b6285cc889F5aA624EaC1F55", 8 | "value": 1, 9 | "gas": 2000000, 10 | "maxFeePerGas": 2000000000, 11 | "maxPriorityFeePerGas": 1000000000, 12 | "nonce": nonce, 13 | "chainId": 1337, 14 | } 15 | # signed = w3.eth.account.sign_transaction(transaction, account.key) 16 | # res = w3.eth.send_raw_transaction(signed.raw_transaction.hex()) 17 | res = w3.eth.send_transaction(transaction) 18 | print("type 1 tx:", res.hex()) 19 | res = tx_7560.send() 20 | print("type 4 tx:", res) 21 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/gaswaste/GasWasteAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@rip7560/contracts/interfaces/IRip7560Transaction.sol"; 5 | import "@rip7560/contracts/utils/RIP7560Utils.sol"; 6 | import "../utils/TestUtils.sol"; 7 | 8 | contract GasWasteAccount { 9 | uint256 public accCounter = 0; 10 | 11 | constructor() payable { 12 | } 13 | 14 | function validateTransaction( 15 | uint256, 16 | bytes32, 17 | bytes calldata 18 | ) external { 19 | do { 20 | accCounter++; 21 | } while (gasleft() > 3000); 22 | RIP7560Utils.accountAcceptTransaction(1, type(uint48).max - 1); 23 | } 24 | 25 | function anyExecutionFunction() external { 26 | do { 27 | accCounter++; 28 | } while (gasleft() > 100); 29 | } 30 | 31 | receive() external payable {} 32 | } 33 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/gaswaste/GasWastePaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@rip7560/contracts/utils/RIP7560Utils.sol"; 5 | 6 | contract GasWastePaymaster { 7 | uint256 public pmCounter = 0; 8 | 9 | function validatePaymasterTransaction( 10 | uint256 version, 11 | bytes32 txHash, 12 | bytes calldata transaction) 13 | external 14 | { 15 | do { 16 | pmCounter++; 17 | } while (gasleft() > 3000); 18 | RIP7560Utils.paymasterAcceptTransaction("", 1, type(uint48).max - 1); 19 | } 20 | 21 | function postPaymasterTransaction( 22 | bool success, 23 | uint256 actualGasCost, 24 | bytes calldata context 25 | ) external { 26 | do { 27 | pmCounter++; 28 | } while (gasleft() > 100); 29 | } 30 | 31 | receive() external payable { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/OpcodesTestPaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "../../Stakable.sol"; 5 | import "../TestAccount.sol"; 6 | import {IRip7560Paymaster} from "@rip7560/contracts/interfaces/IRip7560Paymaster.sol"; 7 | 8 | contract OpcodesTestPaymaster is IRip7560Paymaster { 9 | 10 | constructor() payable {} 11 | 12 | function validatePaymasterTransaction( 13 | uint256 version, 14 | bytes32 txHash, 15 | bytes calldata transaction) 16 | external 17 | { 18 | TestUtils.emitEvmData("paymaster-validation"); 19 | RIP7560Utils.paymasterAcceptTransaction("!hello hello hello!", 1, type(uint48).max - 1); 20 | } 21 | 22 | function postPaymasterTransaction( 23 | bool success, 24 | uint256 actualGasCost, 25 | bytes calldata context 26 | ) external { 27 | TestUtils.emitEvmData("paymaster-postop"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/contracts/TestReputationAccountFactory.sol: -------------------------------------------------------------------------------- 1 | import "./Stakable.sol"; 2 | import "./TestReputationAccount.sol"; 3 | 4 | contract TestReputationAccountFactory is Stakable { 5 | address public immutable entryPoint; 6 | uint256 public accountState; 7 | 8 | uint counter; 9 | constructor(address _ep) { 10 | entryPoint = _ep; 11 | } 12 | 13 | function setAccountState(uint _state) external { 14 | accountState =_state; 15 | } 16 | 17 | function create(uint nonce) public returns (TestReputationAccount) { 18 | TestReputationAccount account = new TestReputationAccount{salt : bytes32(nonce)}(entryPoint); 19 | //this test passes validation, and fails bundle creation 20 | if (counter++ > 0) { 21 | account.setState(accountState); 22 | } 23 | return account; 24 | } 25 | 26 | receive() external payable { 27 | IEntryPoint(entryPoint).depositTo(address (this)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/RIP7560TestTimeRangePaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.15; 3 | 4 | import "@rip7560/contracts/interfaces/IRip7560Paymaster.sol"; 5 | import "@rip7560/contracts/utils/RIP7560Utils.sol"; 6 | 7 | contract RIP7560TestTimeRangePaymaster is IRip7560Paymaster { 8 | 9 | constructor() payable {} 10 | 11 | function validatePaymasterTransaction( 12 | uint256 version, 13 | bytes32 txHash, 14 | bytes calldata transaction) 15 | external 16 | { 17 | RIP7560Transaction memory txStruct = RIP7560Utils.decodeTransaction(version, transaction); 18 | (uint48 validAfter, uint48 validUntil) = 19 | abi.decode(txStruct.paymasterData, (uint48, uint48)); 20 | RIP7560Utils.paymasterAcceptTransaction("", validAfter, validUntil); 21 | } 22 | function postPaymasterTransaction( 23 | bool success, 24 | uint256 actualGasCost, 25 | bytes calldata context 26 | ) external {} 27 | } 28 | -------------------------------------------------------------------------------- /tests/contracts/TestReputationPaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@account-abstraction/contracts/interfaces/IPaymaster.sol"; 5 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 6 | import "./State.sol"; 7 | import "./Stakable.sol"; 8 | 9 | contract TestReputationPaymaster is IPaymaster, Stakable { 10 | 11 | IEntryPoint ep; 12 | uint256 public state; 13 | 14 | constructor(address _ep) payable { 15 | ep = IEntryPoint(_ep); 16 | (bool req,) = address(ep).call{value : msg.value}(""); 17 | require(req); 18 | } 19 | 20 | function setState(uint _state) external { 21 | state=_state; 22 | } 23 | 24 | function validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256) 25 | external returns (bytes memory context, uint256 deadline) { 26 | require(state != 0xdead, "No bundle for you"); 27 | return ("", 0); 28 | } 29 | 30 | receive() external payable { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/RIP7560NonceManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | contract RIP7560NonceManager { 5 | address private entryPoint; 6 | 7 | mapping (address => mapping(uint192 => uint64)) private nonces; 8 | 9 | constructor(address _entryPoint){ 10 | entryPoint = _entryPoint; 11 | } 12 | 13 | // The production NonceManager will not emit events 14 | event NonceIncrease(address account, uint192 key, uint64 newNonce); 15 | 16 | fallback(bytes calldata data) external returns (bytes memory) { 17 | address account = address(bytes20(data[:20])); 18 | uint192 key = uint192(bytes24(data[20:44])); 19 | if (msg.sender == entryPoint){ 20 | uint64 nonce = uint64(bytes8(data[44:52])); 21 | require(nonces[account][key]++ == nonce, "nonce mismatch"); 22 | emit NonceIncrease(account, key, nonces[account][key]); 23 | return ""; 24 | } 25 | else { 26 | return abi.encodePacked(nonces[account][key]); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/RIP7560TestRulesAccountDeployer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "../Create2.sol"; 5 | import "../ValidationRules.sol"; 6 | import "../Stakable.sol"; 7 | 8 | import "@rip7560/contracts/interfaces/IRip7560Transaction.sol"; 9 | import "./RIP7560TestRulesAccount.sol"; 10 | 11 | contract RIP7560TestRulesAccountDeployer is Stakable { 12 | TestCoin public immutable coin = new TestCoin(); 13 | constructor() payable { 14 | } 15 | 16 | function createAccount(address owner, uint256 salt, string memory rule) public returns (RIP7560TestRulesAccount) { 17 | RIP7560TestRulesAccount account = new RIP7560TestRulesAccount{salt : bytes32(salt)}(); 18 | account.setCoin(coin); 19 | return account; 20 | } 21 | 22 | function getCreate2Address(address owner, uint256 salt, string memory rule) public view returns (address) { 23 | return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked( 24 | type(RIP7560TestRulesAccount).creationCode 25 | ))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/contracts/TestRulesTarget.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 5 | import "./TestRulesTarget.sol"; 6 | import "./ValidationRulesStorage.sol"; 7 | import "./ValidationRules.sol"; 8 | import "./TestCoin.sol"; 9 | 10 | contract TestRulesTarget is ValidationRulesStorage { 11 | 12 | receive() external payable {} 13 | 14 | function runFactorySpecificRule( 15 | uint nonce, 16 | string memory rule, 17 | address _entryPoint, 18 | address create2address 19 | ) external payable { 20 | return ValidationRules.runFactorySpecificRule(nonce, rule, _entryPoint, create2address); 21 | } 22 | 23 | function runRule( 24 | string memory rule, 25 | IState account, 26 | address paymaster, 27 | address factory, 28 | TestCoin coin, 29 | ValidationRulesStorage self, 30 | TestRulesTarget target 31 | ) external payable returns (uint) { 32 | return ValidationRules.runRule(rule, account, paymaster, factory, coin, self, target); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/contracts/TestSimplePaymaster.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8; 3 | 4 | import "@account-abstraction/contracts/interfaces/IPaymaster.sol"; 5 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 6 | 7 | contract TestSimplePaymaster is IPaymaster { 8 | IEntryPoint public entryPoint; 9 | constructor(address _ep) payable { 10 | entryPoint = IEntryPoint(_ep); 11 | if (_ep != address(0)) { 12 | (bool req,) = address(_ep).call{value : msg.value}(""); 13 | require(req); 14 | } 15 | } 16 | 17 | function validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256) external returns (bytes memory context, uint256 validationData) { 18 | return ("", 0); 19 | } 20 | 21 | function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint) external {} 22 | 23 | function withdrawTo(address payable to, uint256 amount) external { 24 | entryPoint.withdrawTo(to, amount); 25 | } 26 | 27 | receive() external payable { 28 | entryPoint.depositTo{value: msg.value}(address(this)); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /tests/contracts/Helper.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.25; 2 | //SPDX-License-Identifier: MIT 3 | 4 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 5 | 6 | contract Helper { 7 | 8 | //helper to return address (ep.getSenderAddress returns the address as an exception, which is hard to catch) 9 | function getSenderAddress(IEntryPoint ep, bytes memory initCode) public returns (address addr) { 10 | try ep.getSenderAddress(initCode) { 11 | revert("expected to revert with SenderAddressResult"); 12 | } 13 | catch(bytes memory ret) { 14 | (bool success, bytes memory ret1) = address(this).call(ret); 15 | require(success, string.concat("wrong error sig ", string(ret))); 16 | addr = abi.decode(ret1, (address)); 17 | } 18 | } 19 | 20 | //helper to parse the "error SenderAddressResult" (by exposing same inteface 21 | function SenderAddressResult(address sender) external returns (address){ 22 | return sender; 23 | } 24 | 25 | function getUserOpHash(IEntryPoint ep, PackedUserOperation calldata userOp) public view returns (bytes32) { 26 | return IEntryPoint(ep).getUserOpHash(userOp); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/contracts/TestReputationAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 5 | import "./State.sol"; 6 | import "./ITestAccount.sol"; 7 | import "./Stakable.sol"; 8 | 9 | contract TestReputationAccount is ITestAccount, Stakable { 10 | 11 | IEntryPoint ep; 12 | uint256 public state; 13 | 14 | constructor(address _ep) payable { 15 | ep = IEntryPoint(_ep); 16 | (bool req,) = address(ep).call{value : msg.value}(""); 17 | require(req); 18 | } 19 | 20 | function setState(uint _state) external { 21 | state=_state; 22 | } 23 | 24 | function funTSTORE() external returns(uint256) { 25 | revert("not used"); 26 | } 27 | 28 | function validateUserOp(PackedUserOperation calldata userOp, bytes32, uint256 missingWalletFunds) 29 | public override virtual returns (uint256 validationData) { 30 | require(state != 0xdead, "No bundle for you"); 31 | if (missingWalletFunds>0) { 32 | msg.sender.call{value:missingWalletFunds}(""); 33 | } 34 | return 0; 35 | } 36 | 37 | receive() external payable { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/p2p/test_p2p.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import pytest 3 | import os 4 | import time 5 | 6 | from tests.types import UserOperation 7 | from tests.utils import ( 8 | clear_mempool, 9 | deploy_and_deposit, 10 | dump_mempool, 11 | p2p_mempool, 12 | set_manual_bundling_mode, 13 | ) 14 | 15 | 16 | # todo: relies on 'ports: [ "3001:3000" ]' for peer bundler. 17 | BUNDLER2 = "http://localhost:3001/rpc" 18 | 19 | # todo: this is the "real" bundler2 definition. 20 | # However, it can only work if the script runs inside the docker-compose environment. 21 | # BUNDLER2 = "http://bundler2:3000/rpc" 22 | 23 | 24 | # Sanity test: make sure a simple userop is propagated 25 | def test_simple_p2p(w3, entrypoint_contract, manual_bundling_mode): 26 | wallet = deploy_and_deposit(w3, entrypoint_contract, "SimpleWallet", False) 27 | op = UserOperation(sender=wallet.address) 28 | 29 | set_manual_bundling_mode(BUNDLER2) 30 | clear_mempool() 31 | clear_mempool(BUNDLER2) 32 | 33 | ref = dump_mempool(BUNDLER2) 34 | op.send() 35 | assert dump_mempool() == [op], "failed to appear in same mempool" 36 | assert p2p_mempool(ref, url=BUNDLER2) == [ 37 | op 38 | ], "failed to propagate to remote mempool" 39 | -------------------------------------------------------------------------------- /tests/single/rpc/test_eth_getUserOperationReceipt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from jsonschema import validate, Validator 3 | 4 | from tests.types import RPCRequest, RPCErrorCode 5 | from tests.utils import userop_hash, assert_rpc_error 6 | 7 | 8 | @pytest.mark.usefixtures("execute_user_operation") 9 | @pytest.mark.parametrize("schema_method", ["eth_getUserOperationReceipt"], ids=[""]) 10 | def test_eth_getUserOperationReceipt(helper_contract, userop, w3, schema): 11 | response = RPCRequest( 12 | method="eth_getUserOperationReceipt", 13 | params=[userop_hash(helper_contract, userop)], 14 | ).send() 15 | assert response.result["userOpHash"] == userop_hash(helper_contract, userop) 16 | receipt = w3.eth.get_transaction_receipt( 17 | response.result["receipt"]["transactionHash"] 18 | ) 19 | assert response.result["receipt"]["blockHash"] == receipt["blockHash"].to_0x_hex() 20 | Validator.check_schema(schema) 21 | validate(instance=response.result, schema=schema) 22 | 23 | 24 | def test_eth_getUserOperationReceipt_error(): 25 | response = RPCRequest(method="eth_getUserOperationReceipt", params=[""]).send() 26 | assert_rpc_error( 27 | response, "Missing/invalid userOpHash", RPCErrorCode.INVALID_FIELDS 28 | ) 29 | -------------------------------------------------------------------------------- /tests/contracts/ValidationRulesStorage.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 5 | import "./ITestAccount.sol"; 6 | 7 | contract ValidationRulesStorage is IState { 8 | IEntryPoint public entryPoint; 9 | uint256 public state; 10 | 11 | function funTSTORE() external override returns(uint256) { 12 | assembly { 13 | tstore(0, 1) 14 | } 15 | return 0; 16 | } 17 | 18 | function funSSTORE() external returns(uint256) { 19 | assembly { 20 | sstore(0, 1) 21 | } 22 | return 0; 23 | } 24 | 25 | 26 | function funTLOAD() external returns(uint256) { 27 | uint256 tval; 28 | assembly { 29 | tval := tload(0) 30 | } 31 | emit State(tval, tval); 32 | return tval; 33 | } 34 | 35 | event State(uint oldState, uint newState); 36 | 37 | function setState(uint _state) public { 38 | emit State(state, _state); 39 | state = _state; 40 | } 41 | 42 | function revertOOG() public { 43 | uint256 i = 0; 44 | while(true) { 45 | keccak256(abi.encode(i++)); 46 | } 47 | } 48 | 49 | function revertOOGSSTORE() public { 50 | state += 1; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/contracts/TestCoin.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | contract TestCoin { 5 | mapping(address => uint) balances; 6 | 7 | struct Struct { 8 | uint a; 9 | uint b; 10 | uint c; 11 | } 12 | mapping(address=>Struct) public structInfo; 13 | 14 | function setStructMember(address addr) public returns (uint){ 15 | return structInfo[addr].c = 3; 16 | } 17 | 18 | function balanceOf(address addr) public returns (uint) { 19 | return balances[addr]; 20 | } 21 | 22 | function mint(address addr) public returns (uint) { 23 | return balances[addr] += 100; 24 | } 25 | 26 | //unrelated to token: testing inner object revert 27 | function reverting() public returns (uint) { 28 | revert("inner-revert"); 29 | } 30 | 31 | function wasteGas() public returns (uint) { 32 | string memory buffer = "string to be duplicated"; 33 | while (true) { 34 | buffer = string.concat(buffer, buffer); 35 | } 36 | return 0; 37 | } 38 | 39 | function receiveValue() public payable returns (uint256) { 40 | require(msg.value > 0, "value is zero"); 41 | return msg.value; 42 | } 43 | 44 | function destruct() public { 45 | selfdestruct(payable(msg.sender)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/TestPostOpPaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "../Stakable.sol"; 5 | import "./TestAccount.sol"; 6 | import {IRip7560Paymaster} from "@rip7560/contracts/interfaces/IRip7560Paymaster.sol"; 7 | import "../ValidationRules.sol"; 8 | 9 | contract TestPostOpPaymaster is IRip7560Paymaster { 10 | using ValidationRules for string; 11 | uint256 public counter; 12 | constructor() payable {} 13 | 14 | function validatePaymasterTransaction( 15 | uint256 version, 16 | bytes32 txHash, 17 | bytes calldata transaction) 18 | external { 19 | RIP7560Transaction memory txStruct = RIP7560Utils.decodeTransaction(version, transaction); 20 | bytes memory context = txStruct.authorizationData; 21 | if (string(context).eq("no context")) { 22 | context = ""; 23 | } 24 | RIP7560Utils.paymasterAcceptTransaction(context, 1, type(uint48).max - 1); 25 | } 26 | 27 | function postPaymasterTransaction( 28 | bool success, 29 | uint256 actualGasCost, 30 | bytes calldata context 31 | ) external { 32 | string memory rule = string(context); 33 | if (rule.eq("revert")) { 34 | revert("post op revert message"); 35 | } 36 | counter++; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tests/single/rpc/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import pytest 5 | 6 | from tests.user_operation_erc4337 import UserOperation 7 | 8 | 9 | @pytest.fixture 10 | def bad_sig_userop(wallet_contract): 11 | return UserOperation( 12 | sender=wallet_contract.address, 13 | callData=wallet_contract.encode_abi( 14 | abi_element_identifier="setState", args=[1111111] 15 | ), 16 | signature="0xdead", 17 | ) 18 | 19 | 20 | @pytest.fixture 21 | def invalid_sig_userop(wallet_contract): 22 | return UserOperation( 23 | sender=wallet_contract.address, 24 | callData=wallet_contract.encode_abi( 25 | abi_element_identifier="setState", args=[1111111] 26 | ), 27 | signature="0xdeaf", 28 | ) 29 | 30 | 31 | @pytest.fixture(scope="session") 32 | def openrpcschema(): 33 | current_dirname = os.path.dirname(__file__) 34 | spec_filename = "openrpc.json" 35 | spec_path = os.path.realpath(current_dirname + "/../../../spec/") 36 | with open(os.path.join(spec_path, spec_filename), encoding="utf-8") as contractfile: 37 | return json.load(contractfile) 38 | 39 | 40 | @pytest.fixture 41 | def schema(openrpcschema, schema_method): 42 | return next( 43 | m["result"]["schema"] 44 | for m in openrpcschema["methods"] 45 | if m["name"] == schema_method 46 | ) 47 | -------------------------------------------------------------------------------- /tests/single/rpc/test_eth_getUserOperationByHash.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from jsonschema import validate, Validator 3 | from tests.types import RPCRequest, CommandLineArgs, RPCErrorCode 4 | from tests.user_operation_erc4337 import UserOperation 5 | from tests.utils import userop_hash, assert_rpc_error 6 | 7 | 8 | @pytest.mark.usefixtures("execute_user_operation") 9 | @pytest.mark.parametrize("schema_method", ["eth_getUserOperationByHash"], ids=[""]) 10 | def test_eth_getUserOperationByHash(helper_contract, userop, schema): 11 | response = RPCRequest( 12 | method="eth_getUserOperationByHash", 13 | params=[userop_hash(helper_contract, userop)], 14 | ).send() 15 | assert userop_hash( 16 | helper_contract, UserOperation(**response.result["userOperation"]) 17 | ) == userop_hash(helper_contract, userop), "user operation mismatch" 18 | assert ( 19 | response.result["entryPoint"] == CommandLineArgs.entrypoint 20 | ), "wrong entrypoint" 21 | assert response.result["blockNumber"], "no block number" 22 | assert response.result["blockHash"], "no block hash" 23 | Validator.check_schema(schema) 24 | validate(instance=response.result, schema=schema) 25 | 26 | 27 | def test_eth_getUserOperationByHash_error(): 28 | response = RPCRequest(method="eth_getUserOperationByHash", params=[""]).send() 29 | assert_rpc_error( 30 | response, "Missing/invalid userOpHash", RPCErrorCode.INVALID_FIELDS 31 | ) 32 | -------------------------------------------------------------------------------- /tests/contracts/TestFakeWalletPaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@account-abstraction/contracts/interfaces/IAccount.sol"; 5 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 6 | import "../../@account-abstraction/contracts/interfaces/IPaymaster.sol"; 7 | 8 | /// @dev A test contract that represents a potential attack where a wallet entity is also 9 | /// used as a Paymaster by a different PackedUserOperation. 10 | /// This allows this couple of UserOperations to escape the sandbox and invalidate a bundle. 11 | contract TestFakeWalletPaymaster is IAccount, IPaymaster { 12 | 13 | IEntryPoint entryPoint; 14 | 15 | constructor(address _ep) payable { 16 | entryPoint = IEntryPoint(_ep); 17 | } 18 | 19 | function validateUserOp(PackedUserOperation calldata userOp, bytes32, uint256 missingWalletFunds) 20 | public override returns (uint256 validationData) { 21 | validationData = 0; 22 | } 23 | 24 | function validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) 25 | external returns (bytes memory context, uint256 validationData){ 26 | context = ""; 27 | validationData = 0; 28 | } 29 | 30 | function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint) external {} 31 | 32 | receive() external payable { 33 | entryPoint.depositTo{value: msg.value}(address(this)); 34 | } 35 | 36 | fallback() external {} 37 | } 38 | -------------------------------------------------------------------------------- /tests/single/rpc/test_eth_estimateUserOperationGas.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test suite for `eip4337 bunlder` module. 3 | See https://github.com/eth-infinitism/bundler 4 | """ 5 | 6 | from dataclasses import asdict 7 | import pytest 8 | from jsonschema import validate, Validator 9 | from tests.types import RPCRequest, CommandLineArgs, RPCErrorCode 10 | from tests.user_operation_erc4337 import UserOperation 11 | from tests.utils import assert_rpc_error 12 | 13 | 14 | @pytest.mark.parametrize("schema_method", ["eth_estimateUserOperationGas"], ids=[""]) 15 | def test_eth_estimateUserOperationGas(userop: UserOperation, schema): 16 | response = RPCRequest( 17 | method="eth_estimateUserOperationGas", 18 | params=[asdict(userop), CommandLineArgs.entrypoint], 19 | ).send() 20 | Validator.check_schema(schema) 21 | validate(instance=response.result, schema=schema) 22 | 23 | 24 | def test_eth_estimateUserOperationGas_execution_revert( 25 | wallet_contract, userop: UserOperation 26 | ): 27 | userop.callData = wallet_contract.encode_abi(abi_element_identifier="fail") 28 | response = RPCRequest( 29 | method="eth_estimateUserOperationGas", 30 | params=[asdict(userop), CommandLineArgs.entrypoint], 31 | ).send() 32 | assert_rpc_error(response, "", RPCErrorCode.EXECUTION_REVERTED) 33 | 34 | 35 | def test_eth_estimateUserOperationGas_simulation_revert(bad_sig_userop: UserOperation): 36 | response = RPCRequest( 37 | method="eth_estimateUserOperationGas", 38 | params=[asdict(bad_sig_userop), CommandLineArgs.entrypoint], 39 | ).send() 40 | assert_rpc_error(response, "dead signature", RPCErrorCode.REJECTED_BY_EP_OR_ACCOUNT) 41 | -------------------------------------------------------------------------------- /tests/single/rpc/test_eth_sendUserOperation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test suite for `eip4337 bunlder` module. 3 | See https://github.com/eth-infinitism/bundler 4 | """ 5 | 6 | import pytest 7 | from jsonschema import validate, Validator 8 | from tests.types import RPCErrorCode 9 | from tests.utils import userop_hash, assert_rpc_error, send_bundle_now 10 | 11 | 12 | @pytest.mark.parametrize("schema_method", ["eth_sendUserOperation"], ids=[""]) 13 | def test_eth_sendUserOperation(w3, wallet_contract, helper_contract, userop, schema): 14 | state_before = wallet_contract.functions.state().call() 15 | assert state_before == 0 16 | response = userop.send() 17 | send_bundle_now() 18 | state_after = wallet_contract.functions.state().call() 19 | assert response.result == userop_hash(helper_contract, userop) 20 | assert state_after == 1111111 21 | Validator.check_schema(schema) 22 | validate(instance=response.result, schema=schema) 23 | 24 | 25 | def test_eth_sendUserOperation_revert(w3, wallet_contract, bad_sig_userop): 26 | state_before = wallet_contract.functions.state().call() 27 | assert state_before == 0 28 | response = bad_sig_userop.send() 29 | send_bundle_now() 30 | state_after = wallet_contract.functions.state().call() 31 | assert state_after == 0 32 | assert_rpc_error( 33 | response, "testWallet: dead signature", RPCErrorCode.REJECTED_BY_EP_OR_ACCOUNT 34 | ) 35 | 36 | 37 | def test_eth_sendUserOperation_invalid_signature(invalid_sig_userop): 38 | response = invalid_sig_userop.send() 39 | assert_rpc_error( 40 | response, 41 | response.message, 42 | RPCErrorCode.INVALID_SIGNATURE, 43 | ) 44 | -------------------------------------------------------------------------------- /tests/contracts/TestCodeHashFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "@account-abstraction/contracts/interfaces/IAccount.sol"; 5 | 6 | contract TestCodeHashAccount is IAccount { 7 | uint public immutable num; 8 | 9 | constructor(address ep, TestCodeHashFactory factory) payable { 10 | (bool req,) = address(ep).call{value : msg.value}(""); 11 | require(req); 12 | num = factory.num(); 13 | } 14 | 15 | function destruct() public { 16 | selfdestruct(payable(msg.sender)); 17 | } 18 | function validateUserOp(PackedUserOperation calldata userOp, bytes32, uint256 missingWalletFunds) 19 | external override returns (uint256 deadline) { 20 | if (missingWalletFunds>0) { 21 | msg.sender.call{value:missingWalletFunds}(""); 22 | } 23 | // require(num == userOp.nonce, "Reverting second simulation"); 24 | } 25 | 26 | receive() external payable {} 27 | } 28 | 29 | 30 | contract TestCodeHashFactory { 31 | uint public num; 32 | event ContractCreated(address account); 33 | function destroy(TestCodeHashAccount account) public { 34 | account.destruct(); 35 | } 36 | function getNums(TestCodeHashAccount account) public view returns (uint, uint) { 37 | return (num, account.num()); 38 | } 39 | function create(uint nonce, uint _num, address entrypoint) public payable returns (TestCodeHashAccount) { 40 | num = _num; 41 | TestCodeHashAccount newAccount = new TestCodeHashAccount{salt : bytes32(nonce), value: msg.value}(entrypoint, this); 42 | emit ContractCreated(address(newAccount)); 43 | return newAccount; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/single/bundle/test_paymaster.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import pytest 3 | from eth_utils import to_hex 4 | 5 | from tests.types import RPCErrorCode 6 | from tests.user_operation_erc4337 import UserOperation 7 | from tests.utils import ( 8 | assert_ok, 9 | assert_rpc_error, 10 | deploy_wallet_contract, 11 | deploy_contract, 12 | get_userop_max_cost, 13 | ) 14 | 15 | 16 | # EREP-010: paymaster should have deposit to cover all userops in mempool 17 | @pytest.mark.usefixtures("manual_bundling_mode") 18 | def test_paymaster_deposit(w3, entrypoint_contract, paymaster_contract): 19 | """ 20 | test paymaster has deposit to cover all userops in mempool. 21 | make paymaster deposit enough for 2 userops. 22 | send 2 userops. 23 | see that the 3rd userop is dropped. 24 | """ 25 | paymaster = deploy_contract(w3, "TestRulesPaymaster", [entrypoint_contract.address]) 26 | userops = [] 27 | for i in range(3): 28 | sender = deploy_wallet_contract(w3).address 29 | userop = UserOperation( 30 | sender=sender, 31 | paymaster=paymaster.address, 32 | paymasterVerificationGasLimit=to_hex(50000), 33 | paymasterData=to_hex(text="nothing"), 34 | ) 35 | userops.append(userop) 36 | 37 | sums = [get_userop_max_cost(userop) for userop in userops] 38 | total_cost = sum(sums) 39 | 40 | # deposit enough just below the total cost 41 | entrypoint_contract.functions.depositTo(paymaster.address).transact( 42 | {"from": w3.eth.default_account, "value": total_cost - 1} 43 | ) 44 | for u in userops[0:-1]: 45 | assert_ok(u.send()) 46 | 47 | res = userops[-1].send() 48 | assert_rpc_error(res, "too low", RPCErrorCode.PAYMASTER_DEPOSIT_TOO_LOW) 49 | -------------------------------------------------------------------------------- /tests/contracts/SimpleWallet.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 5 | import "./State.sol"; 6 | import "./ITestAccount.sol"; 7 | 8 | contract SimpleWallet is ITestAccount { 9 | 10 | IEntryPoint ep; 11 | uint256 public state; 12 | 13 | function funTSTORE() external returns(uint256) { 14 | assembly { 15 | tstore(0, 1) 16 | } 17 | return 0; 18 | } 19 | 20 | constructor(address _ep) payable { 21 | ep = IEntryPoint(_ep); 22 | (bool req,) = address(ep).call{value : msg.value}(""); 23 | require(req); 24 | } 25 | 26 | function addStake(IEntryPoint _ep, uint32 delay) public payable { 27 | _ep.addStake{value: msg.value}(delay); 28 | } 29 | 30 | function setState(uint _state) external { 31 | state=_state; 32 | } 33 | 34 | function wasteGas() external { 35 | uint i = 0; 36 | while (gasleft() > 10000) { 37 | i++; 38 | } 39 | } 40 | 41 | 42 | function fail() external { 43 | revert("test fail"); 44 | } 45 | 46 | function validateUserOp(PackedUserOperation calldata userOp, bytes32, uint256 missingWalletFunds) 47 | public override virtual returns (uint256 validationData) { 48 | if (userOp.callData.length == 20) { 49 | State(address(bytes20(userOp.callData))).getState(address(this)); 50 | } 51 | 52 | if (missingWalletFunds>0) { 53 | msg.sender.call{value:missingWalletFunds}(""); 54 | } 55 | bytes2 sig = bytes2(userOp.signature); 56 | require(sig != 0xdead, "testWallet: dead signature"); 57 | return sig == 0xdeaf ? 1 : 0; 58 | } 59 | 60 | receive() external payable { 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/contracts/TestRulesAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "./ITestAccount.sol"; 5 | import "./Stakable.sol"; 6 | import "./TestRulesTarget.sol"; 7 | import "./UserOpGetters.sol"; 8 | import "./ValidationRules.sol"; 9 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 10 | 11 | contract TestRulesAccount is Stakable, ValidationRulesStorage, ITestAccount { 12 | 13 | using ValidationRules for string; 14 | using UserOpGetters for PackedUserOperation; 15 | 16 | TestCoin public coin; 17 | TestRulesTarget public immutable target = new TestRulesTarget(); 18 | 19 | function setCoin(TestCoin _coin) public { 20 | coin = _coin; 21 | } 22 | 23 | constructor(address _ep) payable { 24 | entryPoint = IEntryPoint(_ep); 25 | if (_ep != address(0) && msg.value > 0) { 26 | (bool req,) = address(_ep).call{value : msg.value}(""); 27 | require(req); 28 | } 29 | // true only when deploying through TestRulesAccountFactory, in which case the factory sets the coin 30 | if (msg.sender.code.length == 0) { 31 | coin = new TestCoin(); 32 | } 33 | } 34 | 35 | receive() external payable {} 36 | 37 | function validateUserOp(PackedUserOperation calldata userOp, bytes32, uint256 missingAccountFunds) 38 | external override returns (uint256 deadline) { 39 | if (missingAccountFunds > 0) { 40 | /* solhint-disable-next-line avoid-low-level-calls */ 41 | (bool success,) = msg.sender.call{value : missingAccountFunds}(""); 42 | success; 43 | } 44 | string memory rule = string(userOp.signature); 45 | ValidationRules.runRule(rule, this, userOp.getPaymaster(), userOp.getFactory(), coin, this, target); 46 | return 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/RIP7560TestRulesAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.15; 3 | 4 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 5 | 6 | import "../ValidationRules.sol"; 7 | 8 | import "./TestAccount.sol"; 9 | import "@rip7560/contracts/interfaces/IRip7560Transaction.sol"; 10 | import "../Stakable.sol"; 11 | 12 | interface IRip7560EntryPointWrong { 13 | function acceptAccountWrongSig(uint256 validAfter, uint256 validUntil) external; 14 | } 15 | 16 | contract RIP7560TestRulesAccount is ValidationRulesStorage, Stakable { 17 | 18 | using ValidationRules for string; 19 | 20 | TestCoin public coin; 21 | 22 | constructor() payable { 23 | // true only when deploying through TestRulesAccountFactory, in which case the factory sets the coin 24 | if (msg.sender.code.length == 0) { 25 | coin = new TestCoin{salt:bytes32(0)}(); 26 | } 27 | } 28 | 29 | receive() external payable {} 30 | 31 | function setCoin(TestCoin _coin) public { 32 | coin = _coin; 33 | } 34 | 35 | 36 | function validateTransaction( 37 | uint256 version, 38 | bytes32 txHash, 39 | bytes calldata transaction 40 | ) external { 41 | RIP7560Transaction memory txStruct = RIP7560Utils.decodeTransaction(version, transaction); 42 | string memory rule = string(txStruct.authorizationData); 43 | if (ValidationRules.eq(rule, "wrong-callback-method")) { 44 | ENTRY_POINT.call(abi.encodeCall(IRip7560EntryPointWrong.acceptAccountWrongSig, (666, 777))); 45 | return; 46 | } 47 | ValidationRules.runRule(rule, this, txStruct.paymaster, txStruct.deployer, coin, this, TestRulesTarget(payable(0))); 48 | setState(1); 49 | RIP7560Utils.accountAcceptTransaction(1, type(uint48).max - 1); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/rip7560/test_rip7712.py: -------------------------------------------------------------------------------- 1 | from eth_abi.packed import encode_packed 2 | 3 | from tests.types import RPCErrorCode 4 | from tests.utils import send_bundle_now, assert_rpc_error, assert_ok, to_number 5 | 6 | 7 | def get_nonce(w3, nonce_manager, address, key): 8 | get_nonce_call_data = encode_packed(["address", "uint256"], [address, key]) 9 | return w3.eth.call({"to": nonce_manager.address, "data": get_nonce_call_data}) 10 | 11 | 12 | def test_eth_sendTransaction7560_7712_valid( 13 | w3, wallet_contract, nonce_manager, tx_7560 14 | ): 15 | key = 777 16 | 17 | state_before = wallet_contract.functions.state().call() 18 | nonce_before = get_nonce(w3, nonce_manager, wallet_contract.address, key) 19 | legacy_nonce_before = w3.eth.get_transaction_count(tx_7560.sender) 20 | assert state_before == 0 21 | assert to_number(nonce_before.hex()) == 0 22 | assert legacy_nonce_before == 1 23 | 24 | tx_7560.nonceKey = hex(key) 25 | tx_7560.nonce = hex(0) 26 | assert_ok(tx_7560.send()) 27 | send_bundle_now() 28 | 29 | state_after = wallet_contract.functions.state().call() 30 | nonce_after = get_nonce(w3, nonce_manager, wallet_contract.address, key) 31 | legacy_nonce_after = w3.eth.get_transaction_count(tx_7560.sender) 32 | assert to_number(nonce_after.hex()) == 1 33 | assert state_after == 2 34 | assert legacy_nonce_after == 1 35 | 36 | 37 | def test_eth_sendTransaction7560_7712_failed(wallet_contract, tx_7560): 38 | key = 777 39 | tx_7560.nonceKey = hex(key) 40 | tx_7560.nonce = hex(0) 41 | tx_7560.send() 42 | send_bundle_now() 43 | state_after = wallet_contract.functions.state().call() 44 | assert state_after == 2 45 | 46 | ret = tx_7560.send() 47 | assert_rpc_error( 48 | ret, 49 | "rip-7712 nonce validation failed: execution reverted", 50 | RPCErrorCode.INVALID_INPUT, 51 | ) 52 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/TestAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@rip7560/contracts/interfaces/IRip7560Transaction.sol"; 5 | import "@rip7560/contracts/utils/RIP7560Utils.sol"; 6 | import "./utils/TestUtils.sol"; 7 | import "@rip7560/contracts/interfaces/IRip7560Account.sol"; 8 | 9 | contract TestAccount is IRip7560Account { 10 | uint256 public accCounter = 0; 11 | uint256 public state = 0; 12 | 13 | event Funded(string id, uint256 amount); 14 | 15 | event AccountValidationEvent(uint256 state, uint256 counter); 16 | 17 | event AccountExecutionEvent(uint256 state, uint256 counter, bytes data); 18 | 19 | constructor() payable { 20 | } 21 | 22 | function validateTransaction( 23 | uint256 version, 24 | bytes32 txHash, 25 | bytes calldata transaction 26 | ) public virtual { 27 | 28 | emit AccountValidationEvent(state, accCounter); 29 | 30 | /* Modify account state */ 31 | accCounter++; 32 | state = 1; 33 | 34 | RIP7560Utils.accountAcceptTransaction(1, type(uint48).max - 1); 35 | } 36 | 37 | function anyExecutionFunction() external { 38 | TestUtils.emitEvmData("anyExecutionFunction"); 39 | 40 | emit AccountExecutionEvent(state, accCounter, msg.data); 41 | 42 | state = 2; 43 | } 44 | 45 | function revertingFunction() external { 46 | revert("reverting"); 47 | } 48 | 49 | function reset() external { 50 | state = 0; 51 | accCounter = 0; 52 | } 53 | 54 | receive() external payable { 55 | emit Funded("account", msg.value); 56 | } 57 | 58 | fallback(bytes calldata) external returns (bytes memory) { 59 | // accCounter++; 60 | // emit AccountEvent("account", string(msg.data)); 61 | return "account-returned-data-here"; 62 | } 63 | 64 | function funTSTORE() external returns(uint256) { 65 | assembly { 66 | tstore(0, 1) 67 | } 68 | return 0; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/contracts/TestFakeWalletToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@account-abstraction/contracts/interfaces/IAccount.sol"; 5 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 6 | 7 | /// @dev A test contract that represents a potential attack where a wallet entity is also 8 | /// used as an associated storage by a different PackedUserOperation. 9 | /// This allows this couple of UserOperations to escape the sandbox and invalidate a bundle. 10 | contract TestFakeWalletToken is IAccount { 11 | 12 | mapping(address => uint256) private balances; 13 | TestFakeWalletToken public anotherWallet; 14 | IEntryPoint ep; 15 | 16 | constructor(address _ep) payable { 17 | ep = IEntryPoint(_ep); 18 | } 19 | 20 | function balanceOf(address _owner) public view returns (uint256 balance) { 21 | return balances[_owner]; 22 | } 23 | 24 | function sudoSetBalance(address _owner, uint256 balance) public { 25 | balances[_owner] = balance; 26 | } 27 | 28 | function sudoSetAnotherWallet(TestFakeWalletToken _anotherWallet) public { 29 | anotherWallet = _anotherWallet; 30 | } 31 | 32 | function validateUserOp(PackedUserOperation calldata userOp, bytes32, uint256 missingWalletFunds) 33 | public override returns (uint256 validationData) { 34 | if (missingWalletFunds>0) { 35 | msg.sender.call{value:missingWalletFunds}(""); 36 | } 37 | if (userOp.callData.length == 20) { 38 | // the first PackedUserOperation sets the second sender's "associated" balance to 0 39 | address senderToDrain = address(bytes20(userOp.callData[:20])); 40 | balances[senderToDrain] = 0; 41 | } else { 42 | // the second PackedUserOperation will hit this only if included in a bundle with the first one 43 | require(anotherWallet.balanceOf(address(this)) > 0, "no balance"); 44 | } 45 | return 0; 46 | } 47 | 48 | receive() external payable { 49 | } 50 | 51 | fallback() external { 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EIP4337 bundler compatibility tests. 2 | 3 | ## Version 0.8 4 | 5 | For more information on the motivation and importance of having a compatibility test suite, see https://notes.ethereum.org/@yoav/unified-erc-4337-mempool 6 | 7 | For the formal schema EIP-4337 bundler RPC API spec, see https://github.com/eth-infinitism/bundler-spec 8 | 9 | The spec test for previous releases 10 | - [releases/v0.7](https://github.com/eth-infinitism/bundler-spec-tests/tree/releases/v0.7) 11 | - [releases/v0.6](https://github.com/eth-infinitism/bundler-spec-tests/tree/releases/v0.6) 12 | 13 | #### Prerequisites 14 | 15 | Python version 3.8+ 16 | PDM - python package and dependency manager version 2.2.1+ 17 | 18 | #### Installation 19 | Run `pdm install && pdm run update-deps` 20 | 21 | #### Running the tests 22 | 23 | ##### Running with an up and running Ethereum node and bundler 24 | Assuming you already have an Ethereum node running, EntryPoint deployed and your bundler running and ready for requests, you can run the test suite with: 25 | ```shell script 26 | pdm test 27 | ``` 28 | With the following parameters: 29 | 30 | * **--url** the bundler to test (defaults to http://localhost:3000) 31 | * **--entry-point** (defaults to `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`) 32 | * **--ethereum-node** (defaults to http://localhost:8545) 33 | * **--launcher-script** (See below) 34 | * **-k** <regex>, (or any other pytest param) 35 | 36 | ##### Running with a launcher script 37 | You can provide a launcher script by adding the option `--launcher-script` to the command line. 38 | 39 | Your launcher script will be invoked by the shell with: 40 | ```shell script 41 | {start|stop|restart} 42 | ``` 43 | where: 44 | - `start` should start an Ethereum node, deploy an EntryPoint contract and start your bundler. 45 | - `stop` should terminate both the Ethereum node and your bundler processes, and cleanup if necessary. 46 | - `restart` should stop and start atomically. 47 | 48 | 49 | ##### Running using the "test executor" 50 | 51 | See https://github.com/eth-infinitism/bundler-test-executor, for the test executor to run the test suite against all registered bundler implementations. -------------------------------------------------------------------------------- /tests/contracts/TestRulesPaymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "@account-abstraction/contracts/interfaces/IPaymaster.sol"; 5 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 6 | import "@account-abstraction/contracts/core/UserOperationLib.sol"; 7 | import "./ValidationRules.sol"; 8 | import "./UserOpGetters.sol"; 9 | import "./TestRulesTarget.sol"; 10 | import "./SimpleWallet.sol"; 11 | 12 | contract TestRulesPaymaster is IPaymaster, ValidationRulesStorage { 13 | using ValidationRules for string; 14 | using UserOpGetters for PackedUserOperation; 15 | 16 | TestCoin immutable public coin = new TestCoin(); 17 | TestRulesTarget private immutable target = new TestRulesTarget(); 18 | // IEntryPoint public entryPoint; 19 | 20 | constructor(address _ep) payable { 21 | entryPoint = IEntryPoint(_ep); 22 | if (_ep != address(0)) { 23 | (bool req,) = address(_ep).call{value : msg.value}(""); 24 | require(req); 25 | } 26 | } 27 | 28 | function addStake(IEntryPoint ep, uint32 delay) public payable { 29 | ep.addStake{value: msg.value}(delay); 30 | } 31 | 32 | function validatePaymasterUserOp(PackedUserOperation calldata userOp, bytes32, uint256) 33 | external returns (bytes memory context, uint256 deadline) { 34 | 35 | //first byte after paymaster address. 36 | string memory rule = string(userOp.paymasterAndData[UserOperationLib.PAYMASTER_DATA_OFFSET:]); 37 | if (rule.includes("context")) { 38 | return ("this is a context", 0); 39 | } else if (rule.includes("nothing")) { 40 | return ("", 0); 41 | } else { 42 | ValidationRules.runRule( 43 | rule, 44 | ITestAccount(userOp.sender), 45 | userOp.getPaymaster(), 46 | userOp.getFactory(), 47 | coin, 48 | this, 49 | target 50 | ); 51 | return ("", 0); 52 | } 53 | } 54 | 55 | function postOp(PostOpMode mode, bytes calldata context, uint256 actualGasCost, uint) external {} 56 | 57 | receive() external payable { 58 | entryPoint.depositTo{value: msg.value}(address(this)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/RIP7560Paymaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "../TestCoin.sol"; 5 | import "../ValidationRules.sol"; 6 | 7 | import "@rip7560/contracts/utils/RIP7560Utils.sol"; 8 | import "@rip7560/contracts/interfaces/IRip7560Transaction.sol"; 9 | import "../Stakable.sol"; 10 | 11 | interface IRip7560EntryPointWrong { 12 | function acceptPaymasterWrongSig(uint256 validAfter, uint256 validUntil, bytes calldata context) external; 13 | } 14 | 15 | contract RIP7560Paymaster is ValidationRulesStorage, Stakable { 16 | using ValidationRules for string; 17 | TestCoin immutable public coin = new TestCoin(); 18 | TestRulesTarget private immutable target = new TestRulesTarget(); 19 | 20 | uint256 public pmCounter = 0; 21 | 22 | event Funded(string id, uint256 amount); 23 | event PaymasterValidationEvent(string name, uint256 counter); 24 | event PaymasterPostTxEvent(string name, uint256 counter, bytes context); 25 | 26 | constructor() payable {} 27 | 28 | function validatePaymasterTransaction( 29 | uint256 version, 30 | bytes32 txHash, 31 | bytes calldata transaction) 32 | external 33 | { 34 | bytes memory context = abi.encodePacked("context here"); 35 | RIP7560Transaction memory txStruct = RIP7560Utils.decodeTransaction(version, transaction); 36 | string memory rule = string(txStruct.paymasterData); 37 | if (ValidationRules.eq(rule, "wrong-callback-method")) { 38 | ENTRY_POINT.call(abi.encodeCall(IRip7560EntryPointWrong.acceptPaymasterWrongSig, (666, 777, bytes("wrong context")))); 39 | return; 40 | } 41 | if (!rule.eq("context")) { 42 | ValidationRules.runRule(rule, ITestAccount(txStruct.sender), txStruct.paymaster, txStruct.deployer, coin, this, target); 43 | } 44 | RIP7560Utils.paymasterAcceptTransaction(context, 1, type(uint48).max -1 ); 45 | } 46 | 47 | function postPaymasterTransaction( 48 | bool success, 49 | uint256 actualGasCost, 50 | bytes calldata context 51 | ) external { 52 | emit PaymasterPostTxEvent("the-paymaster", pmCounter, context); 53 | } 54 | 55 | receive() external payable { 56 | emit Funded("account", msg.value); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/single/reputation/test_replace.py: -------------------------------------------------------------------------------- 1 | from tests.conftest import assert_ok, deploy_and_deposit, paymaster_contract 2 | from tests.single.bundle.test_bundle import bump_fee_by 3 | from tests.utils import dump_mempool, dump_reputation, to_number 4 | 5 | 6 | def bump_gas_fees(userop): 7 | userop.maxPriorityFeePerGas = hex( 8 | bump_fee_by(to_number(userop.maxPriorityFeePerGas), 15) 9 | ) 10 | userop.maxFeePerGas = hex(bump_fee_by(to_number(userop.maxFeePerGas), 15)) 11 | 12 | 13 | def pm_opsSeen(pmAddr): 14 | reputation = dump_reputation() 15 | for entry in reputation: 16 | if entry["address"].lower() == pmAddr.lower(): 17 | return to_number(entry["opsSeen"]) 18 | 19 | 20 | # when userop is replaced, old userop opsSeen increments should be reverted 21 | def test_replace_paymaster(w3, manual_bundling_mode, entrypoint_contract, userop): 22 | pm1 = deploy_and_deposit(w3, entrypoint_contract, "TestRulesPaymaster", False) 23 | 24 | userop.paymaster = pm1.address 25 | userop.paymasterVerificationGasLimit = hex(100000) 26 | userop.paymasterPostOpGasLimit = hex(100000) 27 | userop.paymasterData = "0x" 28 | 29 | assert_ok(userop.send()) 30 | assert dump_mempool() == [userop] 31 | assert pm_opsSeen(pm1.address) == 1 32 | bump_gas_fees(userop) 33 | 34 | # replace with unmodified userop. 35 | assert_ok(userop.send()) 36 | assert dump_mempool() == [userop] 37 | assert pm_opsSeen(pm1.address) == 1, "replace with unmodified userop" 38 | 39 | # replace paymaster: should "move" opsSeen to new paymaster 40 | pm2 = deploy_and_deposit(w3, entrypoint_contract, "TestRulesPaymaster", False) 41 | userop.paymaster = pm2.address 42 | 43 | bump_gas_fees(userop) 44 | assert_ok(userop.send()) 45 | assert dump_mempool() == [userop] 46 | assert pm_opsSeen(pm2.address) == 1, "replaced paymaster" 47 | assert pm_opsSeen(pm1.address) == 0, "old paymaster should have no opsSeen" 48 | 49 | # remove paymaster completely: 50 | userop.paymaster = None 51 | userop.paymasterVerificationGasLimit = None 52 | userop.paymasterPostOpGasLimit = None 53 | userop.paymasterData = None 54 | bump_gas_fees(userop) 55 | 56 | assert_ok(userop.send()) 57 | assert dump_mempool() == [userop] 58 | assert pm_opsSeen(pm2.address) == 0, "removed paymaster" 59 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/RIP7560Deployer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "../Create2.sol"; 5 | import "../ValidationRules.sol"; 6 | 7 | import "@rip7560/contracts/interfaces/IRip7560Transaction.sol"; 8 | import {TestAccount} from "./TestAccount.sol"; 9 | import "../Stakable.sol"; 10 | 11 | contract RIP7560Deployer is ValidationRulesStorage, Stakable { 12 | using ValidationRules for string; 13 | TestCoin immutable public coin = new TestCoin(); 14 | TestRulesTarget private immutable target = new TestRulesTarget(); 15 | 16 | event DeployerEvent(string name, uint256 counter, address deployed); 17 | event Uint(uint); 18 | 19 | constructor () payable {} 20 | 21 | function createAccount(address owner, uint256 salt, string memory rule) external returns (address ret) { 22 | address create2address = getCreate2Address(owner, salt, rule); 23 | if (rule.eq("EXTCODEx_CALLx_undeployed_sender")) { 24 | // CALL 25 | create2address.call(""); 26 | // CALLCODE 27 | assembly { 28 | let res := callcode(5000, create2address, 0, 0, 0, 0, 0) 29 | } 30 | // DELEGATECALL 31 | create2address.delegatecall(""); 32 | // STATICCALL 33 | create2address.staticcall(""); 34 | // EXTCODESIZE 35 | emit Uint(uint256(keccak256(create2address.code))); 36 | if (create2address == address(0)) revert(); 37 | // EXTCODEHASH 38 | emit Uint(uint256(create2address.codehash)); 39 | // EXTCODECOPY 40 | assembly { 41 | extcodecopy(create2address, 0, 0, 2) 42 | } 43 | } 44 | if (ValidationRules.eq(rule, "skip-deploy-msg")){ 45 | return address(0); 46 | } 47 | ret = address(new TestAccount{salt : bytes32(salt)}()); 48 | if (!rule.eq("EXTCODEx_CALLx_undeployed_sender")) { 49 | ValidationRules.runRule(rule, ITestAccount(ret), address(0), address(this), coin, this, target); 50 | } 51 | } 52 | 53 | function getCreate2Address(address owner, uint256 salt, string memory rule) public view returns (address) { 54 | return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked( 55 | type(TestAccount).creationCode 56 | ))); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/aabundler-launcher: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # launch bundler: also start geth, and deploy entrypoint. 3 | cd `dirname $0`/../../bundler 4 | 5 | GETH=geth 6 | GETHPORT=8545 7 | BUNDLERPORT=3000 8 | PIDLIST=/tmp/aabundler.pidlist 9 | VERSION="aabundler-js-0.1" 10 | 11 | BUNDLERLOG=/tmp/aabundler.log 12 | 13 | BUNDLERURL=http://localhost:$BUNDLERPORT/rpc 14 | NODEURL=http://localhost:$GETHPORT 15 | 16 | function fatal { 17 | echo "$@" 1>&2 18 | exit 1 19 | } 20 | 21 | function isPortFree { 22 | port=$1 23 | curl http://localhost:$port 2>&1 | grep -q Connection.refused 24 | } 25 | 26 | 27 | function waitForPort { 28 | port=$1 29 | while isPortFree $port; do true; done 30 | } 31 | 32 | function startGeth { 33 | isPortFree $GETHPORT || fatal port $GETHPORT not free 34 | echo == starting geth 1>&2 35 | $GETH version | grep ^Version: 1>&2 36 | 37 | $GETH --miner.gaslimit 12000000 \ 38 | --http \ 39 | --http.api personal,eth,net,web3,debug \ 40 | --allow-insecure-unlock \ 41 | --rpc.allow-unprotected-txs \ 42 | --http.vhosts '*,localhost,host.docker.internal' \ 43 | --http.corsdomain '*' \ 44 | --http.addr "0.0.0.0" \ 45 | --dev \ 46 | --nodiscover --maxpeers 0 --mine \ 47 | --miner.threads 1 \ 48 | --verbosity 1 \ 49 | --ignore-legacy-receipts & 50 | 51 | waitForPort $GETHPORT 52 | } 53 | 54 | function deployEntryPoint { 55 | cd packages/bundler 56 | echo == Deploying entrypoint 1>&2 57 | export TS_NODE_TRANSPILE_ONLY=1 58 | npx hardhat deploy --network localhost 59 | } 60 | 61 | function startBundler { 62 | isPortFree $BUNDLERPORT || fatal port $BUNDLERPORT not free 63 | echo == Starting bundler 1>&2 64 | yarn ts-node -T ./src/exec.ts --config ./localconfig/bundler.config.json --port $BUNDLERPORT --network http://localhost:$GETHPORT & 65 | waitForPort $BUNDLERPORT 66 | echo Listening on $BUNDLERURL 67 | } 68 | 69 | function start { 70 | startGeth 71 | deployEntryPoint 72 | startBundler > $BUNDLERLOG 73 | pstree -p $$ | grep -oE [0-9]* > $PIDLIST 74 | echo == Bundler, Geth started. log to $BUNDLERLOG 75 | } 76 | 77 | function stop { 78 | echo == stopping bundler 79 | test -r $PIDLIST && kill -9 `cat $PIDLIST` 2>/dev/null 80 | rm $PIDLIST 81 | echo == bundler, geth stopped 82 | } 83 | 84 | case $1 in 85 | 86 | start) 87 | start 88 | ;; 89 | stop) 90 | stop 91 | ;; 92 | 93 | restart) 94 | echo == restarting bundler 95 | stop 96 | start 97 | ;; 98 | 99 | *) echo "usage: $0 {start|stop|restart}" 100 | exit 1 ;; 101 | 102 | 103 | esac 104 | -------------------------------------------------------------------------------- /tests/types.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from dataclasses import dataclass, field, asdict 3 | from enum import IntEnum 4 | from typing import ClassVar 5 | 6 | import json 7 | import jsonrpcclient 8 | import requests 9 | 10 | 11 | @dataclass() 12 | class CommandLineArgs: 13 | url: ClassVar[str] 14 | entrypoint: ClassVar[str] 15 | nonce_manager: ClassVar[str] 16 | stake_manager: ClassVar[str] 17 | ethereum_node: ClassVar[str] 18 | launcher_script: ClassVar[str] 19 | log_rpc: ClassVar[bool] 20 | 21 | @classmethod 22 | # pylint: disable=too-many-arguments 23 | def configure( 24 | cls, 25 | url, 26 | entrypoint, 27 | query_ep, 28 | nonce_manager, 29 | stake_manager, 30 | ethereum_node, 31 | launcher_script, 32 | log_rpc, 33 | ): 34 | cls.url = url 35 | cls.entrypoint = entrypoint 36 | cls.nonce_manager = nonce_manager 37 | cls.stake_manager = stake_manager 38 | cls.ethereum_node = ethereum_node 39 | cls.launcher_script = launcher_script 40 | cls.log_rpc = log_rpc 41 | if query_ep: 42 | cls.entrypoint = ( 43 | RPCRequest(method="eth_supportedEntryPoints").send(cls.url).result[0] 44 | ) 45 | 46 | 47 | @dataclass 48 | class RPCRequest: 49 | method: str 50 | id: int = field(default_factory=itertools.count().__next__) 51 | params: list = field(default_factory=list, compare=False) 52 | jsonrpc: str = "2.0" 53 | 54 | def send(self, url=None) -> jsonrpcclient.responses.Response: 55 | if url is None: 56 | url = CommandLineArgs.url 57 | # return requests.post(url, json=asdict(self)).json() 58 | if CommandLineArgs.log_rpc: 59 | print(">>", url, json.dumps(asdict(self))) 60 | postres = requests.post(url, json=asdict(self), timeout=10).json() 61 | if CommandLineArgs.log_rpc: 62 | # https://github.com/pylint-dev/pylint/issues/7891 63 | # pylint: disable=no-member 64 | print("<<", postres) 65 | res = jsonrpcclient.responses.to_response(postres) 66 | return res 67 | 68 | 69 | class RPCErrorCode(IntEnum): 70 | INVALID_INPUT = -32000 71 | REJECTED_BY_EP_OR_ACCOUNT = -32500 72 | REJECTED_BY_PAYMASTER = -32501 73 | BANNED_OPCODE = -32502 74 | SHORT_DEADLINE = -32503 75 | BANNED_OR_THROTTLED_PAYMASTER = -32504 76 | INAVLID_PAYMASTER_STAKE = -32505 77 | INVALID_AGGREGATOR = -32506 78 | INVALID_SIGNATURE = -32507 79 | PAYMASTER_DEPOSIT_TOO_LOW = -32508 80 | 81 | EXECUTION_REVERTED = -32521 82 | INVALID_FIELDS = -32602 83 | 84 | 85 | def remove_nulls(obj): 86 | return {k: v for k, v in obj.items() if v is not None} 87 | -------------------------------------------------------------------------------- /tests/user_operation_erc4337.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict, dataclass 2 | 3 | from eth_typing import HexStr 4 | from eth_utils import to_checksum_address 5 | 6 | from tests.transaction_eip_7702 import TupleEIP7702 7 | from tests.types import RPCRequest, CommandLineArgs, remove_nulls 8 | 9 | 10 | @dataclass 11 | class UserOperation: 12 | # pylint: disable=too-many-instance-attributes, invalid-name 13 | sender: HexStr 14 | nonce: HexStr = hex(0) 15 | factory: HexStr = None 16 | factoryData: HexStr = None 17 | callData: HexStr = "0x" 18 | callGasLimit: HexStr = hex(3 * 10**5) 19 | verificationGasLimit: HexStr = hex(10**6) 20 | preVerificationGas: HexStr = hex(4 * 10**5) 21 | maxFeePerGas: HexStr = hex(4 * 10**9) 22 | maxPriorityFeePerGas: HexStr = hex(3 * 10**9) 23 | signature: HexStr = "0x" 24 | paymaster: HexStr = None 25 | paymasterData: HexStr = None 26 | paymasterVerificationGasLimit: HexStr = None 27 | paymasterPostOpGasLimit: HexStr = None 28 | eip7702Auth: TupleEIP7702 = None 29 | 30 | def __post_init__(self): 31 | self.sender = to_checksum_address(self.sender) 32 | self.callData = self.callData.lower() 33 | self.signature = self.signature.lower() 34 | if self.paymaster is not None: 35 | self.paymaster = to_checksum_address(self.paymaster) 36 | if self.paymasterVerificationGasLimit is None: 37 | self.paymasterVerificationGasLimit = hex(10**5) 38 | if self.paymasterPostOpGasLimit is None: 39 | self.paymasterPostOpGasLimit = hex(10**5) 40 | if self.paymasterData is None: 41 | self.paymasterData = "0x" 42 | else: 43 | self.paymasterData = self.paymasterData.lower() 44 | if self.factory is not None: 45 | self.factory = to_checksum_address(self.factory) 46 | if self.factoryData is None: 47 | self.factoryData = "0x" 48 | else: 49 | self.factoryData = self.factoryData.lower() 50 | 51 | def send(self, entrypoint=None, url=None): 52 | if entrypoint is None: 53 | entrypoint = CommandLineArgs.entrypoint 54 | return RPCRequest( 55 | method="eth_sendUserOperation", 56 | params=[remove_nulls(asdict(self)), entrypoint], 57 | ).send(url) 58 | 59 | # send into the mempool without applying tracing/validations 60 | def debug_send(self, entrypoint=None, url=None): 61 | if entrypoint is None: 62 | entrypoint = CommandLineArgs.entrypoint 63 | return RPCRequest( 64 | method="debug_bundler_sendUserOperationSkipValidation", 65 | params=[asdict(self), entrypoint], 66 | ).send(url) 67 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "bundler-spec-tests" 3 | version = "0.4.0" 4 | description = "" 5 | authors = ["shahafn "] 6 | readme = "README.md" 7 | packages = [{include = "bundler_spec_tests"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.8" 11 | 12 | [tool.black] 13 | line-length = 88 14 | 15 | [tool.pylint] 16 | disable = ["C0114", "C0115", "C0116", "W0621", "R0914", "R1735"] # missing-module-docstring, redefined-outer-name as it conflicts with pytest fixtures, too-many-locals, use-dict-literal 17 | ignored-classes = ["Error"] 18 | good-names = ["w3", "id", "i", "s"] 19 | good-names-rgxs = ["test_.*"] 20 | max-line-length=120 21 | max-branches=15 22 | 23 | [tool.pdm] 24 | [tool.pdm.build] 25 | includes = ["bundler_spec_tests"] 26 | [build-system] 27 | requires = ["pdm-pep517>=1.0.0"] 28 | build-backend = "pdm.pep517.api" 29 | 30 | [tool.pdm.scripts] 31 | submodule-update = {shell = "git submodule update --init --recursive"} 32 | spec-build = {shell = "cd spec && yarn && yarn build"} 33 | rip7560-build = {shell = "cd @rip7560 && yarn && yarn compile-hardhat" } 34 | dep-build = {composite = ["spec-build", "rip7560-build"]} 35 | update-deps = {composite = ["submodule-update", "dep-build"]} 36 | update-deps-remote = {shell = "git submodule update --init --recursive --remote && cd @account-abstraction && yarn && yarn compile && cd ../spec && yarn && yarn build && cd ../@rip7560 && yarn && yarn compile-hardhat"} 37 | test = "pytest --tb=short -rA -W ignore::DeprecationWarning --url http://localhost:3000/rpc --entry-point 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 --ethereum-node http://127.0.0.1:8545/ tests/single" 38 | test-rip7560 = "pytest --tb=short -rA -W ignore::DeprecationWarning --url http://localhost:3000/rpc --nonce-manager 0x59c405Dc6D032d9Ff675350FefC66F3b6c1bEbaB --stake-manager 0xc142Db6f76A8B4Edb7D3F24638e4d6f8BC6199FE --ethereum-node http://127.0.0.1:8545/ tests/rip7560" 39 | p2ptest = "pytest --tb=short -rA -W ignore::DeprecationWarning --url http://localhost:3000/rpc --entry-point 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 --ethereum-node http://127.0.0.1:8545/ tests/p2p" 40 | lint = "pylint tests" 41 | format = "black tests" 42 | 43 | [project] 44 | name = "bundler-spec-tests" 45 | version = "0.1.0" 46 | description = "" 47 | authors = [ 48 | {name = "shahafn", email = "shahaflol@gmail.com"}, 49 | ] 50 | dependencies = [ 51 | "pytest>=7.2.0", 52 | "requests>=2.28.1", 53 | "setuptools>=65.6.0", 54 | "eth-utils>=1.2.0", 55 | "py-solc-x>=2.0.3", 56 | "eth-tester>=0.6.0b6", 57 | "web3>=5.31.1", 58 | "jsonrpcclient>=4.0.2", 59 | "jsonschema>=4.21.1", 60 | "black>=22.12.0", 61 | "pylint>=2.15.8", 62 | ] 63 | requires-python = ">=3.10,<3.11" 64 | license = {text = "MIT"} 65 | readme = "README.md" 66 | 67 | -------------------------------------------------------------------------------- /tests/transaction_eip_7702.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, asdict 2 | from typing import Optional 3 | 4 | import rlp 5 | from eth_keys import keys 6 | from eth_typing import HexStr 7 | from eth_utils import to_bytes 8 | from web3 import Web3 9 | 10 | from tests.rip7560.types import remove_nulls 11 | from tests.types import RPCRequest 12 | 13 | 14 | @dataclass 15 | class TupleEIP7702: 16 | # pylint: disable=invalid-name 17 | chainId: HexStr 18 | address: HexStr 19 | nonce: HexStr 20 | # pylint: disable=invalid-name 21 | yParity: Optional[HexStr] = None 22 | r: Optional[HexStr] = None 23 | s: Optional[HexStr] = None 24 | signer_private_key: Optional[HexStr] = None 25 | 26 | def __post_init__(self): 27 | if self.signer_private_key: 28 | self._sign(self.signer_private_key) 29 | self.signer_private_key = None 30 | 31 | def _sign(self, private_key: str): 32 | pk = keys.PrivateKey(bytes.fromhex(private_key)) 33 | nonce = self.nonce 34 | if nonce == "0x0": 35 | nonce = "0x" 36 | 37 | chain_id = self.chainId 38 | if chain_id == "0x0": 39 | chain_id = "0x" 40 | 41 | rlp_encode = bytearray( 42 | rlp.encode( 43 | [ 44 | to_bytes(hexstr=chain_id), 45 | to_bytes(hexstr=self.address), 46 | to_bytes(hexstr=nonce), 47 | ] 48 | ) 49 | ) 50 | rlp_encode.insert(0, 5) 51 | rlp_encode_hash = Web3.keccak(hexstr=rlp_encode.hex()) 52 | signature = pk.sign_msg_hash(rlp_encode_hash) 53 | self.yParity = hex(signature.v) 54 | self.r = hex(signature.r) 55 | self.s = hex(signature.s) 56 | 57 | 58 | # pylint: disable=fixme 59 | # TODO: Will we have any tests sending EIP-7702 transactions directly? 60 | # If not, this class can be removed. 61 | @dataclass 62 | class TransactionEIP7702: 63 | # pylint: disable=too-many-instance-attributes, invalid-name 64 | to: HexStr = "0x0000000000000000000000000000000000000000" 65 | data: HexStr = "0x00" 66 | nonce: HexStr = hex(0) 67 | gasLimit: HexStr = hex(1_000_000) # alias for callGasLimit 68 | maxFeePerGas: HexStr = hex(4 * 10**9) 69 | maxPriorityFeePerGas: HexStr = hex(3 * 10**9) 70 | chainId: HexStr = hex(1337) 71 | value: HexStr = hex(0) 72 | # pylint: disable=fixme 73 | accessList: list[HexStr] = () # todo: type is not correct, must always be empty! 74 | authorizationList: list[TupleEIP7702] = () 75 | 76 | # pylint: disable=fixme 77 | # todo: implement 78 | def cleanup(self): 79 | return self 80 | 81 | def send(self, url=None): 82 | return RPCRequest( 83 | method="eth_sendTransaction", params=[remove_nulls(asdict(self.cleanup()))] 84 | ).send(url) 85 | -------------------------------------------------------------------------------- /tests/contracts/TestRulesFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "./Create2.sol"; 5 | import "./SimpleWallet.sol"; 6 | import "./Stakable.sol"; 7 | import "./ValidationRules.sol"; 8 | import "@account-abstraction/contracts/interfaces/IAccount.sol"; 9 | 10 | contract TestRulesFactory is Stakable, ValidationRulesStorage { 11 | 12 | using ValidationRules for string; 13 | 14 | TestCoin private immutable coin = new TestCoin(); 15 | TestRulesTarget private immutable target = new TestRulesTarget(); 16 | 17 | constructor(address _entryPoint) { 18 | entryPoint = IEntryPoint(_entryPoint); 19 | } 20 | 21 | receive() external payable {} 22 | 23 | event Address(address); 24 | event Uint(uint); 25 | 26 | function create(uint nonce, string memory rule, address _entryPoint) public returns (SimpleWallet account) { 27 | // note that the 'EXT*'/'CALL*' opcodes are allowed on the zero code address if it is later deployed 28 | address create2address = getAddress(nonce, _entryPoint); 29 | if (rule.eq("EXTCODEx_CALLx_undeployed_sender")) { 30 | ValidationRules.runFactorySpecificRule(nonce, rule, _entryPoint, create2address); 31 | } 32 | else if (rule.eq("DELEGATECALL:>EXTCODEx_CALLx_undeployed_sender")) { 33 | string memory innerRule = rule.slice(14, bytes(rule).length - 14); 34 | bytes memory callData = abi.encodeCall(target.runFactorySpecificRule, (nonce, innerRule, _entryPoint, create2address)); 35 | (bool success, bytes memory ret) = address(target).delegatecall(callData); 36 | require(success, string(abi.encodePacked("DELEGATECALL rule reverted", ret))); 37 | } 38 | else if (rule.eq("CALL:>EXTCODEx_CALLx_undeployed_sender")) { 39 | string memory innerRule = rule.slice(6, bytes(rule).length - 6); 40 | target.runFactorySpecificRule(nonce, innerRule, _entryPoint, create2address); 41 | } 42 | 43 | account = new SimpleWallet{salt: bytes32(nonce)}(_entryPoint); 44 | require(address(account) != address(0), "create failed"); 45 | // do not revert on rules checked before account creation 46 | if (rule.includes("EXTCODEx_CALLx_undeployed_sender")) {} 47 | else { 48 | ValidationRules.runRule(rule, account, address(0), address(this), coin, this, target); 49 | } 50 | return account; 51 | } 52 | 53 | /** 54 | * calculate the counterfactual address of this account as it would be returned by createAccount() 55 | */ 56 | function getAddress(uint256 salt, address _entryPoint) public view returns (address) { 57 | return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked( 58 | type(SimpleWallet).creationCode, 59 | abi.encode( 60 | address(_entryPoint) 61 | ) 62 | ))); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/rip7560/test_gas_usage.py: -------------------------------------------------------------------------------- 1 | from tests.rip7560.types import TransactionRIP7560 2 | from tests.utils import assert_ok, deploy_contract, send_bundle_now 3 | 4 | AA_ENTRYPOINT = "0x0000000000000000000000000000000000007560" 5 | AA_SENDER_CREATOR = "0x00000000000000000000000000000000ffff7560" 6 | 7 | 8 | def test_gas_usage_no_paymaster(w3, tx_7560): 9 | send_and_check_payment(w3, tx_7560) 10 | 11 | 12 | def test_gas_usage_with_paymaster(w3, wallet_contract): 13 | paymaster = deploy_contract(w3, "rip7560/TestPaymaster", value=1 * 10**18) 14 | tx = TransactionRIP7560( 15 | sender=wallet_contract.address, 16 | nonceKey=hex(0), 17 | nonce=hex(1), 18 | paymaster=paymaster.address, 19 | maxFeePerGas=hex(100000000000), 20 | maxPriorityFeePerGas=hex(12345), 21 | authorizationData="0xface", 22 | executionData=wallet_contract.encode_abi( 23 | abi_element_identifier="anyExecutionFunction" 24 | ), 25 | # nonce = "0x1234" 26 | ) 27 | send_and_check_payment(w3, tx) 28 | 29 | 30 | # check transaction payments: 31 | # - paymaster (or account) pays the gas 32 | # - coinbase gets the tip 33 | def send_and_check_payment(w3, tx: TransactionRIP7560): 34 | 35 | if tx.paymaster is not None: 36 | paymaster_pre_balance = w3.eth.get_balance(tx.paymaster) 37 | 38 | wallet_pre_balance = w3.eth.get_balance(tx.sender) 39 | coinbase_pre_balance = w3.eth.get_balance(w3.eth.get_block("latest").miner) 40 | ret = tx.send() 41 | assert_ok(ret) 42 | send_bundle_now() 43 | 44 | if tx.paymaster is not None: 45 | assert ( 46 | w3.eth.get_balance(tx.sender) == wallet_pre_balance 47 | ), "wallet balance should not change" 48 | block = w3.eth.get_block("latest") 49 | 50 | coinbase_post_balance = w3.eth.get_balance(block.miner) 51 | coinbase_diff = coinbase_post_balance - coinbase_pre_balance 52 | # coinbase is paid for every tx in the block, and for each just the tip, not the full gas 53 | total_tx_tips = 0 54 | total_tx_cost = 0 55 | for txhash in block.transactions: 56 | rcpt = w3.eth.get_transaction_receipt(txhash) 57 | actual_priority = rcpt.effectiveGasPrice - block.baseFeePerGas 58 | used = rcpt.gasUsed 59 | 60 | if used > rcpt.cumulativeGasUsed: 61 | print( 62 | f"::warning {__file__} : gas_used field is BROKEN for 2nd TX. Workaround until AA-438 is fixed" 63 | ) 64 | used = 21000 65 | 66 | if rcpt.type == TransactionRIP7560.Type: 67 | tx_cost = used * rcpt.effectiveGasPrice 68 | total_tx_cost = total_tx_cost + tx_cost 69 | 70 | print(f"{txhash.hex()} used {used} cumulative {rcpt.cumulativeGasUsed}") 71 | tip = actual_priority * used 72 | total_tx_tips += tip 73 | 74 | assert coinbase_diff == total_tx_tips 75 | 76 | if tx.paymaster is not None: 77 | paymaster_paid = paymaster_pre_balance - w3.eth.get_balance(tx.paymaster) 78 | assert paymaster_paid == total_tx_cost 79 | else: 80 | wallet_paid = wallet_pre_balance - w3.eth.get_balance(tx.sender) 81 | assert wallet_paid == total_tx_cost 82 | -------------------------------------------------------------------------------- /tests/rip7560/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, asdict 2 | 3 | from web3.constants import ADDRESS_ZERO 4 | from eth_typing import HexStr 5 | from eth_utils import to_checksum_address 6 | from tests.types import RPCRequest 7 | from tests.types import remove_nulls 8 | 9 | 10 | @dataclass 11 | class TransactionRIP7560: 12 | # pylint: disable=too-many-instance-attributes, invalid-name 13 | Type = 5 14 | sender: HexStr 15 | nonceKey: HexStr = hex(0) 16 | nonce: HexStr = hex(0) 17 | factory: HexStr = "0x0000000000000000000000000000000000000000" 18 | deployer: HexStr = None # alias for factory 19 | factoryData: HexStr = "0x" 20 | deployerData: HexStr = None # alias for factoryData 21 | executionData: HexStr = "0x" 22 | callGasLimit: HexStr = hex(3 * 10**5) 23 | gas: HexStr = None # alias for callGasLimit 24 | verificationGasLimit: HexStr = hex(10**6) 25 | maxFeePerGas: HexStr = hex(4 * 10**9) 26 | maxPriorityFeePerGas: HexStr = hex(3 * 10**9) 27 | authorizationData: HexStr = "0x" 28 | paymaster: HexStr = "0x0000000000000000000000000000000000000000" 29 | paymasterData: HexStr = "0x" 30 | paymasterVerificationGasLimit: HexStr = hex(0) 31 | paymasterPostOpGasLimit: HexStr = hex(0) 32 | chainId: HexStr = hex(1337) 33 | value: HexStr = hex(0) 34 | accessList = "0x" 35 | builderFee: HexStr = hex(0) 36 | 37 | def __post_init__(self): 38 | # pylint: disable=duplicate-code 39 | self.sender = to_checksum_address(self.sender) 40 | if self.deployer is not None: 41 | self.factory = self.deployer 42 | self.deployer = None 43 | if self.deployerData is not None: 44 | self.factoryData = self.deployerData 45 | self.deployerData = None 46 | if self.gas is not None: 47 | self.callGasLimit = self.gas 48 | self.gas = None 49 | if self.paymaster is not None: 50 | if ( 51 | self.paymasterVerificationGasLimit is None 52 | or self.paymasterVerificationGasLimit == "0x0" 53 | ): 54 | self.paymasterVerificationGasLimit = hex(10**6) 55 | if ( 56 | self.paymasterPostOpGasLimit is None 57 | or self.paymasterPostOpGasLimit == "0x0" 58 | ): 59 | self.paymasterPostOpGasLimit = hex(10**6) 60 | if self.paymasterData is None: 61 | self.paymasterData = "0x" 62 | 63 | # clean paymaster and factory fields if they are None 64 | def cleanup(self): 65 | if self.paymaster is None or self.paymaster == ADDRESS_ZERO: 66 | self.paymaster = None 67 | self.paymasterPostOpGasLimit = None 68 | self.paymasterVerificationGasLimit = None 69 | self.paymasterData = None 70 | if self.factory is None or self.factory == ADDRESS_ZERO: 71 | self.factory = None 72 | self.factoryData = None 73 | return self 74 | 75 | def send(self, url=None): 76 | return RPCRequest( 77 | method="eth_sendTransaction", params=[remove_nulls(asdict(self.cleanup()))] 78 | ).send(url) 79 | 80 | def send_skip_validation(self, url=None): 81 | return RPCRequest( 82 | method="debug_bundler_sendTransactionSkipValidation", 83 | params=[remove_nulls(asdict(self.cleanup()))], 84 | ).send(url) 85 | -------------------------------------------------------------------------------- /tests/rip7560/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | 5 | import pytest 6 | 7 | from tests.rip7560.types import TransactionRIP7560 8 | from tests.types import CommandLineArgs 9 | from tests.utils import ( 10 | deploy_contract, 11 | compile_contract, 12 | ) 13 | 14 | 15 | @pytest.fixture 16 | def wallet_contract(w3): 17 | contract = deploy_contract( 18 | w3, 19 | "rip7560/TestAccount", 20 | value=10**18, 21 | ) 22 | return contract 23 | 24 | 25 | @pytest.fixture 26 | def paymaster_contract_7560(w3): 27 | contract = deploy_contract( 28 | w3, 29 | "rip7560/RIP7560Paymaster", 30 | value=10**18, 31 | ) 32 | return contract 33 | 34 | 35 | @pytest.fixture 36 | def factory_contract_7560(w3): 37 | contract = deploy_contract( 38 | w3, 39 | "rip7560/RIP7560Deployer", 40 | value=0 * 10**18, 41 | ) 42 | time.sleep(0.1) 43 | return contract 44 | 45 | 46 | # pylint: disable=fixme 47 | # TODO: deduplicate 48 | @pytest.fixture 49 | def wallet_contract_rules(w3): 50 | contract = deploy_contract( 51 | w3, 52 | "rip7560/RIP7560TestRulesAccount", 53 | value=0 * 10**18, 54 | ) 55 | time.sleep(0.1) 56 | w3.eth.send_transaction( 57 | {"from": w3.eth.default_account, "to": contract.address, "value": 10**18} 58 | ) 59 | return contract 60 | 61 | 62 | @pytest.fixture 63 | def tx_7560(wallet_contract): 64 | return TransactionRIP7560( 65 | sender=wallet_contract.address, 66 | nonceKey=hex(0), 67 | nonce=hex(1), 68 | maxFeePerGas=hex(100000000000), 69 | maxPriorityFeePerGas=hex(100000000000), 70 | verificationGasLimit=hex(2000000), 71 | executionData=wallet_contract.encode_abi( 72 | abi_element_identifier="anyExecutionFunction" 73 | ), 74 | authorizationData="0xface", 75 | ) 76 | 77 | 78 | @pytest.fixture(scope="session") 79 | def entry_point_rip7560(w3): 80 | entry_point_interface = compile_contract( 81 | "../../@rip7560/contracts/interfaces/IRip7560EntryPoint" 82 | ) 83 | entry_point = w3.eth.contract( 84 | abi=entry_point_interface["abi"], 85 | address="0x0000000000000000000000000000000000007560", 86 | ) 87 | return entry_point 88 | 89 | 90 | @pytest.fixture(scope="session") 91 | def nonce_manager(w3): 92 | artifact_path = "/../../@rip7560/artifacts/contracts/predeploys/NonceManager.sol/NonceManager.json" 93 | return contract_from_artifact(w3, artifact_path, CommandLineArgs.nonce_manager) 94 | 95 | 96 | @pytest.fixture(scope="session") 97 | def stake_manager(w3): 98 | artifact_path = "/../../@rip7560/artifacts/contracts/predeploys/Rip7560StakeManager.sol/Rip7560StakeManager.json" 99 | return contract_from_artifact(w3, artifact_path, CommandLineArgs.stake_manager) 100 | 101 | 102 | def contract_from_artifact(w3, artifact_path, contract_address): 103 | current_dirname = os.path.dirname(__file__) 104 | artifact_realpath = os.path.realpath(current_dirname + artifact_path) 105 | code = w3.eth.get_code(contract_address) 106 | assert len(code) > 2, "contract not deployed: " + contract_address + artifact_path 107 | with open(artifact_realpath, encoding="utf-8") as file: 108 | json_file = json.load(file) 109 | return w3.eth.contract(abi=json_file["abi"], address=contract_address) 110 | -------------------------------------------------------------------------------- /tests/single/gas/test_eip_7623.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | 3 | from tests.find_min import find_min_userop_with_field 4 | from tests.conftest import assert_ok 5 | from tests.types import RPCErrorCode, RPCRequest, CommandLineArgs 6 | from tests.user_operation_erc4337 import UserOperation 7 | from tests.utils import assert_rpc_error, to_hex 8 | 9 | 10 | def with_min_validation_gas(op): 11 | min = find_min_userop_with_field( 12 | op, 13 | "verificationGasLimit", 14 | 1, 15 | 200000, 16 | ) 17 | op.verificationGasLimit = to_hex(min) 18 | return op 19 | 20 | 21 | def test_normal_calldata(w3, wallet_contract, entrypoint_contract): 22 | call_wasteGas = wallet_contract.encode_abi( 23 | abi_element_identifier="wasteGas", args=[] 24 | ) 25 | op = with_min_validation_gas( 26 | UserOperation( 27 | sender=wallet_contract.address, 28 | callData=call_wasteGas, 29 | callGasLimit=to_hex(10_000), 30 | preVerificationGas=to_hex(42000), 31 | signature="0x", 32 | ) 33 | ) 34 | 35 | assert_ok(op.send()) 36 | 37 | 38 | def test_huge_calldata_failed(w3, wallet_contract, entrypoint_contract): 39 | # userop has large calldata, and requires more gas as per EIP-7623 40 | # note that actual tx will take enough gas, but we can only count validation+10% of callGasLimit 41 | 42 | call_wasteGas = wallet_contract.encode_abi( 43 | abi_element_identifier="wasteGas", args=[] 44 | ) 45 | op1 = with_min_validation_gas( 46 | UserOperation( 47 | sender=wallet_contract.address, 48 | callData=call_wasteGas + "ff" * 1000, 49 | callGasLimit=to_hex(10_000), 50 | preVerificationGas=to_hex(42000), 51 | signature="0x", 52 | ) 53 | ) 54 | ret = op1.send() 55 | assert_rpc_error(ret, "preVerificationGas", RPCErrorCode.INVALID_FIELDS) 56 | 57 | 58 | def test_huge_calldata_with_callgas(w3, wallet_contract, entrypoint_contract): 59 | # large userop, but with enough callGasLimit, so that 10% is enough to pay. 60 | 61 | call_wasteGas = wallet_contract.encode_abi( 62 | abi_element_identifier="wasteGas", args=[] 63 | ) 64 | op1 = with_min_validation_gas( 65 | UserOperation( 66 | sender=wallet_contract.address, 67 | callData=call_wasteGas + "ff" * 1000, 68 | callGasLimit=to_hex(1_000_000), 69 | preVerificationGas=to_hex(42000), 70 | signature="0x", 71 | ) 72 | ) 73 | ret = op1.send() 74 | assert_rpc_error(ret, "preVerificationGas", RPCErrorCode.INVALID_FIELDS) 75 | 76 | 77 | def test_estimate_huge_calldata(w3, wallet_contract, entrypoint_contract): 78 | # large userop, but with enough callGasLimit, so that 10% is enough to pay. 79 | 80 | call_wasteGas = wallet_contract.encode_abi( 81 | abi_element_identifier="wasteGas", args=[] 82 | ) 83 | op = UserOperation( 84 | sender=wallet_contract.address, 85 | callData=call_wasteGas + "ff" * 1000, 86 | callGasLimit=to_hex(1_000_000), 87 | preVerificationGas=to_hex(42000), 88 | signature="0x", 89 | ) 90 | 91 | response = RPCRequest( 92 | method="eth_estimateUserOperationGas", 93 | params=[asdict(op), CommandLineArgs.entrypoint], 94 | ).send() 95 | print(response) 96 | 97 | op.verificationGasLimit = response.result["verificationGasLimit"] 98 | op.preVerificationGas = response.result["preVerificationGas"] 99 | 100 | assert_ok(op.send()) 101 | -------------------------------------------------------------------------------- /tests/single/bundle/test_codehash.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.user_operation_erc4337 import UserOperation 4 | from tests.utils import ( 5 | dump_mempool, 6 | send_bundle_now, 7 | deploy_contract, 8 | ) 9 | 10 | 11 | def assert_useroperation_event(entrypoint_contract, userop, from_block): 12 | logs = entrypoint_contract.events.UserOperationEvent.getLogs(fromBlock=from_block) 13 | assert len(logs) == 1 14 | assert logs[0].args.sender == userop.sender 15 | 16 | 17 | def assert_no_useroperation_event(entrypoint_contract, from_block): 18 | logs = entrypoint_contract.events.UserOperationEvent.getLogs(fromBlock=from_block) 19 | assert len(logs) == 0 20 | 21 | 22 | def create_account(w3, codehash_factory_contract, entrypoint_contract, num): 23 | nonce = 123 24 | tx_hash = codehash_factory_contract.functions.create( 25 | nonce, num, entrypoint_contract.address 26 | ).transact({"from": w3.eth.default_account, "value": 10**18}) 27 | receipt = w3.eth.wait_for_transaction_receipt(tx_hash) 28 | logs = codehash_factory_contract.events.ContractCreated().process_receipt(receipt) 29 | account = logs[0].args.account 30 | codehash = w3.eth.get_proof(account, [])["codeHash"].hex() 31 | return account, codehash 32 | 33 | 34 | @pytest.mark.usefixtures("manual_bundling_mode") 35 | def test_codehash_changed(w3, entrypoint_contract): 36 | codehash_factory_contract = deploy_contract(w3, "TestCodeHashFactory") 37 | # Creating account with num == 0 38 | account0, codehash0 = create_account( 39 | w3, codehash_factory_contract, entrypoint_contract, 0 40 | ) 41 | block_number = w3.eth.get_block_number() 42 | nonce = entrypoint_contract.functions.getNonce(account0, 123).call() 43 | userop = UserOperation(sender=account0, nonce=hex(nonce)) 44 | response = userop.send() 45 | assert response.result, "userop dropped by bundler" 46 | assert dump_mempool() == [userop] 47 | # Calling SELFDESTRUCT before constructing the account on the same address with different code hash 48 | 49 | tx_hash = codehash_factory_contract.functions.destroy(account0).transact( 50 | {"from": w3.eth.default_account} 51 | ) 52 | w3.eth.wait_for_transaction_receipt(tx_hash) 53 | if len(w3.eth.get_code(account0)) != 0: 54 | pytest.skip("no self destruct. can't check code change..") 55 | # Creating account with num == 1 56 | account1, codehash1 = create_account( 57 | w3, codehash_factory_contract, entrypoint_contract, 1 58 | ) 59 | assert account0 == account1, "could not create account on the same address" 60 | # assert codehash0 != codehash1, "could not create account with a different codehash" 61 | if codehash0 == codehash1: 62 | pytest.skip( 63 | "selfdestruct opcode removed, no need for a codehash change test anymore." 64 | ) 65 | send_bundle_now() 66 | # Asserting that the even though second simulation passes, codehash change is sufficient to remove a userop 67 | # so no bundle was sent. 68 | assert_no_useroperation_event(entrypoint_contract, from_block=block_number) 69 | # Bundler should drop the op from the mempool after codehash changed 70 | assert dump_mempool() == [] 71 | # Sanity check: reconstructing the accounts again to see that they can be bundled 72 | for i in range(2): 73 | tx_hash = codehash_factory_contract.functions.destroy(account0).transact( 74 | {"from": w3.eth.default_account} 75 | ) 76 | w3.eth.wait_for_transaction_receipt(tx_hash) 77 | block_number = w3.eth.get_block_number() 78 | create_account(w3, codehash_factory_contract, entrypoint_contract, i) 79 | userop.nonce = hex(i) 80 | userop.send() 81 | assert dump_mempool() == [userop] 82 | send_bundle_now() 83 | assert dump_mempool() == [] 84 | assert_useroperation_event(entrypoint_contract, userop, from_block=block_number) 85 | -------------------------------------------------------------------------------- /tests/contracts/Create2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // OpenZeppelin Contracts (last updated v4.8.0) (utils/Create2.sol) 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer. 8 | * `CREATE2` can be used to compute in advance the address where a smart 9 | * contract will be deployed, which allows for interesting new mechanisms known 10 | * as 'counterfactual interactions'. 11 | * 12 | * See the https://eips.ethereum.org/EIPS/eip-1014#motivation[EIP] for more 13 | * information. 14 | */ 15 | library Create2 { 16 | /** 17 | * @dev Deploys a contract using `CREATE2`. The address where the contract 18 | * will be deployed can be known in advance via {computeAddress}. 19 | * 20 | * The bytecode for a contract can be obtained from Solidity with 21 | * `type(contractName).creationCode`. 22 | * 23 | * Requirements: 24 | * 25 | * - `bytecode` must not be empty. 26 | * - `salt` must have not been used for `bytecode` already. 27 | * - the factory must have a balance of at least `amount`. 28 | * - if `amount` is non-zero, `bytecode` must have a `payable` constructor. 29 | */ 30 | function deploy( 31 | uint256 amount, 32 | bytes32 salt, 33 | bytes memory bytecode 34 | ) internal returns (address addr) { 35 | require(address(this).balance >= amount, "Create2: insufficient balance"); 36 | require(bytecode.length != 0, "Create2: bytecode length is zero"); 37 | /// @solidity memory-safe-assembly 38 | assembly { 39 | addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) 40 | } 41 | require(addr != address(0), "Create2: Failed on deploy"); 42 | } 43 | 44 | /** 45 | * @dev Returns the address where a contract will be stored if deployed via {deploy}. Any change in the 46 | * `bytecodeHash` or `salt` will result in a new destination address. 47 | */ 48 | function computeAddress(bytes32 salt, bytes32 bytecodeHash) internal view returns (address) { 49 | return computeAddress(salt, bytecodeHash, address(this)); 50 | } 51 | 52 | /** 53 | * @dev Returns the address where a contract will be stored if deployed via {deploy} from a contract located at 54 | * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}. 55 | */ 56 | function computeAddress( 57 | bytes32 salt, 58 | bytes32 bytecodeHash, 59 | address deployer 60 | ) internal pure returns (address addr) { 61 | /// @solidity memory-safe-assembly 62 | assembly { 63 | let ptr := mload(0x40) // Get free memory pointer 64 | 65 | // | | ↓ ptr ... ↓ ptr + 0x0B (start) ... ↓ ptr + 0x20 ... ↓ ptr + 0x40 ... | 66 | // |-------------------|---------------------------------------------------------------------------| 67 | // | bytecodeHash | CCCCCCCCCCCCC...CC | 68 | // | salt | BBBBBBBBBBBBB...BB | 69 | // | deployer | 000000...0000AAAAAAAAAAAAAAAAAAA...AA | 70 | // | 0xFF | FF | 71 | // |-------------------|---------------------------------------------------------------------------| 72 | // | memory | 000000...00FFAAAAAAAAAAAAAAAAAAA...AABBBBBBBBBBBBB...BBCCCCCCCCCCCCC...CC | 73 | // | keccak(start, 85) | ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ | 74 | 75 | mstore(add(ptr, 0x40), bytecodeHash) 76 | mstore(add(ptr, 0x20), salt) 77 | mstore(ptr, deployer) // Right-aligned with 12 preceding garbage bytes 78 | let start := add(ptr, 0x0b) // The hashed data starts at the final garbage byte which we will set to 0xff 79 | mstore8(start, 0xff) 80 | addr := keccak256(start, 85) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/contracts/rip7560/utils/TestUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.12; 3 | 4 | import "@rip7560/contracts/interfaces/IRip7560Transaction.sol"; 5 | 6 | library TestUtils { 7 | 8 | event ValidationTransactionFunctionParams( 9 | uint256 version, 10 | bytes32 txHash 11 | ); 12 | 13 | event Type4TransactionParamsEvent( 14 | address sender, 15 | uint256 nonceKey, 16 | uint256 nonce, 17 | uint256 validationGasLimit, 18 | uint256 paymasterValidationGasLimit, 19 | uint256 postOpGasLimit, 20 | uint256 callGasLimit, 21 | uint256 maxFeePerGas, 22 | uint256 maxPriorityFeePerGas, 23 | uint256 builderFee, 24 | address paymaster, 25 | bytes paymasterData, 26 | address deployer, 27 | bytes deployerData, 28 | bytes callData, 29 | bytes signature 30 | ); 31 | 32 | struct OpcodesOutput { 33 | uint256 GAS; 34 | uint256 GASPRICE; 35 | uint256 BASEFEE; 36 | uint256 BALANCE; 37 | uint256 SELFBALANCE; 38 | uint256 GASLIMIT; 39 | uint256 TIMESTAMP; 40 | uint256 NUMBER; 41 | uint256 CHAINID; 42 | uint256 CALLVALUE; 43 | address ORIGIN; 44 | address CALLER; 45 | address ADDRESS; 46 | address COINBASE; 47 | } 48 | 49 | event OpcodesEvent( 50 | string tag, 51 | OpcodesOutput opcodes 52 | ); 53 | 54 | function emitValidationParams( 55 | uint256 version, 56 | bytes32 txHash, 57 | bytes calldata transaction 58 | ) internal { 59 | emit ValidationTransactionFunctionParams(version, txHash); 60 | 61 | RIP7560Transaction memory txStruct = abi.decode(transaction, (RIP7560Transaction)); 62 | 63 | /* Emit transaction details as seen on-chain */ 64 | emit Type4TransactionParamsEvent( 65 | txStruct.sender, 66 | txStruct.nonceKey, 67 | txStruct.nonceSequence, 68 | txStruct.validationGasLimit, 69 | txStruct.paymasterValidationGasLimit, 70 | txStruct.postOpGasLimit, 71 | txStruct.callGasLimit, 72 | txStruct.maxFeePerGas, 73 | txStruct.maxPriorityFeePerGas, 74 | txStruct.builderFee, 75 | txStruct.paymaster, 76 | txStruct.paymasterData, 77 | txStruct.deployer, 78 | txStruct.deployerData, 79 | txStruct.executionData, 80 | txStruct.authorizationData 81 | ); 82 | } 83 | 84 | function emitEvmData(string memory tag) internal { 85 | emit OpcodesEvent(tag, getOpcodesOutput()); 86 | } 87 | 88 | function getOpcodesOutput() internal returns (OpcodesOutput memory) { 89 | uint256 GAS; 90 | uint256 GASPRICE; 91 | uint256 BASEFEE; 92 | uint256 BALANCE; 93 | uint256 SELFBALANCE; 94 | uint256 GASLIMIT; 95 | uint256 TIMESTAMP; 96 | uint256 NUMBER; 97 | uint256 CHAINID; 98 | uint256 CALLVALUE; 99 | address ORIGIN; 100 | address CALLER; 101 | address ADDRESS; 102 | address COINBASE; 103 | 104 | assembly { 105 | GAS := gas() 106 | GASPRICE := gasprice() 107 | BASEFEE := basefee() 108 | ADDRESS := address() 109 | BALANCE := balance(ADDRESS) 110 | SELFBALANCE := selfbalance() 111 | GASLIMIT := gaslimit() 112 | TIMESTAMP := timestamp() 113 | CHAINID := chainid() 114 | ORIGIN := origin() 115 | CALLER := caller() 116 | CALLVALUE := callvalue() 117 | COINBASE := coinbase() 118 | NUMBER := number() 119 | } 120 | 121 | return OpcodesOutput( 122 | GAS, 123 | GASPRICE, 124 | BASEFEE, 125 | BALANCE, 126 | SELFBALANCE, 127 | GASLIMIT, 128 | TIMESTAMP, 129 | NUMBER, 130 | CHAINID, 131 | CALLVALUE, 132 | ORIGIN, 133 | CALLER, 134 | ADDRESS, 135 | COINBASE 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/find_min.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | 3 | from eth_abi import decode 4 | 5 | from tests.conftest import global_entrypoint 6 | from tests.user_operation_erc4337 import UserOperation 7 | from tests.utils import pack_user_op, to_prefixed_hex, hex_concat 8 | 9 | 10 | def resolve_revert(e): 11 | s = str(e) 12 | # "FailedOpWithRevert(uint256,string,bytes)" 13 | if "0x65c8fd4d" in s: 14 | ret = decode(["uint256", "string", "bytes"], bytes.fromhex(e.data[10:])) 15 | return ValueError( 16 | ret[1] + " data= " + ret[2].hex() # pylint: disable=unsubscriptable-object 17 | ) 18 | # "FailedOp(uint256,string)" 19 | if "0x220266b6" in s: 20 | ret = decode(["uint256", "string"], bytes.fromhex(e.data[10:])) 21 | return ValueError(ret[1]) # pylint: disable=unsubscriptable-object 22 | return e 23 | 24 | 25 | def userop_with_field(op, op_field, val, append=False): 26 | d = asdict(op) 27 | if append: 28 | val = hex_concat(d[op_field], val) 29 | d[op_field] = val 30 | return UserOperation(**d) 31 | 32 | 33 | # submit userop with given field. 34 | # return exception (with error message), or None if success 35 | # This method works directly with entrypoint, bypassing the bundler. 36 | def call_userop_with_gas_field(op, op_field, val, prefix_user_op=None): 37 | """ 38 | Call userop with given field. 39 | :param op: userop to check 40 | :param op_field: field to change 41 | :param val: new value for op_field 42 | :param prefix_user_op: if not None, place that UserOp before the actual tested UserOp 43 | note that prefix_user_op should not revert. 44 | :return: None if success, or exception (with error message) on failure 45 | """ 46 | op1 = userop_with_field(op, op_field, to_prefixed_hex(val)) 47 | b = global_entrypoint() 48 | packed_array = [] 49 | if prefix_user_op: 50 | packed_array.append(pack_user_op(prefix_user_op)) 51 | 52 | packed_array.append(pack_user_op(op1)) 53 | try: 54 | global_entrypoint().functions.handleOps(packed_array, b.address).call( 55 | {"gas": 10**7} 56 | ) 57 | except Exception as e: # pylint: disable=broad-exception-caught 58 | return resolve_revert(e) 59 | return None 60 | 61 | 62 | def find_min_func(func, low, high): 63 | """ 64 | Find the minimum value that reverts with the given function. 65 | :param func: function to call, with current value as argument. Returns None if success 66 | :param low: minimum value. function should fail with this value 67 | :param high: maximum value. function should succeed with this value 68 | :return: minimum value that succeeds 69 | """ 70 | e = func(high) 71 | if e: 72 | raise ValueError(f"high {high} should be high enough, but reverts with: {e}") 73 | 74 | e = func(low) 75 | if not e: 76 | raise ValueError(f"low {low} should be low enough, but does not revert") 77 | 78 | while low < high: 79 | mid = low + (high - low) // 2 80 | e = func(mid) 81 | if e is None: 82 | high = mid 83 | else: 84 | low = mid + 1 85 | return low 86 | 87 | 88 | def find_min_userop_with_field(op, op_field, low, high, prefix_user_op=None): 89 | return find_min_func( 90 | lambda val: call_userop_with_gas_field(op, op_field, val, prefix_user_op), 91 | low, 92 | high, 93 | ) 94 | 95 | 96 | def find_verification_limits(op, prefix_user_op=None): 97 | """ 98 | find minimum verificationGasLimit, paymasterVerificationGasLimit for this userop. 99 | :param op: 100 | :param prefix_user_op: 101 | :return: 102 | """ 103 | has_paymaster = op.paymaster is not None 104 | if has_paymaster: 105 | op = userop_with_field(op, "paymasterVerificationGasLimit", 100_000) 106 | vgl = find_min_userop_with_field( 107 | op, "verificationGasLimit", 0, 10**6, prefix_user_op 108 | ) 109 | if has_paymaster: 110 | op = userop_with_field(op, "verificationGasLimit", vgl) 111 | pmvgl = find_min_userop_with_field( 112 | op, "paymasterVerificationGasLimit", 0, 10**6, prefix_user_op 113 | ) 114 | else: 115 | pmvgl = None 116 | 117 | return vgl, pmvgl 118 | -------------------------------------------------------------------------------- /tests/rip7560/test_env_opcodes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from eth_utils import to_checksum_address 3 | 4 | from tests.rip7560.types import TransactionRIP7560 5 | from tests.utils import ( 6 | assert_ok, 7 | deploy_contract, 8 | send_bundle_now, 9 | get_rip7560_tx_max_cost, 10 | ) 11 | 12 | AA_ENTRYPOINT = "0x0000000000000000000000000000000000007560" 13 | AA_SENDER_CREATOR = "0x00000000000000000000000000000000ffff7560" 14 | 15 | 16 | def test_environment_introspection_opcodes(w3): 17 | account_abi = deploy_contract(w3, "rip7560/OpcodesTestAccount") 18 | factory = deploy_contract(w3, "rip7560/OpcodesTestAccountFactory") 19 | paymaster = deploy_contract(w3, "rip7560/OpcodesTestPaymaster", value=1 * 10**18) 20 | 21 | create_account_func = factory.functions.createAccount(1) 22 | sender = create_account_func.call() 23 | sender_contract = w3.eth.contract(abi=account_abi.abi, address=sender) 24 | 25 | tx = TransactionRIP7560( 26 | sender=sender, 27 | nonceKey=hex(0), 28 | nonce=hex(0), 29 | factory=factory.address, 30 | factoryData=create_account_func.build_transaction()["data"], 31 | paymaster=paymaster.address, 32 | maxFeePerGas=hex(400000000), 33 | maxPriorityFeePerGas=hex(400000000), 34 | authorizationData="0xface", 35 | executionData=account_abi.encode_abi(abi_element_identifier="saveEventOpcodes"), 36 | ) 37 | 38 | paymaster_balance_before = w3.eth.get_balance(paymaster.address) 39 | 40 | ret = tx.send_skip_validation() 41 | assert_ok(ret) 42 | send_bundle_now() 43 | 44 | account_opcode_events = sender_contract.events.OpcodesEvent().get_logs() 45 | paymaster_opcode_events = paymaster.events.OpcodesEvent().get_logs() 46 | factory_opcode_events = factory.events.OpcodesEvent().get_logs() 47 | assert len(account_opcode_events) == 2 48 | assert len(paymaster_opcode_events) == 2 49 | assert len(factory_opcode_events) == 1 50 | 51 | tmp_deployment_gas_cost = ( 52 | 230000 # we should expose these values in the receipt and use the real ones 53 | # for now the value is extracted from the geth code manually 54 | ) 55 | pre_charge = get_rip7560_tx_max_cost(tx) 56 | # paymaster balance should not change within the transaction runtime 57 | expected_paymaster_balance = paymaster_balance_before - pre_charge 58 | tmp_remaining_account_validation_gas = hex( 59 | int(str(tx.verificationGasLimit), 0) - tmp_deployment_gas_cost 60 | ) 61 | validate_event( 62 | w3, 63 | tx, 64 | "factory-validation", 65 | factory.address, 66 | AA_SENDER_CREATOR, 67 | tx.verificationGasLimit, 68 | factory_opcode_events[0].args, 69 | ) 70 | validate_event( 71 | w3, 72 | tx, 73 | "account-validation", 74 | sender, 75 | AA_ENTRYPOINT, 76 | tmp_remaining_account_validation_gas, 77 | account_opcode_events[0].args, 78 | ) 79 | validate_event( 80 | w3, 81 | tx, 82 | "paymaster-validation", 83 | paymaster.address, 84 | AA_ENTRYPOINT, 85 | tx.paymasterVerificationGasLimit, 86 | paymaster_opcode_events[0].args, 87 | expected_paymaster_balance, 88 | ) 89 | validate_event( 90 | w3, 91 | tx, 92 | "account-execution", 93 | sender, 94 | AA_ENTRYPOINT, 95 | tx.callGasLimit, 96 | account_opcode_events[1].args, 97 | ) 98 | validate_event( 99 | w3, 100 | tx, 101 | "paymaster-postop", 102 | paymaster.address, 103 | AA_ENTRYPOINT, 104 | tx.paymasterPostOpGasLimit, 105 | paymaster_opcode_events[1].args, 106 | expected_paymaster_balance, 107 | ) 108 | 109 | 110 | # pylint: disable=too-many-arguments 111 | def validate_event( 112 | w3, tx, tag, entity_address, caller, gas, event_args, expected_balance=None 113 | ): 114 | assert event_args.tag == tag 115 | struct = dict(event_args.opcodes) 116 | 117 | if expected_balance is None: 118 | expected_balance = w3.eth.get_balance(entity_address) 119 | block = w3.eth.get_block("latest", True) 120 | 121 | tx_prio = int(tx.maxPriorityFeePerGas, 16) 122 | tx_maxfee = int(tx.maxFeePerGas, 16) 123 | block_basefee = block.baseFeePerGas 124 | 125 | expected_struct = { 126 | "TIMESTAMP": block.timestamp, 127 | "NUMBER": block.number, 128 | "CHAINID": 1337, 129 | "GAS": pytest.approx(int(gas, 16), rel=0.15), 130 | "GASLIMIT": block.gasLimit, 131 | "GASPRICE": min(tx_maxfee, tx_prio + block_basefee), 132 | "BALANCE": expected_balance, 133 | "SELFBALANCE": expected_balance, 134 | "ORIGIN": tx.sender, 135 | "CALLER": to_checksum_address(caller), 136 | "ADDRESS": entity_address, 137 | "COINBASE": block.miner, 138 | "BASEFEE": block.baseFeePerGas, 139 | "CALLVALUE": 0, 140 | } 141 | assert struct == expected_struct 142 | -------------------------------------------------------------------------------- /tests/single/opbanning/test_op_banning.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test suite for `eip4337 bunlder` module. 3 | See https://github.com/eth-infinitism/bundler 4 | """ 5 | 6 | import pytest 7 | from tests.conftest import entrypoint_contract 8 | 9 | from tests.types import RPCErrorCode 10 | from tests.user_operation_erc4337 import UserOperation 11 | from tests.utils import ( 12 | assert_ok, 13 | assert_rpc_error, 14 | deposit_to_undeployed_sender, 15 | to_hex, 16 | to_prefixed_hex, 17 | ) 18 | 19 | 20 | # All opcodes are part of the OP-011 rule 21 | banned_opcodes = [ 22 | "GAS", 23 | "NUMBER", 24 | "TIMESTAMP", 25 | "COINBASE", 26 | "DIFFICULTY", 27 | "BASEFEE", 28 | "GASLIMIT", 29 | "GASPRICE", 30 | "SELFBALANCE", 31 | "BALANCE", 32 | "ORIGIN", 33 | "BLOCKHASH", 34 | "CREATE", 35 | "CREATE2", 36 | "SELFDESTRUCT", 37 | "BLOBHASH", 38 | "BLOBBASEFEE", 39 | ] 40 | 41 | # the "OP-052" tested elsewhere 42 | allowed_opcode_sequences = ["GAS CALL", "GAS DELEGATECALL"] 43 | 44 | # All opcodes as part of the OP-080 rule 45 | allowed_opcodes_when_staked = ["BALANCE", "SELFBALANCE"] 46 | 47 | 48 | @pytest.mark.parametrize("frame_entry_opcode", ["", "CALL:>", "DELEGATECALL:>"]) 49 | @pytest.mark.parametrize("banned_op", banned_opcodes) 50 | def test_account_banned_opcode(rules_account_contract, banned_op, frame_entry_opcode): 51 | response = UserOperation( 52 | sender=rules_account_contract.address, 53 | signature=to_prefixed_hex(frame_entry_opcode + banned_op), 54 | ).send() 55 | assert_rpc_error( 56 | response, "account uses banned opcode: " + banned_op, RPCErrorCode.BANNED_OPCODE 57 | ) 58 | 59 | 60 | @pytest.mark.parametrize("frame_entry_opcode", ["", "CALL:>", "DELEGATECALL:>"]) 61 | @pytest.mark.parametrize("op", allowed_opcodes_when_staked) 62 | def test_account_allowed_opcode_when_staked( 63 | rules_staked_account_contract, op, frame_entry_opcode 64 | ): 65 | response = UserOperation( 66 | sender=rules_staked_account_contract.address, 67 | signature=to_prefixed_hex(frame_entry_opcode + op), 68 | ).send() 69 | assert_ok(response) 70 | 71 | 72 | # OP-012 73 | @pytest.mark.parametrize("frame_entry_opcode", ["", "CALL:>", "DELEGATECALL:>"]) 74 | @pytest.mark.parametrize("allowed_op_sequence", allowed_opcode_sequences) 75 | def test_account_allowed_opcode_sequence( 76 | rules_account_contract, allowed_op_sequence, frame_entry_opcode 77 | ): 78 | response = UserOperation( 79 | sender=rules_account_contract.address, 80 | signature=to_prefixed_hex(frame_entry_opcode + allowed_op_sequence), 81 | ).send() 82 | assert_ok(response) 83 | 84 | 85 | @pytest.mark.parametrize("frame_entry_opcode", ["", "CALL:>", "DELEGATECALL:>"]) 86 | @pytest.mark.parametrize("banned_op", banned_opcodes) 87 | def test_paymaster_banned_opcode( 88 | paymaster_contract, wallet_contract, banned_op, frame_entry_opcode 89 | ): 90 | response = UserOperation( 91 | sender=wallet_contract.address, 92 | paymaster=paymaster_contract.address, 93 | paymasterData="0x" + to_hex(frame_entry_opcode + banned_op), 94 | paymasterVerificationGasLimit=hex(300000), 95 | ).send() 96 | assert_rpc_error( 97 | response, 98 | "paymaster uses banned opcode: " + banned_op, 99 | RPCErrorCode.BANNED_OPCODE, 100 | ) 101 | 102 | 103 | @pytest.mark.parametrize("op", allowed_opcodes_when_staked) 104 | def test_paymaster_allowed_opcode_when_staked( 105 | staked_paymaster_contract, wallet_contract, op 106 | ): 107 | response = UserOperation( 108 | sender=wallet_contract.address, 109 | paymaster=staked_paymaster_contract.address, 110 | paymasterVerificationGasLimit=hex(50000), 111 | paymasterData=to_prefixed_hex(op), 112 | ).send() 113 | assert_ok(response) 114 | 115 | 116 | @pytest.mark.parametrize("banned_op", banned_opcodes) 117 | def test_factory_banned_opcode(w3, factory_contract, entrypoint_contract, banned_op): 118 | factoryData = factory_contract.functions.create( 119 | 123, banned_op, entrypoint_contract.address 120 | ).build_transaction()["data"] 121 | sender = deposit_to_undeployed_sender( 122 | w3, entrypoint_contract, factory_contract.address, factoryData 123 | ) 124 | response = UserOperation( 125 | sender=sender, factory=factory_contract.address, factoryData=factoryData 126 | ).send() 127 | assert_rpc_error( 128 | response, 129 | "factory", 130 | RPCErrorCode.BANNED_OPCODE, 131 | ) 132 | assert_rpc_error( 133 | response, 134 | banned_op, 135 | RPCErrorCode.BANNED_OPCODE, 136 | ) 137 | 138 | 139 | @pytest.mark.parametrize("op", allowed_opcodes_when_staked) 140 | def test_factory_allowed_opcode_when_staked( 141 | w3, staked_factory_contract, entrypoint_contract, op 142 | ): 143 | factory_data = staked_factory_contract.functions.create( 144 | 123, op, entrypoint_contract.address 145 | ).build_transaction()["data"] 146 | 147 | sender = deposit_to_undeployed_sender( 148 | w3, entrypoint_contract, staked_factory_contract.address, factory_data 149 | ) 150 | response = UserOperation( 151 | sender=sender, factory=staked_factory_contract.address, factoryData=factory_data 152 | ).send() 153 | assert_ok(response) 154 | -------------------------------------------------------------------------------- /tests/rip7560/test_validation_rules.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from web3.constants import ADDRESS_ZERO 3 | from tests.rip7560.types import TransactionRIP7560 4 | from tests.single.bundle.test_storage_rules import ( 5 | cases, 6 | case_id_function, 7 | PAYMASTER, 8 | FACTORY, 9 | SENDER, 10 | AGGREGATOR, 11 | ) 12 | 13 | from tests.utils import ( 14 | assert_ok, 15 | deploy_contract, 16 | deploy_state_contract, 17 | fund, 18 | staked_contract, 19 | ) 20 | 21 | 22 | def deploy_unstaked_factory(w3, _): 23 | contract = deploy_contract( 24 | w3, 25 | "rip7560/RIP7560TestRulesAccountDeployer", 26 | value=1 * 10**18, 27 | ) 28 | return contract 29 | 30 | 31 | def deploy_staked_factory(w3, stake_manager): 32 | contract = deploy_contract( 33 | w3, 34 | "rip7560/RIP7560TestRulesAccountDeployer", 35 | value=1 * 10**18, 36 | ) 37 | contract = staked_contract(w3, stake_manager, contract) 38 | return contract 39 | 40 | 41 | def with_initcode( 42 | build_tx7560_func, stake_manager, deploy_factory_func=deploy_unstaked_factory 43 | ): 44 | def _with_initcode(w3, contract, rule): 45 | factory_contract = deploy_factory_func(w3, stake_manager) 46 | tx7560 = build_tx7560_func(w3, contract, rule) 47 | factory_data = factory_contract.functions.createAccount( 48 | ADDRESS_ZERO, 123, "" 49 | ).build_transaction()["data"] 50 | sender = factory_contract.functions.getCreate2Address( 51 | ADDRESS_ZERO, 123, "" 52 | ).call() 53 | fund(w3, sender) 54 | tx7560.sender = sender 55 | tx7560.factory = factory_contract.address 56 | tx7560.factoryData = factory_data 57 | tx7560.verificationGasLimit = hex(5000000) 58 | tx7560.nonce = hex(0) 59 | return tx7560 60 | 61 | return _with_initcode 62 | 63 | 64 | def build_tx7560_for_paymaster(w3, paymaster_contract, rule): 65 | wallet = deploy_contract(w3, "rip7560/TestAccount") 66 | return TransactionRIP7560( 67 | sender=wallet.address, 68 | paymaster=paymaster_contract.address, 69 | paymasterData="0x" + rule.encode().hex(), 70 | nonceKey=hex(0), 71 | nonce=hex(1), 72 | ) 73 | 74 | 75 | def build_tx7560_for_sender(w3, rules_account_contract, rule): 76 | call_data = deploy_state_contract(w3).address 77 | signature = "0x" + rule.encode().hex() 78 | return TransactionRIP7560( 79 | sender=rules_account_contract.address, 80 | executionData=call_data, 81 | authorizationData=signature, 82 | nonceKey=hex(0), 83 | nonce=hex(2), 84 | ) 85 | 86 | 87 | def build_tx7560_for_factory(w3, factory_contract, rule): 88 | # pass 89 | factory_data = factory_contract.functions.createAccount( 90 | ADDRESS_ZERO, 123, rule 91 | ).build_transaction()["data"] 92 | sender = factory_contract.functions.getCreate2Address( 93 | ADDRESS_ZERO, 123, rule 94 | ).call() 95 | fund(w3, sender) 96 | return TransactionRIP7560( 97 | sender=sender, factory=factory_contract.address, factoryData=factory_data 98 | ) 99 | 100 | 101 | def entity_to_contract_name(entity): 102 | if entity == PAYMASTER: 103 | return "rip7560/RIP7560Paymaster" 104 | if entity == FACTORY: 105 | return "rip7560/RIP7560Deployer" 106 | if entity == SENDER: 107 | return "rip7560/RIP7560TestRulesAccount" 108 | if entity == AGGREGATOR: 109 | # Not implemented yet 110 | return None 111 | return None 112 | 113 | 114 | def get_build_func(entity, rule, assert_func, stake_manager): 115 | build_func = None 116 | if entity == PAYMASTER: 117 | build_func = build_tx7560_for_paymaster 118 | elif entity == FACTORY: 119 | build_func = build_tx7560_for_factory 120 | elif entity == SENDER: 121 | build_func = build_tx7560_for_sender 122 | elif entity == AGGREGATOR: 123 | # Not implemented yet 124 | pass 125 | if rule.find("init_code") > 0: 126 | # STO-022 factory must be staked 127 | if assert_func is assert_ok: 128 | build_func = with_initcode(build_func, stake_manager, deploy_staked_factory) 129 | else: 130 | build_func = with_initcode(build_func, stake_manager) 131 | return build_func 132 | 133 | 134 | @pytest.mark.parametrize("case", cases, ids=case_id_function) 135 | def test_rule(w3, stake_manager, case): 136 | # skip unimplemented EREP-050 (was fixed only for 4337) 137 | if case.ruleId == "EREP-050": 138 | pytest.skip("EREP-050 is not implemented yet") 139 | 140 | # EntryPoint rules are mostly irrelevant 141 | if case.rule == "entryPoint_call_balanceOf" or case.rule in ( 142 | "eth_value_transfer_entryPoint", 143 | "eth_value_transfer_entryPoint_depositTo", 144 | ): 145 | pytest.skip() 146 | 147 | entity_contract_name = entity_to_contract_name(case.entity) 148 | entity_contract = deploy_contract(w3, entity_contract_name, value=1 * 10**18) 149 | if case.staked: 150 | entity_contract = staked_contract(w3, stake_manager, entity_contract) 151 | build_func = get_build_func(case.entity, case.rule, case.assert_func, stake_manager) 152 | tx7560 = build_func(w3, entity_contract, case.rule) 153 | response = tx7560.send() 154 | case.assert_func(response) 155 | -------------------------------------------------------------------------------- /tests/single/opbanning/test_create.py: -------------------------------------------------------------------------------- 1 | # create opcode rules 2 | 3 | """ 4 | **[OP-031]** `CREATE2` is allowed exactly once in the deployment phase and must deploy code for the "sender" address. 5 | (Either by the factory itself, or by a utility contract it calls) 6 | * **[OP-032]** If there is a `factory` (even unstaked), the `sender` contract is allowed to use `CREATE` opcode 7 | (That is, only the sender contract itself, not through utility contract) 8 | * Staked factory creation rules: 9 | * **[EREP-060]** If the factory is staked, either the factory itself or the sender may use the CREATE2 and CREATE opcode 10 | (the sender is allowed to use the CREATE with unstaked factory, with OP-032) 11 | * **[EREP-061]** A staked factory may also use a utility contract that calls the `CREATE` 12 | 13 | if no factory, create/create2 are banned (even for staked entities: they could potentially create another account, and thus 14 | "blame" another userop, which we can't link back to this one) 15 | """ 16 | 17 | from dataclasses import dataclass 18 | import pytest 19 | from tests.conftest import assert_ok, deploy_contract 20 | from tests.types import RPCErrorCode 21 | from tests.user_operation_erc4337 import UserOperation 22 | from tests.utils import ( 23 | assert_rpc_error, 24 | deposit_to_undeployed_sender, 25 | staked_contract, 26 | to_hex, 27 | to_prefixed_hex, 28 | ) 29 | 30 | 31 | # OP-031 32 | # OP-032 33 | @pytest.mark.parametrize("create_op", ["CREATE", "CREATE2"]) 34 | def test_account_no_factory(rules_account_contract, create_op): 35 | userop = UserOperation( 36 | sender=rules_account_contract.address, signature=to_prefixed_hex(create_op) 37 | ) 38 | assert_rpc_error( 39 | userop.send(), 40 | "account uses banned opcode: " + create_op, 41 | RPCErrorCode.BANNED_OPCODE, 42 | ) 43 | 44 | 45 | @pytest.mark.parametrize("create_op", ["CREATE", "CREATE2"]) 46 | def test_paymaster_banned_create(paymaster_contract, wallet_contract, create_op): 47 | userop = UserOperation( 48 | sender=wallet_contract.address, 49 | paymaster=paymaster_contract.address, 50 | paymasterData="0x" + to_hex(create_op), 51 | paymasterVerificationGasLimit=hex(300_000), 52 | ) 53 | assert_rpc_error( 54 | userop.send(), 55 | "paymaster uses banned opcode: " + create_op, 56 | RPCErrorCode.BANNED_OPCODE, 57 | ) 58 | 59 | 60 | # OP-031 CREATE2 is allowed exactly once in the deployment phase. 61 | # (so we check that it fails if it called more than once) 62 | def test_account_create2_once(w3, factory_contract, entrypoint_contract): 63 | factoryData = factory_contract.functions.create( 64 | 123, "CREATE2", entrypoint_contract.address 65 | ).build_transaction()["data"] 66 | sender = deposit_to_undeployed_sender( 67 | w3, entrypoint_contract, factory_contract.address, factoryData 68 | ) 69 | response = UserOperation( 70 | sender=sender, factory=factory_contract.address, factoryData=factoryData 71 | ).send() 72 | assert_rpc_error( 73 | response, 74 | "factory", 75 | RPCErrorCode.BANNED_OPCODE, 76 | ) 77 | assert_rpc_error( 78 | response, 79 | "CREATE2", 80 | RPCErrorCode.BANNED_OPCODE, 81 | ) 82 | 83 | 84 | # generic dataclass: can be constructed with any named param. 85 | # 86 | @dataclass 87 | class Case: 88 | def __init__(self, **kwargs): 89 | for k, v in kwargs.items(): 90 | setattr(self, k, v) 91 | 92 | def str(self): 93 | return ",".join([k + "=" + str(v) for k, v in self.__dict__.items()]) 94 | 95 | 96 | # [OP-031] `CREATE2` is allowed exactly once in the deployment phase and must deploy code for the "sender" address. 97 | # [OP-032] If there is a `factory` (even unstaked), the `sender` contract is allowed to use `CREATE` opcode 98 | 99 | # [EREP-060] If the factory is staked, either the factory itself or the sender may use the CREATE2 and CREATE opcode 100 | # [EREP-061] A staked factory may also use a utility contract that calls the `CREATE` 101 | account_cases = [ 102 | Case(factory="unstaked", op="CREATE", expect="ok", rule="OP-032"), 103 | Case(factory="unstaked", op="CREATE2", expect="err", rule="OP-032"), 104 | Case(factory="staked", op="CREATE", expect="ok", rule="EREP-060"), 105 | Case(factory="staked", op="nested-CREATE", expect="err", rule="EREP-060"), 106 | Case(factory="staked", op="nested-CREATE2", expect="err", rule="EREP-060"), 107 | Case(factory="staked", op="CREATE2", expect="ok", rule="EREP-060"), 108 | ] 109 | 110 | 111 | @pytest.mark.parametrize("case", account_cases, ids=Case.str) 112 | def test_account_create_with_factory(w3, entrypoint_contract, case): 113 | factory = deploy_contract( 114 | w3, "TestRulesAccountFactory", ctrparams=[entrypoint_contract.address] 115 | ) 116 | if case.factory == "staked": 117 | staked_contract(w3, entrypoint_contract, factory) 118 | 119 | factoryData = factory.functions.create( 120 | 123, "", entrypoint_contract.address 121 | ).build_transaction()["data"] 122 | 123 | sender = deposit_to_undeployed_sender( 124 | w3, entrypoint_contract, factory.address, factoryData 125 | ) 126 | 127 | userop = UserOperation( 128 | sender=sender, 129 | signature=to_prefixed_hex(case.op), 130 | factory=factory.address, 131 | factoryData=factoryData, 132 | verificationGasLimit=hex(5000000), 133 | ) 134 | response = userop.send() 135 | if case.expect == "ok": 136 | assert_ok(response) 137 | else: 138 | assert_rpc_error( 139 | response, 140 | "account uses banned opcode: " + case.op.replace("nested-", ""), 141 | RPCErrorCode.BANNED_OPCODE, 142 | ) 143 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import pytest 4 | from eth_account import Account 5 | from solcx import install_solc 6 | from web3 import Web3 7 | from web3.middleware import SignAndSendRawMiddlewareBuilder, ExtraDataToPOAMiddleware 8 | from .types import RPCRequest, CommandLineArgs 9 | 10 | from .user_operation_erc4337 import UserOperation 11 | 12 | from .utils import ( 13 | assert_ok, 14 | clear_state, 15 | compile_contract, 16 | deploy_and_deposit, 17 | deploy_contract, 18 | deploy_wallet_contract, 19 | send_bundle_now, 20 | set_manual_bundling_mode, 21 | ) 22 | 23 | 24 | def pytest_configure(config): 25 | CommandLineArgs.configure( 26 | url=config.getoption("--url"), 27 | entrypoint=config.getoption("--entry-point"), 28 | query_ep=config.getoption("--query-ep"), 29 | nonce_manager=config.getoption("--nonce-manager"), 30 | stake_manager=config.getoption("--stake-manager"), 31 | ethereum_node=config.getoption("--ethereum-node"), 32 | launcher_script=config.getoption("--launcher-script"), 33 | log_rpc=config.getoption("--log-rpc"), 34 | ) 35 | install_solc(version="0.8.28") 36 | 37 | 38 | def pytest_sessionstart(): 39 | if CommandLineArgs.launcher_script is not None: 40 | subprocess.run( 41 | [CommandLineArgs.launcher_script, "start"], check=True, text=True 42 | ) 43 | 44 | 45 | def pytest_sessionfinish(): 46 | if CommandLineArgs.launcher_script is not None: 47 | subprocess.run([CommandLineArgs.launcher_script, "stop"], check=True, text=True) 48 | 49 | 50 | def pytest_addoption(parser): 51 | parser.addoption("--url", action="store") 52 | parser.addoption("--entry-point", action="store") 53 | parser.addoption("--nonce-manager", action="store") 54 | parser.addoption("--stake-manager", action="store") 55 | parser.addoption("--ethereum-node", action="store") 56 | parser.addoption("--launcher-script", action="store") 57 | parser.addoption("--log-rpc", action="store_true", default=False) 58 | parser.addoption("--query-ep", action="store_true", default=False) 59 | 60 | 61 | @pytest.fixture(scope="session") 62 | def w3(): 63 | w3 = Web3(Web3.HTTPProvider(CommandLineArgs.ethereum_node)) 64 | if len(w3.eth.accounts) == 0: 65 | private_key = ( 66 | "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" 67 | ) 68 | # pylint doesn't deal with @combomethod well 69 | # pylint: disable = no-value-for-parameter 70 | account = Account.from_key(private_key) 71 | w3.eth.default_account = account.address 72 | w3.middleware_onion.add(SignAndSendRawMiddlewareBuilder.build(private_key)) 73 | else: 74 | w3.eth.default_account = w3.eth.accounts[0] 75 | w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) 76 | return w3 77 | 78 | 79 | @pytest.fixture 80 | def wallet_contract(w3): 81 | return deploy_wallet_contract(w3) 82 | 83 | 84 | @pytest.fixture(scope="session") 85 | def entrypoint_contract(w3): 86 | entrypoint_interface = compile_contract( 87 | "@account-abstraction/contracts/interfaces/IEntryPoint" 88 | ) 89 | return w3.eth.contract( 90 | abi=entrypoint_interface["abi"], address=CommandLineArgs.entrypoint 91 | ) 92 | 93 | 94 | @pytest.fixture 95 | def paymaster_contract(w3, entrypoint_contract): 96 | return deploy_and_deposit(w3, entrypoint_contract, "TestRulesPaymaster", False) 97 | 98 | 99 | @pytest.fixture 100 | def staked_paymaster_contract(w3, entrypoint_contract): 101 | return deploy_and_deposit(w3, entrypoint_contract, "TestRulesPaymaster", True) 102 | 103 | 104 | @pytest.fixture 105 | def factory_contract(w3, entrypoint_contract): 106 | return deploy_and_deposit(w3, entrypoint_contract, "TestRulesFactory", False) 107 | 108 | 109 | @pytest.fixture 110 | def staked_factory_contract(w3, entrypoint_contract): 111 | return deploy_and_deposit(w3, entrypoint_contract, "TestRulesFactory", True) 112 | 113 | 114 | @pytest.fixture 115 | def rules_account_contract(w3, entrypoint_contract): 116 | return deploy_and_deposit(w3, entrypoint_contract, "TestRulesAccount", False) 117 | 118 | 119 | @pytest.fixture 120 | def rules_staked_account_contract(w3, entrypoint_contract): 121 | return deploy_and_deposit(w3, entrypoint_contract, "TestRulesAccount", True) 122 | 123 | 124 | @pytest.fixture(scope="session") 125 | def helper_contract(w3): 126 | return deploy_contract(w3, "Helper") 127 | 128 | 129 | @pytest.fixture 130 | def userop(wallet_contract): 131 | return UserOperation( 132 | sender=wallet_contract.address, 133 | callData=wallet_contract.encode_abi( 134 | abi_element_identifier="setState", args=[1111111] 135 | ), 136 | signature="0xface", 137 | ) 138 | 139 | 140 | @pytest.fixture 141 | def execute_user_operation(userop): 142 | userop.send() 143 | send_bundle_now() 144 | 145 | 146 | # debug apis 147 | 148 | 149 | # applied to all tests: clear mempool, reputation before each test 150 | @pytest.fixture(autouse=True) 151 | def clear_state_before_each_test(): 152 | clear_state() 153 | 154 | 155 | @pytest.fixture 156 | def manual_bundling_mode(): 157 | return set_manual_bundling_mode() 158 | 159 | 160 | @pytest.fixture 161 | def auto_bundling_mode(): 162 | assert_ok( 163 | RPCRequest(method="debug_bundler_setBundlingMode", params=["auto"]).send() 164 | ) 165 | 166 | 167 | @pytest.fixture 168 | def set_reputation(reputations): 169 | assert_ok( 170 | RPCRequest( 171 | method="debug_bundler_setReputation", 172 | params=[reputations, CommandLineArgs.entrypoint], 173 | ).send() 174 | ) 175 | 176 | 177 | @pytest.fixture 178 | def impl7702(w3): 179 | return deploy_contract(w3, "SimpleWallet", ctrparams=[CommandLineArgs.entrypoint]) 180 | 181 | 182 | _GLOBAL_EP = None 183 | 184 | 185 | @pytest.fixture(scope="session", autouse=True) 186 | def set_global_entrypoint(entrypoint_contract): 187 | # pylint: disable=global-statement 188 | global _GLOBAL_EP 189 | _GLOBAL_EP = entrypoint_contract 190 | 191 | 192 | def global_entrypoint(): 193 | assert ( 194 | _GLOBAL_EP is not None 195 | ), "global_entrypoint() called before set_global_entrypoint()" 196 | return _GLOBAL_EP 197 | -------------------------------------------------------------------------------- /tests/single/reputation/test_erep.py: -------------------------------------------------------------------------------- 1 | # extended reputation rules 2 | from dataclasses import dataclass 3 | 4 | import pytest 5 | from eth_utils import to_hex 6 | 7 | from tests.user_operation_erc4337 import UserOperation 8 | from tests.utils import ( 9 | deploy_contract, 10 | deploy_and_deposit, 11 | assert_ok, 12 | clear_state, 13 | dump_reputation, 14 | set_reputation, 15 | staked_contract, 16 | to_number, 17 | send_bundle_now, 18 | to_prefixed_hex, 19 | ) 20 | 21 | 22 | @dataclass 23 | class Reputation: 24 | address: str 25 | opsSeen: int = 0 26 | opsIncluded: int = 0 27 | status: int = 0 28 | 29 | def __post_init__(self): 30 | self.address = self.address.lower() 31 | self.opsSeen = to_number(self.opsSeen) 32 | self.opsIncluded = to_number(self.opsIncluded) 33 | self.status = to_number(self.status) 34 | 35 | 36 | def get_reputation(addr, reputations=None): 37 | if reputations is None: 38 | reputations = dump_reputation() 39 | addr = addr.lower() 40 | reps = [rep for rep in reputations if rep["address"].lower() == addr] 41 | 42 | if len(reps) == 0: 43 | rep = Reputation(address=addr, opsSeen=0, opsIncluded=0, status=0) 44 | else: 45 | rep = Reputation(**reps[0]) 46 | 47 | return rep 48 | 49 | 50 | def get_reputations(addrs): 51 | reputations = dump_reputation() 52 | return [get_reputation(addr, reputations) for addr in addrs] 53 | 54 | 55 | # EREP-015 A `paymaster` should not have its opsSeen incremented on failure of factory or account 56 | def test_paymaster_on_account_failure(w3, entrypoint_contract, manual_bundling_mode): 57 | """ 58 | - paymaster with some reputation value (nonezero opsSeen/opsIncluded) 59 | - submit userop that passes validation 60 | - paymaster's opsSeen incremented in-memory 61 | - 2nd validation fails (because of account/factory) 62 | - paymaster's opsSeen should remain the same 63 | """ 64 | account = deploy_contract( 65 | w3, "TestReputationAccount", [entrypoint_contract.address], 10**18 66 | ) 67 | paymaster = deploy_and_deposit( 68 | w3, entrypoint_contract, "TestRulesPaymaster", staked=True 69 | ) 70 | clear_state() 71 | set_reputation(paymaster.address, ops_seen=5, ops_included=2) 72 | pre = get_reputation(paymaster.address) 73 | assert_ok( 74 | UserOperation( 75 | sender=account.address, 76 | paymaster=paymaster.address, 77 | paymasterVerificationGasLimit=to_hex(50000), 78 | paymasterData=to_hex(text="nothing"), 79 | ).send() 80 | ) 81 | # userop in mempool opsSeen was advanced 82 | post_submit = get_reputation(paymaster.address) 83 | assert to_number(pre.opsSeen) == to_number(post_submit.opsSeen) - 1 84 | 85 | # make OOB state change to make UserOp in mempool to fail 86 | account.functions.setState(0xDEAD).transact({"from": w3.eth.default_account}) 87 | send_bundle_now() 88 | post = get_reputation(paymaster.address) 89 | assert post == pre 90 | 91 | 92 | # EREP-020: A staked factory is "accountable" for account breaking the rules. 93 | def test_staked_factory_on_account_failure( 94 | w3, entrypoint_contract, manual_bundling_mode 95 | ): 96 | factory = deploy_and_deposit( 97 | w3, entrypoint_contract, "TestReputationAccountFactory", staked=True 98 | ) 99 | 100 | pre = get_reputation(factory.address) 101 | for i in range(2): 102 | factory_data = factory.functions.create(i).build_transaction()["data"] 103 | account = w3.eth.call({"to": factory.address, "data": factory_data})[12:] 104 | w3.eth.send_transaction( 105 | {"from": w3.eth.default_account, "to": account, "value": 10**18} 106 | ) 107 | assert_ok( 108 | UserOperation( 109 | sender=account, 110 | verificationGasLimit=to_hex(5000000), 111 | factory=factory.address, 112 | factoryData=factory_data, 113 | signature=to_prefixed_hex("revert"), 114 | ).send() 115 | ) 116 | 117 | # cause 2nd account to revert in bundle creation 118 | factory.functions.setAccountState(0xDEAD).transact({"from": w3.eth.default_account}) 119 | send_bundle_now() 120 | assert get_reputation(factory.address).opsSeen >= 10000 121 | 122 | 123 | # EREP-030 A Staked Account is accountable for failures in other entities (`paymaster`, `aggregator`) even if they are staked. 124 | @pytest.mark.parametrize("staked_acct", ["staked", "unstaked"]) 125 | def test_account_on_entity_failure( 126 | w3, entrypoint_contract, manual_bundling_mode, staked_acct, rules_account_contract 127 | ): 128 | clear_state() 129 | # userop with staked account, and a paymaster. 130 | # after submission to mempool, we will make paymaster fail 2nd validation 131 | # (the simplest way to make the paymaster fail is withdraw its deposit) 132 | sender = rules_account_contract 133 | if staked_acct == "staked": 134 | print("staking account", sender) 135 | staked_contract(w3, entrypoint_contract, sender) 136 | else: 137 | print("unstaked account", sender) 138 | 139 | paymaster = deploy_and_deposit( 140 | w3, entrypoint_contract, "TestSimplePaymaster", False 141 | ) 142 | 143 | assert get_reputations([sender.address, paymaster.address]) == [ 144 | Reputation(address=sender.address, opsSeen=0), 145 | Reputation(address=paymaster.address, opsSeen=0), 146 | ], "pre: no reputation" 147 | 148 | assert_ok(UserOperation(sender=sender.address, paymaster=paymaster.address).send()) 149 | 150 | if staked_acct == "staked": 151 | assert get_reputations([sender.address, paymaster.address]) == [ 152 | Reputation(address=sender.address, opsSeen=1), 153 | Reputation(address=paymaster.address, opsSeen=1), 154 | ], "valid userop. both staked account and paymaster have opsSeen increment" 155 | else: 156 | assert get_reputations([sender.address, paymaster.address]) == [ 157 | Reputation(address=sender.address, opsSeen=0), 158 | Reputation(address=paymaster.address, opsSeen=1), 159 | ], "valid userop. staked paymaster should have opsSeen increment" 160 | 161 | # drain paymaster, so it would revert 162 | pm_balance = entrypoint_contract.functions.balanceOf(paymaster.address).call() 163 | 164 | tx_hash = paymaster.functions.withdrawTo(sender.address, pm_balance).transact( 165 | {"from": w3.eth.default_account} 166 | ) 167 | w3.eth.wait_for_transaction_receipt(tx_hash) 168 | 169 | bn = w3.eth.block_number 170 | send_bundle_now() 171 | assert w3.eth.block_number == bn, "bundle should revert, and not submitted" 172 | 173 | if staked_acct == "staked": 174 | assert get_reputations([sender.address, paymaster.address]) == [ 175 | Reputation(address=sender.address, opsSeen=1), 176 | Reputation(address=paymaster.address, opsSeen=0), 177 | ], "staked account should be blamed instead of paymaster" 178 | else: 179 | assert get_reputations([sender.address, paymaster.address]) == [ 180 | Reputation(address=sender.address, opsSeen=0), 181 | Reputation(address=paymaster.address, opsSeen=1), 182 | ] 183 | -------------------------------------------------------------------------------- /tests/single/gas/test_pre_verification_gas_calculation.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | import pytest 3 | 4 | from tests.conftest import wallet_contract 5 | from tests.types import RPCRequest, CommandLineArgs 6 | from tests.user_operation_erc4337 import UserOperation 7 | from tests.utils import ( 8 | assert_ok, 9 | deploy_contract, 10 | send_bundle_now, 11 | userop_hash, 12 | deploy_wallet_contract, 13 | ) 14 | 15 | pytest.skip("Slow and requires fixing", allow_module_level=True) 16 | 17 | 18 | # perform a binary search for a minimal valid numeric value for a UserOperation field 19 | def find_min_value_for_field(user_op, test_field_name, minimum_value, maximum_value): 20 | # check that maximum value is sufficient 21 | assert_ok(RPCRequest(method="debug_bundler_clearState").send()) 22 | setattr(user_op, test_field_name, hex(maximum_value)) 23 | res = user_op.send() 24 | if hasattr(res, "message") and res.message is not None: 25 | raise ValueError( 26 | f"providing '{test_field_name}' with maximum_value={maximum_value} should succeed but reverted with: {res.message}" 27 | ) 28 | 29 | # check that minimum value is insufficient 30 | assert_ok(RPCRequest(method="debug_bundler_clearState").send()) 31 | setattr(user_op, test_field_name, hex(minimum_value)) 32 | res = user_op.send() 33 | if not hasattr(res, "message") or res.message is None: 34 | raise ValueError( 35 | f"providing '{test_field_name}' with minimum_value={minimum_value} should revert but it succeeded" 36 | ) 37 | 38 | low = minimum_value 39 | high = maximum_value 40 | while low < high: 41 | mid = low + (high - low) // 2 42 | assert_ok(RPCRequest(method="debug_bundler_clearState").send()) 43 | setattr(user_op, test_field_name, hex(mid)) 44 | res = user_op.send() 45 | if not hasattr(res, "message") or res.message is None: 46 | # user operation has been accepted by the bundler 47 | high = mid 48 | else: 49 | # user operation has been rejected by the bundler 50 | low = mid + 1 51 | return low 52 | 53 | 54 | dynamic_length_field_names = ["callData", "signature", "paymasterData"] 55 | field_lengths = [0, 100, 2000] 56 | expected_bundle_sizes = [1, 2, 5, 10, 20] 57 | 58 | # note that currently there is no significant difference which field is the long one 59 | # this may change with signature aggregation but for now this parametrization is unnecessary 60 | # also note that these values are approximate 61 | expected_min_pre_verification_gas = { 62 | "callData": {0: 51224, 100: 52952, 2000: 83576}, 63 | "paymasterData": {0: 51224, 100: 52952, 2000: 83576}, 64 | "signature": {0: 51224, 100: 52952, 2000: 83576}, 65 | } 66 | 67 | 68 | @pytest.fixture(scope="session") 69 | def test_simple_paymaster(w3, entrypoint_contract): 70 | return deploy_contract( 71 | w3, 72 | "TestSimplePaymaster", 73 | [entrypoint_contract.address], 74 | value=1 * 10**18, 75 | ) 76 | 77 | 78 | @pytest.mark.usefixtures("manual_bundling_mode") 79 | @pytest.mark.parametrize("dynamic_length_field_name", dynamic_length_field_names) 80 | @pytest.mark.parametrize("field_length", field_lengths) 81 | def test_pre_verification_gas_calculation( 82 | w3, 83 | entrypoint_contract, 84 | test_simple_paymaster, 85 | wallet_contract, 86 | dynamic_length_field_name, 87 | field_length, 88 | ): 89 | RPCRequest( 90 | method="debug_bundler_setConfiguration", 91 | params=[{"expectedBundleSize": 1}], 92 | ).send() 93 | # this field can be parametrized as well 94 | # however currently ERC-4337 validates all other fields on-chain so testing them is unnecessary 95 | test_field_name = "preVerificationGas" 96 | 97 | op = UserOperation( 98 | sender=wallet_contract.address, 99 | callData="0x", 100 | signature="0x", 101 | ) 102 | 103 | match dynamic_length_field_name: 104 | case "signature": 105 | op.signature = "0x" + "ff" * field_length 106 | case "paymasterData": 107 | op.paymaster = test_simple_paymaster.address 108 | op.paymasterData = "0x" + "ff" * field_length 109 | op.paymasterPostOpGasLimit = "0xffffff" 110 | op.paymasterVerificationGasLimit = "0xffffff" 111 | case "callData": 112 | op.callData = "0x" + "ff" * field_length 113 | min_pre_verification_gas = find_min_value_for_field(op, test_field_name, 1, 200000) 114 | 115 | expected_pre_vg = expected_min_pre_verification_gas[dynamic_length_field_name][ 116 | field_length 117 | ] 118 | # pylint: disable=fixme 119 | # todo: tighten the 'approx' parameter 120 | assert min_pre_verification_gas == pytest.approx(expected_pre_vg, 0.1) 121 | 122 | 123 | # pylint: disable=fixme 124 | # todo: parametrize this test for different bundler 'expectedBundleSize' (requires new 'debug_' RPC API) 125 | @pytest.mark.usefixtures("manual_bundling_mode") 126 | @pytest.mark.parametrize( 127 | "expected_bundle_size", expected_bundle_sizes, ids=lambda val: f"bundle_size={val}" 128 | ) 129 | @pytest.mark.parametrize( 130 | "field_length", field_lengths, ids=lambda val: f"data_len={val}" 131 | ) 132 | def test_gas_cost_estimate_close_to_reality( 133 | w3, entrypoint_contract, helper_contract, expected_bundle_size, field_length 134 | ): 135 | RPCRequest( 136 | method="debug_bundler_setConfiguration", 137 | params=[{"expectedBundleSize": expected_bundle_size}], 138 | ).send() 139 | user_op_hashes = [] 140 | estimation = None 141 | for i in range(0, expected_bundle_size): 142 | # creating new wallets to avoid juggling the nonce field 143 | wallet = deploy_wallet_contract(w3) 144 | user_op = UserOperation( 145 | sender=wallet.address, 146 | callData=wallet.encode_abi(abi_element_identifier="setState", args=[0]), 147 | signature="0x" + "ff" * field_length, 148 | preVerificationGas="0xfffff", 149 | verificationGasLimit="0xfffff", 150 | callGasLimit="0xfffff", 151 | ) 152 | if estimation is None: 153 | estimation = RPCRequest( 154 | method="eth_estimateUserOperationGas", 155 | params=[asdict(user_op), CommandLineArgs.entrypoint], 156 | ).send() 157 | 158 | user_op.preVerificationGas = estimation.result["preVerificationGas"] 159 | user_op.verificationGasLimit = estimation.result["verificationGasLimit"] 160 | user_op.callGasLimit = estimation.result["callGasLimit"] 161 | response = user_op.send() 162 | op_hash = userop_hash(helper_contract, user_op) 163 | assert response.result == op_hash 164 | user_op_hashes += op_hash 165 | 166 | send_bundle_now() 167 | 168 | response = {} 169 | actual_gas_used = 0 170 | for i in range(0, expected_bundle_size): 171 | response = RPCRequest( 172 | method="eth_getUserOperationReceipt", 173 | params=[op_hash], 174 | ).send() 175 | assert response.result["success"] is True 176 | actual_gas_used += int(response.result["actualGasUsed"], 16) 177 | 178 | gas_used_handle_ops_tx = int(response.result["receipt"]["gasUsed"], 16) 179 | # pylint: disable=fixme 180 | # todo: tighten the 'approx' parameter 181 | assert gas_used_handle_ops_tx == pytest.approx(actual_gas_used, 0.1) 182 | -------------------------------------------------------------------------------- /tests/single/reputation/test_reputation.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import pytest 3 | 4 | from tests.user_operation_erc4337 import UserOperation 5 | from tests.utils import ( 6 | assert_ok, 7 | clear_mempool, 8 | deploy_and_deposit, 9 | dump_reputation, 10 | deposit_to_undeployed_sender, 11 | ) 12 | 13 | MIN_INCLUSION_RATE_DENOMINATOR = 10 14 | THROTTLING_SLACK = 10 15 | BAN_SLACK = 50 16 | 17 | THROTTLED_ENTITY_MEMPOOL_COUNT = 4 18 | 19 | 20 | @dataclass() 21 | class ReputationStatus: 22 | OK = 0 23 | THROTTLED = 1 24 | BANNED = 2 25 | 26 | 27 | def get_max_seen(ops_seen): 28 | return ops_seen / MIN_INCLUSION_RATE_DENOMINATOR 29 | 30 | 31 | def is_banned(max_seen, ops_included): 32 | return max_seen > ops_included + BAN_SLACK 33 | 34 | 35 | def is_throttled(max_seen, ops_included): 36 | return max_seen > ops_included + THROTTLING_SLACK 37 | 38 | 39 | def assert_reputation_status(address, status, ops_seen=None, ops_included=None): 40 | reputations = dump_reputation() 41 | reputation = next( 42 | ( 43 | rep 44 | for rep in reputations 45 | if rep.get("address", "").lower() == address.lower() 46 | ), 47 | None, 48 | ) 49 | assert reputation is not None, "Could not find reputation of " + address.lower() 50 | assert int(reputation.get("status", "-0x1"), 16) == status, ( 51 | "Incorrect reputation status of " + address.lower() 52 | ) 53 | assert ops_seen is None or ops_seen == int( 54 | reputation.get("opsSeen"), 16 55 | ), "opsSeen mismatch" 56 | assert ops_included is None or ops_included == int( 57 | reputation.get("opsIncluded"), 16 58 | ), "opsIncluded mismatch" 59 | 60 | 61 | @pytest.mark.skip("skipped") 62 | @pytest.mark.usefixtures("manual_bundling_mode") 63 | @pytest.mark.parametrize("case", ["with_factory", "without_factory"]) 64 | def test_staked_entity_reputation_threshold(w3, entrypoint_contract, case): 65 | if case == "with_factory": 66 | factory_contract = deploy_and_deposit( 67 | w3, entrypoint_contract, "TestRulesFactory", True 68 | ) 69 | paymaster_contract = deploy_and_deposit( 70 | w3, entrypoint_contract, "TestRulesPaymaster", True 71 | ) 72 | reputations = dump_reputation() 73 | ops_included = next( 74 | (rep for rep in reputations if rep.address == paymaster_contract.address), {} 75 | ).get("opsIncluded", 0) 76 | throttling_threshold = ( 77 | (ops_included + THROTTLING_SLACK) * MIN_INCLUSION_RATE_DENOMINATOR 78 | + MIN_INCLUSION_RATE_DENOMINATOR 79 | - 1 80 | ) 81 | banning_threshold = ( 82 | (ops_included + BAN_SLACK) * MIN_INCLUSION_RATE_DENOMINATOR 83 | + MIN_INCLUSION_RATE_DENOMINATOR 84 | - 1 85 | ) 86 | 87 | if case == "with_factory": 88 | initcodes = [ 89 | ( 90 | factory_contract.address, 91 | factory_contract.functions.create( 92 | i, "", entrypoint_contract.address 93 | ).build_transaction()["data"], 94 | ) 95 | for i in range(banning_threshold + 1) 96 | ] 97 | wallet_ops = [ 98 | UserOperation( 99 | sender=deposit_to_undeployed_sender( 100 | w3, entrypoint_contract, initcodes[i][0], initcodes[i][1] 101 | ), 102 | nonce=hex(i << 64), 103 | paymaster=paymaster_contract.address, 104 | factory=initcodes[i][0], 105 | factoryData=initcodes[i][1], 106 | ) 107 | for i in range(banning_threshold + 1) 108 | ] 109 | else: 110 | wallet = deploy_and_deposit( 111 | w3, entrypoint_contract, "TestReputationAccount", True 112 | ) 113 | # Creating enough user operations until banning threshold 114 | wallet_ops = [ 115 | UserOperation( 116 | sender=wallet.address, 117 | nonce=hex(i << 64), 118 | paymaster=paymaster_contract.address, 119 | ) 120 | for i in range(banning_threshold + 1) 121 | ] 122 | 123 | # Sending ops until the throttling threshold 124 | for i, userop in enumerate(wallet_ops[:throttling_threshold]): 125 | if i % THROTTLED_ENTITY_MEMPOOL_COUNT == 0: 126 | clear_mempool() 127 | assert_ok(userop.send()) 128 | 129 | if case == "with_factory": 130 | assert_reputation_status( 131 | factory_contract.address, 132 | ReputationStatus.OK, 133 | ops_seen=throttling_threshold, 134 | ops_included=0, 135 | ) 136 | else: 137 | assert_reputation_status( 138 | paymaster_contract.address, 139 | ReputationStatus.OK, 140 | ops_seen=throttling_threshold, 141 | ops_included=0, 142 | ) 143 | assert_reputation_status( 144 | wallet.address, 145 | ReputationStatus.OK, 146 | ops_seen=throttling_threshold, 147 | ops_included=0, 148 | ) 149 | 150 | # Going over throttling threshold 151 | wallet_ops[throttling_threshold].send() 152 | 153 | if case == "with_factory": 154 | assert_reputation_status( 155 | factory_contract.address, 156 | ReputationStatus.THROTTLED, 157 | ops_seen=throttling_threshold + 1, 158 | ops_included=0, 159 | ) 160 | else: 161 | assert_reputation_status( 162 | paymaster_contract.address, 163 | ReputationStatus.THROTTLED, 164 | ops_seen=throttling_threshold + 1, 165 | ops_included=0, 166 | ) 167 | assert_reputation_status( 168 | wallet.address, 169 | ReputationStatus.THROTTLED, 170 | ops_seen=throttling_threshold + 1, 171 | ops_included=0, 172 | ) 173 | 174 | # Sending the rest until banning threshold 175 | for i, userop in enumerate( 176 | wallet_ops[throttling_threshold + 1 : banning_threshold] 177 | ): 178 | if i % THROTTLED_ENTITY_MEMPOOL_COUNT == 0: 179 | clear_mempool() 180 | assert_ok(userop.send()) 181 | 182 | if case == "with_factory": 183 | assert_reputation_status( 184 | factory_contract.address, 185 | ReputationStatus.THROTTLED, 186 | ops_seen=banning_threshold, 187 | ops_included=0, 188 | ) 189 | else: 190 | assert_reputation_status( 191 | paymaster_contract.address, 192 | ReputationStatus.THROTTLED, 193 | ops_seen=banning_threshold, 194 | ops_included=0, 195 | ) 196 | assert_reputation_status( 197 | wallet.address, 198 | ReputationStatus.THROTTLED, 199 | ops_seen=banning_threshold, 200 | ops_included=0, 201 | ) 202 | 203 | # Going over banning threshold 204 | wallet_ops[banning_threshold].send() 205 | 206 | if case == "with_factory": 207 | assert_reputation_status( 208 | factory_contract.address, 209 | ReputationStatus.BANNED, 210 | ops_seen=banning_threshold + 1, 211 | ops_included=0, 212 | ) 213 | else: 214 | assert_reputation_status( 215 | paymaster_contract.address, 216 | ReputationStatus.BANNED, 217 | ops_seen=banning_threshold + 1, 218 | ops_included=0, 219 | ) 220 | assert_reputation_status( 221 | wallet.address, 222 | ReputationStatus.BANNED, 223 | ops_seen=banning_threshold + 1, 224 | ops_included=0, 225 | ) 226 | 227 | # tx_hash = wallet.functions.setState(0xdead).transact({"from": w3.eth.accounts[0]}) 228 | # w3.eth.wait_for_transaction_receipt(tx_hash) 229 | # tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) 230 | # assert tx_receipt.status == 1, "Test error: could not call TestReputationAccount.setState() directly" 231 | -------------------------------------------------------------------------------- /tests/single/eip7702/test_eip7702_tuple_userop.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from tests.transaction_eip_7702 import TupleEIP7702 3 | from tests.types import CommandLineArgs, RPCErrorCode 4 | from tests.utils import ( 5 | UserOperation, 6 | assert_ok, 7 | assert_rpc_error, 8 | userop_hash, 9 | send_bundle_now, 10 | fund, 11 | to_hex, 12 | ) 13 | 14 | AUTHORIZED_ACCOUNT_PREFIX = "ef0100" 15 | 16 | 17 | def test_send_eip_7702_tx(w3, userop, impl7702, wallet_contract, helper_contract): 18 | acc = w3.eth.account.create() 19 | fund(w3, acc.address) 20 | 21 | # create an EIP-7702 authorization tuple 22 | auth_tuple = TupleEIP7702( 23 | chainId=hex(1337), 24 | address=impl7702.address, 25 | nonce="0x0", 26 | signer_private_key=acc._private_key.hex(), 27 | ) 28 | 29 | userop.sender = acc.address 30 | userop.eip7702Auth = auth_tuple 31 | 32 | sender_code = w3.eth.get_code(acc.address) 33 | assert len(sender_code) == 0 34 | 35 | response = userop.send() 36 | assert_ok(response) 37 | send_bundle_now() 38 | 39 | assert response.result == userop_hash(helper_contract, userop) 40 | 41 | sender_code = w3.eth.get_code(acc.address) 42 | 43 | # delegated EOA code is always 23 bytes long 44 | assert len(sender_code) == 23 45 | expected_code = "".join([AUTHORIZED_ACCOUNT_PREFIX, impl7702.address[2:].lower()]) 46 | assert sender_code.hex() == expected_code 47 | 48 | eoa_with_authorization = w3.eth.contract( 49 | abi=wallet_contract.abi, 50 | address=acc.address, 51 | ) 52 | 53 | # delegated EOA account can actually have a state 54 | state_after = eoa_with_authorization.functions.state().call() 55 | assert state_after == 1111111 56 | 57 | 58 | def test_send_eip_7702_tx_with_initcode( 59 | w3, userop, impl7702, wallet_contract, helper_contract 60 | ): 61 | acc = w3.eth.account.create() 62 | fund(w3, acc.address) 63 | 64 | # create an EIP-7702 authorization tuple 65 | auth_tuple = TupleEIP7702( 66 | chainId=hex(1337), 67 | address=impl7702.address, 68 | nonce="0x0", 69 | signer_private_key=acc._private_key.hex(), 70 | ) 71 | 72 | userop.sender = acc.address 73 | userop.eip7702Auth = auth_tuple 74 | userop.factory = "0x7702" 75 | # making execution frame revert to make sure 'factoryData' was applied as initCode during validation 76 | userop.callData = impl7702.encode_abi(abi_element_identifier="fail") 77 | userop.factoryData = impl7702.encode_abi( 78 | abi_element_identifier="setState", args=[7702] 79 | ) 80 | 81 | sender_code = w3.eth.get_code(acc.address) 82 | assert len(sender_code) == 0 83 | 84 | response = userop.send() 85 | assert_ok(response) 86 | send_bundle_now() 87 | 88 | sender_code = w3.eth.get_code(acc.address) 89 | 90 | # delegated EOA code is always 23 bytes long 91 | assert len(sender_code) == 23 92 | 93 | eoa_with_authorization = w3.eth.contract( 94 | abi=wallet_contract.abi, 95 | address=acc.address, 96 | ) 97 | 98 | # delegated EOA account can actually have a state 99 | state_after = eoa_with_authorization.functions.state().call() 100 | assert state_after == 7702 101 | 102 | 103 | def test_send_eip_7702_tx_with_flag_no_initcode( 104 | w3, userop, impl7702, wallet_contract, helper_contract 105 | ): 106 | acc = w3.eth.account.create() 107 | fund(w3, acc.address) 108 | 109 | # create an EIP-7702 authorization tuple 110 | auth_tuple = TupleEIP7702( 111 | chainId=hex(1337), 112 | address=impl7702.address, 113 | nonce="0x0", 114 | signer_private_key=acc._private_key.hex(), 115 | ) 116 | 117 | userop.sender = acc.address 118 | userop.eip7702Auth = auth_tuple 119 | userop.factory = "0x7702" 120 | # making execution frame revert to make sure 'factoryData' was applied as initCode during validation 121 | userop.callData = impl7702.encode_abi(abi_element_identifier="fail") 122 | 123 | sender_code = w3.eth.get_code(acc.address) 124 | assert len(sender_code) == 0 125 | 126 | response = userop.send() 127 | assert_ok(response) 128 | send_bundle_now() 129 | 130 | sender_code = w3.eth.get_code(acc.address) 131 | 132 | # delegated EOA code is always 23 bytes long 133 | assert len(sender_code) == 23 134 | 135 | 136 | # normal transaction, using the same sender 137 | @pytest.mark.parametrize("chainid", [0, 1337]) 138 | def test_send_post_eip_7702_tx( 139 | w3, userop, impl7702, wallet_contract, helper_contract, entrypoint_contract, chainid 140 | ): 141 | # first deploy a EIP-7702 address 142 | acc = w3.eth.account.create() 143 | w3.eth.send_transaction( 144 | {"from": w3.eth.accounts[0], "to": acc.address, "value": 10**18} 145 | ) 146 | nonce = w3.eth.get_transaction_count(acc.address) 147 | auth_tuple = TupleEIP7702( 148 | chainId=hex(chainid), 149 | address=impl7702.address, 150 | nonce=hex(nonce), 151 | signer_private_key=acc._private_key.hex(), 152 | ) 153 | userop.sender = acc.address 154 | userop.eip7702Auth = auth_tuple 155 | response = userop.send() 156 | assert_ok(response) 157 | send_bundle_now() 158 | 159 | # use this address in a different UserOp 160 | account = w3.eth.contract( 161 | abi=wallet_contract.abi, 162 | address=acc.address, 163 | ) 164 | 165 | state_before = account.functions.state().call() 166 | 167 | # non-7702 userop, that uses previously created account. 168 | userop = UserOperation( 169 | sender=acc.address, 170 | nonce=hex(entrypoint_contract.functions.getNonce(acc.address, 0).call()), 171 | callData=wallet_contract.encode_abi( 172 | abi_element_identifier="setState", args=[state_before + 1] 173 | ), 174 | signature="0xface", 175 | ) 176 | 177 | response = userop.send() 178 | send_bundle_now() 179 | 180 | assert response.result == userop_hash(helper_contract, userop) 181 | # delegated EOA account can actually have a state 182 | state_after = account.functions.state().call() 183 | assert state_after == state_before + 1 184 | 185 | 186 | def test_send_bad_eip_7702_drop_userop(w3, impl7702, userop): 187 | acc = w3.eth.account.create() 188 | # fund the EOA address 189 | w3.eth.send_transaction( 190 | {"from": w3.eth.accounts[0], "to": acc.address, "value": 10**18} 191 | ) 192 | 193 | assert len(w3.eth.get_code(acc.address)) == 0 194 | 195 | # create an EIP-7702 authorization tuple, with wrong nonce 196 | nonce = w3.eth.get_transaction_count(acc.address) 197 | auth_tuple = TupleEIP7702( 198 | chainId=hex(1337), 199 | address=impl7702.address, 200 | nonce=hex(nonce + 2), 201 | signer_private_key=acc._private_key.hex(), 202 | ) 203 | 204 | userop.sender = acc.address 205 | userop.eip7702Auth = auth_tuple 206 | 207 | response = userop.send() 208 | assert_rpc_error(response, "", RPCErrorCode.REJECTED_BY_EP_OR_ACCOUNT) 209 | 210 | 211 | def test_send_nonsender_eip_7702_drop_userop(w3, impl7702, userop): 212 | another_account = w3.eth.account.create() 213 | 214 | # create an EIP-7702 authorization tuple, with different signer 215 | auth_tuple = TupleEIP7702( 216 | signer_private_key=another_account._private_key.hex(), 217 | chainId=hex(1337), 218 | address=impl7702.address, 219 | nonce="0x0", 220 | ) 221 | userop.eip7702Auth = auth_tuple 222 | 223 | assert_rpc_error(userop.send(), "sender", RPCErrorCode.INVALID_FIELDS) 224 | 225 | 226 | def test_send_wrongchain_eip_7702_drop_userop( 227 | w3, entrypoint_contract, impl7702, userop 228 | ): 229 | # first, create an account: 230 | acc = w3.eth.account.create() 231 | fund(w3, acc.address) 232 | 233 | # create an EIP-7702 authorization tuple 234 | auth_tuple = TupleEIP7702( 235 | chainId=hex(1337), 236 | address=impl7702.address, 237 | nonce="0x0", 238 | signer_private_key=acc._private_key.hex(), 239 | ) 240 | 241 | userop.sender = acc.address 242 | userop.eip7702Auth = auth_tuple 243 | 244 | assert_ok(userop.send()) 245 | send_bundle_now() 246 | assert len(w3.eth.get_code(acc.address)) == 23 247 | 248 | # submit a UserOp with wrong chainId: 249 | sender_nonce = entrypoint_contract.functions.getNonce(acc.address, 0).call() 250 | userop.nonce = to_hex(sender_nonce) 251 | 252 | auth_nonce = w3.eth.get_transaction_count(acc.address) 253 | # create an EIP-7702 authorization tuple, with wrong chain 254 | userop.eip7702Auth = TupleEIP7702( 255 | chainId=hex(1234), 256 | address=impl7702.address, 257 | nonce=hex(auth_nonce), 258 | signer_private_key=acc._private_key.hex(), 259 | ) 260 | 261 | assert_rpc_error(userop.send(), "chainid", RPCErrorCode.INVALID_FIELDS) 262 | -------------------------------------------------------------------------------- /tests/rip7560/test_send_failed.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import pytest 3 | from web3 import Web3 4 | 5 | from web3.constants import ADDRESS_ZERO 6 | from tests.rip7560.types import TransactionRIP7560 7 | from tests.utils import ( 8 | assert_rpc_error, 9 | fund, 10 | get_rip7560_debug_info, 11 | send_bundle_now, 12 | to_prefixed_hex, 13 | deploy_contract, 14 | assert_ok, 15 | dump_mempool, 16 | ) 17 | from tests.types import RPCErrorCode 18 | 19 | 20 | def test_eth_send_no_gas(w3): 21 | contract = deploy_contract( 22 | w3, 23 | "rip7560/TestAccount", 24 | value=0, 25 | ) 26 | 27 | tx = TransactionRIP7560(sender=contract.address, nonce=hex(1)) 28 | 29 | ret = tx.send() 30 | assert_rpc_error(ret, "insufficient funds", RPCErrorCode.INVALID_INPUT) 31 | 32 | 33 | def test_eth_send_no_code(w3): 34 | tx = TransactionRIP7560( 35 | sender="0x1111111111111111111111111111111111111113", 36 | ) 37 | fund(w3, tx.sender) 38 | 39 | ret = tx.send() 40 | assert_rpc_error( 41 | ret, 42 | "account is not deployed and no deployer is specified", 43 | RPCErrorCode.INVALID_INPUT, 44 | ) 45 | 46 | 47 | def test_eth_send_wrong_nonce(tx_7560): 48 | tx_7560.nonce = hex(5) 49 | ret = tx_7560.send() 50 | assert_rpc_error(ret, "nonce too high", RPCErrorCode.INVALID_INPUT) 51 | 52 | tx_7560.nonce = hex(0) 53 | ret = tx_7560.send() 54 | assert_rpc_error(ret, "nonce too low", RPCErrorCode.INVALID_INPUT) 55 | 56 | 57 | RevertTestCase = collections.namedtuple( 58 | "RevertTestCase", 59 | ["rule", "entity", "expected_message", "is_expected_data"], 60 | ) 61 | 62 | cases = [ 63 | # account standard solidity error revert 64 | RevertTestCase( 65 | "revert-msg", 66 | "account", 67 | "validation phase failed in contract account with exception: " 68 | "execution reverted: on-chain revert message string", 69 | False, 70 | ), 71 | # account custom solidity error revert 72 | RevertTestCase( 73 | "revert-custom-msg", 74 | "account", 75 | "validation phase failed in contract account with exception: execution reverted", 76 | True, 77 | ), 78 | # account out of gas error revert 79 | RevertTestCase( 80 | "revert-out-of-gas-msg", 81 | "account", 82 | "validation phase failed in contract account with exception: out of gas", 83 | False, 84 | ), 85 | # paymaster standard solidity error revert 86 | RevertTestCase( 87 | "revert-msg", 88 | "paymaster", 89 | "validation phase failed in contract paymaster with exception: " 90 | "execution reverted: on-chain revert message string", 91 | False, 92 | ), 93 | # paymaster custom solidity error revert 94 | RevertTestCase( 95 | "revert-custom-msg", 96 | "paymaster", 97 | "validation phase failed in contract paymaster with exception: execution reverted", 98 | True, 99 | ), 100 | # paymaster out of gas error revert 101 | RevertTestCase( 102 | "revert-out-of-gas-msg", 103 | "paymaster", 104 | "validation phase failed in contract paymaster with exception: out of gas", 105 | False, 106 | ), 107 | ] 108 | 109 | 110 | def case_id_function(case): 111 | return f"[{case.entity}][{case.rule}]" 112 | 113 | 114 | @pytest.mark.parametrize("case", cases, ids=case_id_function) 115 | def test_eth_send_account_validation_reverts1( 116 | w3, 117 | wallet_contract_rules, 118 | tx_7560: TransactionRIP7560, 119 | paymaster_contract_7560, 120 | case: RevertTestCase, 121 | ): 122 | if case.entity == "account": 123 | tx_7560.nonce = hex(2) 124 | tx_7560.sender = wallet_contract_rules.address 125 | tx_7560.authorizationData = to_prefixed_hex(case.rule) 126 | if case.entity == "paymaster": 127 | tx_7560.paymaster = paymaster_contract_7560.address 128 | tx_7560.paymasterData = to_prefixed_hex(case.rule) 129 | 130 | response = tx_7560.send() 131 | 132 | expected_data = None 133 | if case.is_expected_data: 134 | expected_data = encode_custom_error(w3) 135 | assert_rpc_error(response, case.expected_message, -32000, expected_data) 136 | 137 | 138 | @pytest.mark.parametrize("case", cases, ids=case_id_function) 139 | def test_eth_send_account_validation_reverts_skip_validation_bundler( 140 | w3, 141 | wallet_contract_rules, 142 | tx_7560: TransactionRIP7560, 143 | paymaster_contract_7560, 144 | case: RevertTestCase, 145 | ): 146 | if case.entity == "account": 147 | tx_7560.nonce = hex(2) 148 | tx_7560.sender = wallet_contract_rules.address 149 | tx_7560.authorizationData = to_prefixed_hex(case.rule) 150 | if case.entity == "paymaster": 151 | tx_7560.nonce = hex(2) 152 | tx_7560.sender = wallet_contract_rules.address 153 | tx_7560.authorizationData = to_prefixed_hex("") 154 | tx_7560.paymaster = paymaster_contract_7560.address 155 | tx_7560.paymasterData = to_prefixed_hex(case.rule) 156 | 157 | state_before = wallet_contract_rules.functions.state().call() 158 | assert state_before == 0 159 | 160 | nonce_before = w3.eth.get_transaction_count(tx_7560.sender) 161 | assert nonce_before == 2 162 | 163 | response = tx_7560.send_skip_validation() 164 | send_bundle_now() 165 | 166 | state_after = wallet_contract_rules.functions.state().call() 167 | assert state_after == 0 168 | 169 | nonce_after = w3.eth.get_transaction_count(tx_7560.sender) 170 | assert nonce_after == 2 171 | 172 | debug_info = get_rip7560_debug_info(response.result) 173 | 174 | assert debug_info.result["frameReverted"] is True 175 | assert debug_info.result["revertEntityName"] == case.entity 176 | 177 | 178 | def encode_solidity_error(w3, value): 179 | # manually encoding the custom error message with "encode_abi" here 180 | c = w3.eth.contract( 181 | abi='[{"type":"function","name":"Error",' 182 | '"inputs":[{"name": "error","type": "string"}]}]' 183 | ) 184 | abi_encoding = c.encode_abi(abi_element_identifier="Error", args=[value]) 185 | return abi_encoding 186 | 187 | 188 | def encode_custom_error(w3): 189 | # manually encoding the custom error message with "encode_abi" here 190 | c = w3.eth.contract( 191 | abi='[{"type":"function","name":"CustomError",' 192 | '"inputs":[{"name": "error","type": "string"},{"name": "code","type": "uint256"}]}]' 193 | ) 194 | abi_encoding = c.encode_abi( 195 | abi_element_identifier="CustomError", args=["on-chain custom error", 777] 196 | ) 197 | return abi_encoding 198 | 199 | 200 | def test_eth_send_account_validation_calls_invalid_callback( 201 | wallet_contract_rules, tx_7560 202 | ): 203 | tx_7560.sender = wallet_contract_rules.address 204 | tx_7560.nonce = hex(2) 205 | tx_7560.authorizationData = to_prefixed_hex("wrong-callback-method") 206 | 207 | response = tx_7560.send() 208 | assert_rpc_error( 209 | response, 210 | "validation phase failed with exception: unable to decode acceptAccount: no method with id: 0xd3ae1743", 211 | -32000, 212 | ) 213 | 214 | 215 | def test_eth_send_paymaster_validation_calls_invalid_callback( 216 | paymaster_contract_7560, tx_7560 217 | ): 218 | tx_7560.paymaster = paymaster_contract_7560.address 219 | tx_7560.paymasterData = to_prefixed_hex("wrong-callback-method") 220 | 221 | response = tx_7560.send() 222 | assert_rpc_error( 223 | response, 224 | "validation phase failed with exception: unable to decode acceptPaymaster: no method with id: 0x9a0e28f8", 225 | -32000, 226 | ) 227 | 228 | 229 | def test_eth_send_deployment_reverts(w3, factory_contract_7560, tx_7560): 230 | new_sender_address = factory_contract_7560.functions.getCreate2Address( 231 | ADDRESS_ZERO, 123, "revert-msg" 232 | ).call() 233 | tx_7560.sender = new_sender_address 234 | fund(w3, new_sender_address) 235 | tx_7560.nonce = hex(0) 236 | tx_7560.factory = factory_contract_7560.address 237 | tx_7560.factoryData = factory_contract_7560.functions.createAccount( 238 | ADDRESS_ZERO, 123, "revert-msg" 239 | ).build_transaction({"gas": 1000000})["data"] 240 | response = tx_7560.send() 241 | assert_rpc_error( 242 | response, 243 | "validation phase failed in contract deployer with exception: " 244 | "execution reverted: on-chain revert message string", 245 | -32000, 246 | ) 247 | 248 | 249 | def test_eth_send_deployment_does_not_create_account( 250 | w3, factory_contract_7560, tx_7560 251 | ): 252 | new_sender_address = factory_contract_7560.functions.getCreate2Address( 253 | ADDRESS_ZERO, 123, "skip-deploy-msg" 254 | ).call() 255 | tx_7560.sender = new_sender_address 256 | fund(w3, new_sender_address) 257 | tx_7560.nonce = hex(0) 258 | tx_7560.factory = factory_contract_7560.address 259 | tx_7560.factoryData = factory_contract_7560.functions.createAccount( 260 | ADDRESS_ZERO, 123, "skip-deploy-msg" 261 | ).build_transaction({"gas": 1000000})["data"] 262 | response = tx_7560.send() 263 | assert_rpc_error( 264 | response, 265 | "validation phase failed with exception: sender not deployed by the deployer", 266 | -32000, 267 | ) 268 | 269 | 270 | def test_insufficient_pre_transaction_gas(tx_7560): 271 | tx_7560.verificationGasLimit = hex(30000) 272 | tx_7560.authorizationData = "0x" + ("ff" * 1000) 273 | tx_7560.executionData = "0x" 274 | response = tx_7560.send() 275 | assert_rpc_error( 276 | response, 277 | "insufficient ValidationGasLimit(30000) to cover PreTransactionGasCost(31000)", 278 | -32000, 279 | ) 280 | 281 | 282 | # pylint: disable=duplicate-code 283 | def test_overflow_block_gas_limit(w3: Web3, tx_7560: TransactionRIP7560): 284 | count = 3 285 | wallets = [] 286 | hashes = [] 287 | for i in range(count): 288 | wallets.append( 289 | deploy_contract(w3, "rip7560/gaswaste/GasWasteAccount", value=20**18) 290 | ) 291 | 292 | for i in range(count): 293 | wallet = wallets[i] 294 | new_op = TransactionRIP7560( 295 | sender=wallet.address, 296 | nonce="0x1", 297 | executionData=wallet.encode_abi("anyExecutionFunction"), 298 | callGasLimit=hex(10_000_000), 299 | maxPriorityFeePerGas=tx_7560.maxPriorityFeePerGas, 300 | maxFeePerGas=tx_7560.maxFeePerGas, 301 | ) 302 | # GasWasteAccount uses 'GAS' opcode to leave 100 gas 303 | res = new_op.send_skip_validation() 304 | hashes.append(res.result) 305 | assert_ok(res) 306 | 307 | mempool = dump_mempool() 308 | print(mempool) 309 | assert len(mempool) == count 310 | send_bundle_now() 311 | block = w3.eth.get_block("latest") 312 | tx_len = len(block.transactions) 313 | # note: two 7560 transactions and a zero-value legacy transaction from 'send_bundle_now' 314 | assert tx_len == count 315 | 316 | debug_info = get_rip7560_debug_info(hashes[2]) 317 | 318 | assert debug_info.result["revertEntityName"] == "block gas limit" 319 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | my-executor: 5 | docker: 6 | - image: shahafn/go-python-node 7 | commands: 8 | update-and-build-deps-cached: 9 | steps: 10 | - run: 11 | name: "Update submodules" 12 | command: pdm run submodule-update 13 | - restore_cache: 14 | keys: 15 | - spec-node-modules-{{ checksum "spec/yarn.lock" }} 16 | - spec-node-modules- 17 | - restore_cache: 18 | keys: 19 | - rip7560-node-modules-{{ checksum "@rip7560/yarn.lock" }} 20 | - rip7560-node-modules- 21 | - run: 22 | name: "build submodules" 23 | command: pdm run dep-build 24 | - save_cache: 25 | key: spec-node-modules-{{ checksum "spec/yarn.lock" }} 26 | paths: 27 | - "spec/node_modules" 28 | - save_cache: 29 | key: rip7560-node-modules-{{ checksum "@rip7560/yarn.lock" }} 30 | paths: 31 | - "@rip7560/node_modules" 32 | - save_cache: 33 | key: account-abstraction-git-{{ checksum ".git/modules/account-abstraction/HEAD" }} 34 | paths: 35 | - "@account-abstraction" 36 | - save_cache: 37 | key: spec-git-{{ checksum ".git/modules/spec/HEAD" }} 38 | paths: 39 | - "spec" 40 | - save_cache: 41 | key: rip7560-git-{{ checksum ".git/modules/@rip7560/HEAD" }} 42 | paths: 43 | - "@rip7560" 44 | jobs: 45 | test-erc4337-bundler: 46 | executor: my-executor 47 | steps: 48 | - checkout 49 | - run: 50 | name: "Install PDM" 51 | command: "curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -" 52 | - restore_cache: 53 | keys: 54 | - dependency-cache-pdm-{{ checksum "pdm.lock" }} 55 | - run: 56 | name: "Install dependencies" 57 | command: pdm install 58 | - save_cache: 59 | key: dependency-cache-pdm-{{ checksum "pdm.lock" }} 60 | paths: 61 | - .venv 62 | - run: 63 | name: "Run lint" 64 | command: pdm run lint 65 | - run: 66 | name: "Run format check" 67 | command: pdm run format --check 68 | - update-and-build-deps-cached 69 | - run: 70 | name: "Clone go-ethereum" 71 | # NOTE: using plain geth + native tracer (no need for eip7560) 72 | # temp: avoid current-branch checkout, and force geth-with-tracer 73 | command: CIRCLE_BRANCH=master ./scripts/clone-helper geth-with-erc7562-tracer https://github.com/eth-infinitism/go-ethereum.git --no-submodules 74 | - restore_cache: 75 | keys: 76 | - go-ethereum-build-{{ checksum "go-ethereum/commit-hash.txt" }} 77 | - run: 78 | name: "Build go-ethereum" 79 | working_directory: go-ethereum 80 | command: | 81 | if [ -f build/bin/geth ]; then 82 | echo "geth binary exists, skipping build" 83 | else 84 | echo "geth binary not found, building..." 85 | make geth 86 | fi 87 | - save_cache: 88 | key: go-ethereum-build-{{ checksum "go-ethereum/commit-hash.txt" }} 89 | paths: 90 | - go-ethereum/build/bin 91 | - run: 92 | name: "Clone bundler" 93 | command: ./scripts/clone-helper master https://github.com/eth-infinitism/bundler.git 94 | - restore_cache: 95 | keys: 96 | - bundler-deps-{{ checksum "bundler/yarn.lock" }} 97 | - bundler-deps- 98 | - restore_cache: 99 | keys: 100 | - bundler-aa-submodule-{{ checksum "bundler/submodules/account-abstraction/yarn.lock" }} 101 | - bundler-aa-submodule- 102 | - restore_cache: 103 | keys: 104 | - bundler-rip7560-submodule-{{ checksum "bundler/submodules/rip7560/yarn.lock" }} 105 | - bundler-rip7560-submodule- 106 | - restore_cache: 107 | keys: 108 | - bundler-git-{{ checksum "bundler/commit-hash.txt" }} 109 | - run: 110 | name: "Install bundler dependencies" 111 | working_directory: bundler 112 | command: | 113 | if [ -d packages/bundler/dist ]; then 114 | echo "bundler cache exists, skipping build" 115 | else 116 | echo "bundler cache not found, building..." 117 | yarn install --frozen-lockfile --ignore-engines && yarn preprocess 118 | fi 119 | - save_cache: 120 | key: bundler-deps-{{ checksum "bundler/yarn.lock" }} 121 | paths: 122 | - bundler/node_modules 123 | - save_cache: 124 | key: bundler-aa-submodule-{{ checksum "bundler/submodules/account-abstraction/yarn.lock" }} 125 | paths: 126 | - bundler/submodules/account-abstraction/node_modules 127 | - save_cache: 128 | key: bundler-rip7560-submodule-{{ checksum "bundler/submodules/rip7560/yarn.lock" }} 129 | paths: 130 | - bundler/submodules/rip7560/node_modules 131 | - save_cache: 132 | key: bundler-git-{{ checksum "bundler/commit-hash.txt" }} 133 | paths: 134 | - bundler 135 | - run: 136 | name: "Run go-ethereum" 137 | working_directory: "./go-ethereum" 138 | command: "\ 139 | ./build/bin/geth version ; \ 140 | ./build/bin/geth \ 141 | --dev \ 142 | --dev.gaslimit \ 143 | 30000000 \ 144 | --http \ 145 | --http.api \ 146 | 'eth,net,web3,personal,debug' \ 147 | --http.port \ 148 | 8545 \ 149 | --rpc.allow-unprotected-txs \ 150 | " 151 | 152 | background: true 153 | - run: 154 | name: "Run bundler (ERC4337)" 155 | working_directory: "./bundler" 156 | command: "yarn bundler" 157 | background: true 158 | - run: 159 | name: "Await bundler (ERC4337)" 160 | working_directory: "./bundler" 161 | shell: /bin/sh 162 | command: | 163 | wget --post-data='{"method": "eth_supportedEntryPoints"}' \ 164 | --retry-connrefused --waitretry=2 --timeout=60 --tries=30 \ 165 | http://localhost:3000/rpc 166 | - run: 167 | name: "Run pytest" 168 | command: pdm run test --query-ep 169 | test-rip7560-bundler: 170 | executor: my-executor 171 | steps: 172 | - checkout 173 | - run: 174 | name: "Install PDM" 175 | command: "curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3 -" 176 | - restore_cache: 177 | keys: 178 | - dependency-cache-pdm-{{ checksum "pdm.lock" }} 179 | - run: 180 | name: "Install dependencies" 181 | command: pdm install 182 | - save_cache: 183 | key: dependency-cache-pdm-{{ checksum "pdm.lock" }} 184 | paths: 185 | - .venv 186 | - run: 187 | name: "Run lint" 188 | command: pdm run lint 189 | - run: 190 | name: "Run format check" 191 | command: pdm run format --check 192 | - update-and-build-deps-cached 193 | - run: 194 | name: "Clone go-ethereum" 195 | command: ./scripts/clone-helper RIP-7560-revision-3 https://github.com/eth-infinitism/go-ethereum.git --no-submodules 196 | - restore_cache: 197 | keys: 198 | - go-ethereum-build-{{ checksum "go-ethereum/commit-hash.txt" }} 199 | - run: 200 | name: "Build go-ethereum" 201 | working_directory: go-ethereum 202 | command: | 203 | if [ -f build/bin/geth ]; then 204 | echo "geth binary exists, skipping build" 205 | else 206 | echo "geth binary not found, building..." 207 | make geth 208 | fi 209 | - save_cache: 210 | key: go-ethereum-build-{{ checksum "go-ethereum/commit-hash.txt" }} 211 | paths: 212 | - go-ethereum/build/bin 213 | - go-ethereum/circleciconfig.toml 214 | - run: 215 | name: "Clone bundler" 216 | command: ./scripts/clone-helper master https://github.com/eth-infinitism/bundler.git 217 | - restore_cache: 218 | keys: 219 | - bundler-deps-{{ checksum "bundler/yarn.lock" }} 220 | - bundler-deps- 221 | - restore_cache: 222 | keys: 223 | - bundler-aa-submodule-{{ checksum "bundler/submodules/account-abstraction/yarn.lock" }} 224 | - bundler-aa-submodule- 225 | - restore_cache: 226 | keys: 227 | - bundler-rip7560-submodule-{{ checksum "bundler/submodules/rip7560/yarn.lock" }} 228 | - bundler-rip7560-submodule- 229 | - restore_cache: 230 | keys: 231 | - bundler-git-{{ checksum "bundler/commit-hash.txt" }} 232 | - run: 233 | name: "Install bundler dependencies" 234 | working_directory: bundler 235 | command: | 236 | if [ -d packages/bundler/dist ]; then 237 | echo "bundler cache exists, skipping build" 238 | else 239 | echo "bundler cache not found, building..." 240 | yarn install --frozen-lockfile --ignore-engines && yarn preprocess 241 | fi 242 | - save_cache: 243 | key: bundler-deps-{{ checksum "bundler/yarn.lock" }} 244 | paths: 245 | - bundler/node_modules 246 | - save_cache: 247 | key: bundler-aa-submodule-{{ checksum "bundler/submodules/account-abstraction/yarn.lock" }} 248 | paths: 249 | - bundler/submodules/account-abstraction/node_modules 250 | - save_cache: 251 | key: bundler-rip7560-submodule-{{ checksum "bundler/submodules/rip7560/yarn.lock" }} 252 | paths: 253 | - bundler/submodules/rip7560/node_modules 254 | - save_cache: 255 | key: bundler-git-{{ checksum "bundler/commit-hash.txt" }} 256 | paths: 257 | - bundler 258 | - run: 259 | name: "Run go-ethereum" 260 | working_directory: "./go-ethereum" 261 | command: "\ 262 | ./build/bin/geth version ; \ 263 | ./build/bin/geth \ 264 | --dev \ 265 | --dev.gaslimit \ 266 | 30000000 \ 267 | --http \ 268 | --http.api \ 269 | 'eth,net,web3,personal,debug' \ 270 | --http.port \ 271 | 8545 \ 272 | --rpc.allow-unprotected-txs \ 273 | --config \ 274 | circleciconfig.toml \ 275 | " 276 | background: true 277 | - run: 278 | name: "Run bundler (RIP7560)" 279 | working_directory: bundler 280 | command: yarn bundler-rip7560 281 | background: true 282 | - run: 283 | name: "Await bundler (RIP7560)" 284 | working_directory: "./bundler" 285 | shell: /bin/sh 286 | command: | 287 | wget --post-data='{"method": "eth_supportedEntryPoints"}' \ 288 | --retry-connrefused --waitretry=2 --timeout=60 --tries=30 \ 289 | http://localhost:3000/rpc 290 | - run: 291 | name: "Run pytest (RIP7560)" 292 | command: "pdm run test-rip7560" 293 | 294 | workflows: 295 | version: 2 296 | test-bundler-erc4337-workflow: 297 | jobs: 298 | - test-erc4337-bundler 299 | test-bundler-rip7560-workflow: 300 | jobs: 301 | - test-rip7560-bundler 302 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from functools import cache 4 | 5 | from eth_abi import decode 6 | from eth_utils import to_checksum_address 7 | from solcx import compile_source 8 | 9 | from .rip7560.types import TransactionRIP7560 10 | from .types import RPCRequest, CommandLineArgs 11 | from .user_operation_erc4337 import UserOperation 12 | 13 | 14 | @cache 15 | def compile_contract(contract): 16 | contract_subdir = os.path.dirname(contract) 17 | contract_name = os.path.basename(contract) 18 | 19 | current_dirname = os.path.dirname(__file__) 20 | contracts_dirname = current_dirname + "/contracts/" + contract_subdir + "/" 21 | aa_path = os.path.realpath(current_dirname + "/../@account-abstraction") 22 | aa_relpath = os.path.relpath(aa_path, contracts_dirname) 23 | rip7560_path = os.path.realpath(current_dirname + "/../@rip7560") 24 | rip7560_relpath = os.path.relpath(rip7560_path, contracts_dirname) 25 | allow_paths = aa_relpath + "," + rip7560_relpath 26 | aa_remap = "@account-abstraction=" + aa_relpath 27 | rip7560_remap = "@rip7560=" + rip7560_relpath 28 | if "@account-abstraction" in contract: 29 | contracts_dirname = current_dirname + "/../" + contract_subdir + "/" 30 | 31 | with open( 32 | contracts_dirname + contract_name + ".sol", "r", encoding="utf-8" 33 | ) as contractfile: 34 | test_source = contractfile.read() 35 | compiled_sol = compile_source( 36 | test_source, 37 | base_path=contracts_dirname, 38 | # pylint: disable=fixme 39 | # todo: only do it for 7560 folder 40 | include_path=os.path.abspath(os.path.join(contracts_dirname, os.pardir)) 41 | + "/", 42 | allow_paths=allow_paths, 43 | import_remappings=[aa_remap, rip7560_remap], 44 | output_values=["abi", "bin", "bin-runtime"], 45 | solc_version="0.8.28", 46 | evm_version="cancun", 47 | optimize=True, 48 | optimize_runs=1, 49 | via_ir=False, 50 | ) 51 | return compiled_sol[":" + contract_name] 52 | 53 | 54 | # pylint: disable=too-many-arguments 55 | def deploy_contract( 56 | w3, 57 | contractname, 58 | ctrparams=None, 59 | value=0, 60 | gas=10 * 10**6, 61 | gas_price=10**9, 62 | account=None, 63 | ): 64 | if ctrparams is None: 65 | ctrparams = [] 66 | interface = compile_contract(contractname) 67 | contract = w3.eth.contract( 68 | abi=interface["abi"], 69 | bytecode=interface["bin"], 70 | ) 71 | codesize = len(interface["bin-runtime"]) / 2 72 | if codesize >= 24576: 73 | raise RuntimeError( 74 | f"ERROR! Contract exceed size limit! {contractname} is {codesize} bytes" 75 | ) 76 | if account is None: 77 | account = w3.eth.default_account 78 | tx_hash = contract.constructor(*ctrparams).transact( 79 | { 80 | "gas": gas, 81 | "from": account, 82 | "value": hex(value), 83 | "maxFeePerGas": gas_price, 84 | "maxPriorityFeePerGas": gas_price, 85 | } 86 | ) 87 | tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash) 88 | # print('Deployed contract. hash, receipt:', tx_hash.hex(), tx_receipt) 89 | # print(tx_receipt.contractAddress) 90 | assert tx_receipt.status == 1, ( 91 | "deployment of " + contractname + " failed:" + str(tx_receipt) 92 | ) 93 | return w3.eth.contract(abi=interface["abi"], address=tx_receipt.contractAddress) 94 | 95 | 96 | def deploy_and_deposit( 97 | w3, entrypoint_contract, contractname, staked=False, deposit=10**18 98 | ): 99 | contract = deploy_contract( 100 | w3, 101 | contractname, 102 | ctrparams=[entrypoint_contract.address], 103 | ) 104 | if deposit is not None and deposit > 0: 105 | fund(w3, contract.address, deposit) 106 | if staked: 107 | return staked_contract(w3, entrypoint_contract, contract) 108 | return contract 109 | 110 | 111 | def fund(w3, addr, value=10**18): 112 | tx_hash = w3.eth.send_transaction( 113 | {"from": w3.eth.default_account, "to": addr, "value": value} 114 | ) 115 | w3.eth.wait_for_transaction_receipt(tx_hash) 116 | 117 | 118 | def staked_contract(w3, entrypoint_contract, contract): 119 | tx_hash = contract.functions.addStake(entrypoint_contract.address, 2).transact( 120 | {"from": w3.eth.default_account, "value": 1 * 10**18} 121 | ) 122 | assert int(tx_hash.hex(), 16), "could not stake contract" 123 | w3.eth.wait_for_transaction_receipt(tx_hash) 124 | info = entrypoint_contract.functions.getDepositInfo(contract.address).call() 125 | assert info[1], "could not get deposit information" 126 | return contract 127 | 128 | 129 | def deploy_wallet_contract(w3, value=2 * 10**18): 130 | return deploy_contract( 131 | w3, "SimpleWallet", ctrparams=[CommandLineArgs.entrypoint], value=value 132 | ) 133 | 134 | 135 | def deploy_state_contract(w3): 136 | return deploy_contract(w3, "State") 137 | 138 | 139 | def pack_factory(factory, factory_data): 140 | if factory is None: 141 | return "0x" 142 | return hex_concat(factory, factory_data) 143 | 144 | 145 | def pack_uints(high128, low128): 146 | return to_prefixed_hex((to_number(high128) << 128) + to_number(low128), 32) 147 | 148 | 149 | def pack_paymaster( 150 | paymaster, 151 | paymaster_verification_gas_limit, 152 | paymaster_post_op_gas_limit, 153 | paymaster_data, 154 | ): 155 | if paymaster is None: 156 | return "0x" 157 | if paymaster_data is None: 158 | paymaster_data = "" 159 | return hex_concat( 160 | paymaster, 161 | pack_uints(paymaster_verification_gas_limit, paymaster_post_op_gas_limit), 162 | paymaster_data, 163 | ) 164 | 165 | 166 | def pack_user_op(userop): 167 | 168 | payload = ( 169 | userop.sender, 170 | to_number(userop.nonce), 171 | pack_factory(userop.factory, userop.factoryData), 172 | userop.callData, 173 | pack_uints(userop.verificationGasLimit, userop.callGasLimit), 174 | to_number(userop.preVerificationGas), 175 | pack_uints(userop.maxPriorityFeePerGas, userop.maxFeePerGas), 176 | pack_paymaster( 177 | userop.paymaster, 178 | userop.paymasterVerificationGasLimit, 179 | userop.paymasterPostOpGasLimit, 180 | userop.paymasterData, 181 | ), 182 | userop.signature, 183 | ) 184 | return payload 185 | 186 | 187 | def userop_hash(helper_contract, userop): 188 | payload = pack_user_op(userop) 189 | return ( 190 | "0x" 191 | + helper_contract.functions.getUserOpHash(CommandLineArgs.entrypoint, payload) 192 | .call() 193 | .hex() 194 | ) 195 | 196 | 197 | def assert_ok(response): 198 | try: 199 | assert response.result 200 | except AttributeError as exc: 201 | raise AttributeError(f"expected result object, got:\n{response}") from exc 202 | 203 | 204 | def assert_rpc_error(response, message, code, data=None): 205 | try: 206 | assert response.code == code 207 | assert message.lower() in response.message.lower() 208 | if data is not None: 209 | assert response.data == data 210 | except AttributeError as exc: 211 | raise AttributeError(f"expected error object, got:\n{response}") from exc 212 | 213 | 214 | def get_sender_address(w3, factory, factory_data): 215 | ret = w3.eth.call({"to": factory, "data": factory_data}) 216 | # pylint: disable=unsubscriptable-object 217 | return to_checksum_address(decode(["address"], ret)[0]) 218 | 219 | 220 | def deposit_to_undeployed_sender(w3, entrypoint_contract, factory, factory_data): 221 | sender = get_sender_address(w3, factory, factory_data) 222 | tx_hash = entrypoint_contract.functions.depositTo(sender).transact( 223 | {"value": 10**18, "from": w3.eth.default_account} 224 | ) 225 | w3.eth.wait_for_transaction_receipt(tx_hash) 226 | return sender 227 | 228 | 229 | def send_bundle_now(url=None): 230 | assert_ok(RPCRequest(method="debug_bundler_sendBundleNow").send(url)) 231 | 232 | 233 | def set_manual_bundling_mode(url=None): 234 | assert_ok( 235 | RPCRequest(method="debug_bundler_setBundlingMode", params=["manual"]).send(url) 236 | ) 237 | 238 | 239 | def get_rip7560_debug_info(tx_hash, url=None): 240 | return RPCRequest( 241 | method="eth_getRip7560TransactionDebugInfo", params=[tx_hash] 242 | ).send(url) 243 | 244 | 245 | def dump_mempool(url=None): 246 | mempool = ( 247 | RPCRequest( 248 | method="debug_bundler_dumpMempool", params=[CommandLineArgs.entrypoint] 249 | ) 250 | .send(url) 251 | .result 252 | ) 253 | for i, entry in enumerate(mempool): 254 | if "executionData" in entry: 255 | mempool[i] = TransactionRIP7560(**entry) 256 | else: 257 | mempool[i] = UserOperation(**entry) 258 | return mempool 259 | 260 | 261 | # wait for mempool propagation. 262 | # ref_dump - a "dump_mempool" taken from that bundler before the tested operation. 263 | # wait for the `dump_mempool(url)` to change before returning it. 264 | def p2p_mempool(ref_dump, url=None, timeout=5): 265 | count = timeout * 2 266 | while True: 267 | new_dump = dump_mempool(url) 268 | if ref_dump != new_dump: 269 | return new_dump 270 | count = count - 1 271 | if count <= 0: 272 | raise TimeoutError(f"timed-out waiting mempool change propagate to {url}") 273 | time.sleep(0.5) 274 | 275 | 276 | def clear_mempool(url=None): 277 | return RPCRequest(method="debug_bundler_clearMempool").send(url) 278 | 279 | 280 | def clear_state(): 281 | assert_ok(RPCRequest(method="debug_bundler_clearState").send()) 282 | 283 | 284 | def get_stake_status(address, entry_point): 285 | return ( 286 | RPCRequest(method="debug_bundler_getStakeStatus", params=[address, entry_point]) 287 | .send() 288 | .result 289 | ) 290 | 291 | 292 | def dump_reputation(url=None): 293 | return ( 294 | RPCRequest( 295 | method="debug_bundler_dumpReputation", params=[CommandLineArgs.entrypoint] 296 | ) 297 | .send(url) 298 | .result 299 | ) 300 | 301 | 302 | def clear_reputation(url=None): 303 | assert_ok(RPCRequest(method="debug_bundler_clearReputation").send(url)) 304 | 305 | 306 | def set_reputation(address, ops_seen=1, ops_included=2, url=None): 307 | res = RPCRequest( 308 | method="debug_bundler_setReputation", 309 | params=[ 310 | [ 311 | { 312 | "address": address, 313 | "opsSeen": hex(ops_seen), 314 | "opsIncluded": hex(ops_included), 315 | } 316 | ], 317 | CommandLineArgs.entrypoint, 318 | ], 319 | ).send(url) 320 | 321 | assert res.result 322 | 323 | 324 | def hex_concat(*arr): 325 | return "0x" + "".join(arr).replace("0x", "") 326 | 327 | 328 | def to_prefixed_hex(s, byte_count=None): 329 | string = to_hex(s).replace("0x", "") 330 | pad = 0 331 | if byte_count: 332 | pad = max(byte_count * 2 - len(string), 0) 333 | return "0x" + ("0" * pad) + string 334 | 335 | 336 | def to_hex(s): 337 | if isinstance(s, str) and s.startswith("0x"): 338 | return s 339 | if isinstance(s, int): 340 | return hex(s) 341 | return s.encode().hex() 342 | 343 | 344 | def to_number(num_or_hex): 345 | return num_or_hex if isinstance(num_or_hex, (int, float)) else int(num_or_hex, 16) 346 | 347 | 348 | def sum_hex(*args): 349 | return sum(to_number(i) for i in args if i is not None) 350 | 351 | 352 | def get_userop_max_cost(user_op): 353 | return sum_hex( 354 | user_op.preVerificationGas, 355 | user_op.verificationGasLimit, 356 | user_op.callGasLimit, 357 | user_op.paymasterVerificationGasLimit, 358 | user_op.paymasterPostOpGasLimit, 359 | ) * to_number(user_op.maxFeePerGas) 360 | 361 | 362 | def get_rip7560_tx_max_cost(tx): 363 | tx_max_gas_limit = sum_hex( 364 | 15000, 365 | tx.verificationGasLimit, 366 | tx.callGasLimit, 367 | tx.paymasterVerificationGasLimit, 368 | tx.paymasterPostOpGasLimit, 369 | ) 370 | max_cost = tx_max_gas_limit * to_number(tx.maxFeePerGas) 371 | print( 372 | "get_rip7560_tx_max_cost", 373 | tx_max_gas_limit, 374 | to_number(tx.maxFeePerGas), 375 | max_cost, 376 | ) 377 | return max_cost 378 | -------------------------------------------------------------------------------- /tests/contracts/ValidationRules.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0 2 | pragma solidity ^0.8.25; 3 | 4 | import "@account-abstraction/contracts/interfaces/IEntryPoint.sol"; 5 | 6 | import "./Create2.sol"; 7 | import "./ITestAccount.sol"; 8 | import "./TestCoin.sol"; 9 | import "./TestRulesTarget.sol"; 10 | import "./ValidationRulesStorage.sol"; 11 | 12 | contract Dummy2 {} 13 | 14 | contract Dummy { 15 | uint public immutable value = 1; 16 | 17 | function create() public returns (uint) { 18 | new Dummy2(); 19 | return 0; 20 | } 21 | 22 | function create2() public returns (uint) { 23 | new Dummy2{salt: bytes32(uint(0x1))}(); 24 | return 0; 25 | } 26 | } 27 | 28 | 29 | library ValidationRules { 30 | using ValidationRules for string; 31 | event Uint(uint); 32 | 33 | function eq(string memory a, string memory b) internal pure returns (bool) { 34 | return keccak256(bytes(a)) == keccak256(bytes(b)); 35 | } 36 | 37 | function startsWith(string memory str, string memory prefix) internal pure returns (bool) { 38 | bytes memory strBytes = bytes(str); 39 | bytes memory prefixBytes = bytes(prefix); 40 | if (prefixBytes.length > strBytes.length) { 41 | return false; 42 | } 43 | for (uint256 i = 0; i < prefixBytes.length; i++) { 44 | if (strBytes[i] != prefixBytes[i]) { 45 | return false; 46 | } 47 | } 48 | return true; 49 | } 50 | 51 | function slice(string memory str, uint256 start, uint256 length) internal pure returns (string memory) { 52 | bytes memory strBytes = bytes(str); 53 | require(start + length <= strBytes.length, "Out of bounds"); 54 | bytes memory result = new bytes(length); 55 | for (uint256 i = 0; i < length; i++) { 56 | result[i] = strBytes[start + i]; 57 | } 58 | return string(result); 59 | } 60 | 61 | function includes(string memory str, string memory substr) internal pure returns (bool) { 62 | bytes memory strBytes = bytes(str); 63 | bytes memory substrBytes = bytes(substr); 64 | if (substrBytes.length > strBytes.length) { 65 | return false; 66 | } 67 | for (uint256 i = 0; i <= strBytes.length - substrBytes.length; i++) { 68 | bool matchFound = true; 69 | for (uint256 j = 0; j < substrBytes.length; j++) { 70 | if (strBytes[i + j] != substrBytes[j]) { 71 | matchFound = false; 72 | break; 73 | } 74 | } 75 | if (matchFound) { 76 | return true; 77 | } 78 | } 79 | return false; 80 | } 81 | 82 | //return by runRule if string is unknown. 83 | uint constant public UNKNOWN = type(uint).max; 84 | 85 | error CustomError(string error, uint256 code); 86 | 87 | function runFactorySpecificRule(uint nonce, string memory rule, address _entryPoint, address create2address) internal { 88 | if (eq(rule, "EXTCODEx_CALLx_undeployed_sender")) { 89 | // CALL 90 | create2address.call(""); 91 | // CALLCODE 92 | assembly { 93 | let res := callcode(5000, create2address, 0, 0, 0, 0, 0) 94 | } 95 | // DELEGATECALL 96 | create2address.delegatecall(""); 97 | // STATICCALL 98 | create2address.staticcall(""); 99 | // EXTCODESIZE 100 | emit Uint(create2address.code.length); 101 | // EXTCODEHASH 102 | emit Uint(uint256(create2address.codehash)); 103 | // EXTCODECOPY 104 | assembly { 105 | extcodecopy(create2address, 0, 0, 2) 106 | } 107 | } 108 | } 109 | 110 | function runRule( 111 | string memory rule, 112 | IState account, 113 | address paymaster, 114 | address factory, 115 | TestCoin coin, 116 | ValidationRulesStorage self, 117 | TestRulesTarget target 118 | ) internal returns (uint) { 119 | if (eq(rule, "")) return 0; 120 | else if (eq(rule, "revert-msg")) { 121 | revert("on-chain revert message string"); 122 | } else if (eq(rule, "revert-custom-msg")) { 123 | revert CustomError("on-chain custom error", 777); 124 | } else if (eq(rule, "revert-out-of-gas-msg")) { 125 | uint256 a = 0; 126 | while (true) { 127 | a++; 128 | } 129 | } 130 | else if (eq(rule, "GAS")) return gasleft(); 131 | else if (eq(rule, "NUMBER")) return block.number; 132 | else if (eq(rule, "TIMESTAMP")) return block.timestamp; 133 | else if (eq(rule, "COINBASE")) return uint160(address(block.coinbase)); 134 | else if (eq(rule, "DIFFICULTY")) return uint160(block.difficulty); 135 | else if (eq(rule, "BASEFEE")) return uint160(block.basefee); 136 | else if (eq(rule, "GASLIMIT")) return uint160(block.gaslimit); 137 | else if (eq(rule, "GASPRICE")) return uint160(tx.gasprice); 138 | else if (eq(rule, "BALANCE")) return uint160(address(msg.sender).balance); 139 | else if (eq(rule, "ORIGIN")) return uint160(address(tx.origin)); 140 | else if (eq(rule, "BLOCKHASH")) return uint(blockhash(0)); 141 | else if (eq(rule, "nested-CREATE")) return new Dummy().create(); 142 | else if (eq(rule, "nested-CREATE2")) return new Dummy().create2(); 143 | else if (eq(rule, "CREATE")) return new Dummy().value(); 144 | else if (eq(rule, "CREATE2")) return new Dummy{salt : bytes32(uint(0x1))}().value(); 145 | else if (eq(rule, "SELFDESTRUCT")) { 146 | coin.destruct(); 147 | return 0; 148 | } 149 | else if (eq(rule, "BLOBHASH")) return uint(blobhash(0)); 150 | else if (eq(rule, "BLOBBASEFEE")) return block.blobbasefee; 151 | else if (eq(rule, "CALL_undeployed_contract")) { address(100100).call(""); return 0; } 152 | else if (eq(rule, "CALL_undeployed_contract_allowed_precompile")) { 153 | for (uint160 i = 1; i < 10; i++){ 154 | address(i).call{gas: 100000}(""); 155 | } 156 | return 0; 157 | } 158 | else if (eq(rule, "SELFBALANCE")) { 159 | uint256 selfb; 160 | assembly { 161 | selfb := selfbalance() 162 | } 163 | return selfb; 164 | } 165 | else if (eq(rule, "CALLCODE_undeployed_contract")) { 166 | assembly { 167 | let res := callcode(5000, 100200, 0, 0, 0, 0, 0) 168 | } 169 | return 0; 170 | } 171 | else if (eq(rule, "DELEGATECALL_undeployed_contract")) { address(100300).delegatecall(""); return 0; } 172 | else if (eq(rule, "STATICCALL_undeployed_contract")) { address(100400).staticcall(""); return 0; } 173 | else if (eq(rule, "EXTCODESIZE_undeployed_contract")) return address(100500).code.length; 174 | else if (eq(rule, "EXTCODEHASH_undeployed_contract")) return uint256(address(100600).codehash); 175 | else if (eq(rule, "EXTCODECOPY_undeployed_contract")) { 176 | assembly { 177 | extcodecopy(100700, 0, 0, 2) 178 | } 179 | return 0; 180 | } 181 | else if (eq(rule, "EXTCODESIZE_entrypoint")) { 182 | address ep = address(self.entryPoint()); 183 | uint len; 184 | assembly { 185 | len := extcodesize(ep) 186 | } 187 | return len; 188 | } 189 | else if (eq(rule, "EXTCODEHASH_entrypoint")) return uint256(address(self.entryPoint()).codehash); 190 | else if (eq(rule, "EXTCODECOPY_entrypoint")) { 191 | address ep = address(self.entryPoint()); 192 | assembly { 193 | extcodecopy(ep, 0, 0, 2) 194 | } 195 | return 0; 196 | } 197 | else if (eq(rule, "GAS CALL")) { 198 | // 'GAS CALL' sequence is what solidity does anyway 199 | // this test makes it explicit so we know for sure nothing changes here 200 | address addr = address(coin); 201 | address acc = address(account); 202 | bytes4 sig = coin.balanceOf.selector; 203 | assembly { 204 | let x := mload(0x40) 205 | mstore(x, sig) 206 | mstore(add(x, 0x04), acc) 207 | let success := call( 208 | gas(), // GAS opcode 209 | addr, 210 | 0, 211 | x, 212 | 0x44, 213 | x, 214 | 0x20) 215 | } 216 | return 0; 217 | } 218 | else if (eq(rule, "GAS DELEGATECALL")) { 219 | address addr = address(coin); 220 | address acc = address(account); 221 | bytes4 sig = coin.balanceOf.selector; 222 | assembly { 223 | let x := mload(0x40) 224 | mstore(x, sig) 225 | mstore(add(x, 0x04), acc) 226 | let success := delegatecall( 227 | gas(), // GAS opcode 228 | addr, 229 | x, 230 | 0x44, 231 | x, 232 | 0x20) 233 | } 234 | return 0; 235 | } 236 | 237 | else if (eq(rule, "no_storage")) return 0; 238 | else if (eq(rule, "storage_read")) return self.state(); 239 | else if (eq(rule, "storage_write")) return self.funSSTORE(); 240 | else if (eq(rule, "account_storage")) return account.state(); 241 | 242 | else if (eq(rule, "account_reference_storage")) return coin.balanceOf(address(account)); 243 | else if (eq(rule, "account_reference_storage_struct")) return coin.setStructMember(address(account)); 244 | else if (eq(rule, "account_reference_storage_init_code")) return coin.balanceOf(address(account)); 245 | 246 | else if (eq(rule, "paymaster_reference_storage")) return coin.mint(paymaster); 247 | else if (eq(rule, "paymaster_reference_storage_struct")) return coin.setStructMember(paymaster); 248 | 249 | else if (eq(rule, "factory_reference_storage")) return coin.mint(factory); 250 | else if (eq(rule, "factory_reference_storage_struct")) return coin.setStructMember(factory); 251 | 252 | else if (eq(rule, "external_storage_read")) return coin.balanceOf(address(0xdeadcafe)); 253 | else if (eq(rule, "external_storage_write")) return coin.mint(address(0xdeadcafe)); 254 | 255 | else if (eq(rule, "transient_storage_tstore")) return self.funTSTORE(); 256 | else if (eq(rule, "transient_storage_tload")) return self.funTLOAD(); 257 | else if (eq(rule, "account_transient_storage")) return account.funTSTORE(); 258 | 259 | else if (eq(rule, "entryPoint_call_balanceOf")) return self.entryPoint().balanceOf(address(account)); 260 | else if (eq(rule, "eth_value_transfer_forbidden")) return coin.receiveValue{value: 666}(); 261 | else if (eq(rule, "eth_value_transfer_entryPoint")) { 262 | payable(address(self.entryPoint())).call{value: 777}(""); 263 | return 777; 264 | } 265 | 266 | else if (eq(rule, "eth_value_transfer_entryPoint_depositTo")) { 267 | self.entryPoint().depositTo{value: 888}(address(account)); 268 | return 888; 269 | } 270 | else if (eq(rule, "out_of_gas")) { 271 | (bool success,) = address(this).call{gas:10000}(abi.encodeWithSelector(self.revertOOG.selector)); 272 | require(!success, "reverting oog"); 273 | return 0; 274 | } 275 | else if (eq(rule, "sstore_out_of_gas")) { 276 | (bool success,) = address(this).call{gas:2299}(abi.encodeWithSelector(self.revertOOGSSTORE.selector)); 277 | require(!success, "reverting pseudo oog"); 278 | return 0; 279 | } 280 | else if (startsWith(rule, "DELEGATECALL:>")) { 281 | string memory innerRule = rule.slice(14, bytes(rule).length - 14); 282 | bytes memory callData = abi.encodeCall(target.runRule, (innerRule, account, paymaster, factory, coin, self, TestRulesTarget(payable(0)))); 283 | (bool success, bytes memory ret) = address(target).delegatecall(callData); 284 | require(success, string(abi.encodePacked("DELEGATECALL rule reverted", ret))); 285 | return 0; 286 | } 287 | else if (startsWith(rule, "CALL:>")) { 288 | string memory innerRule = rule.slice(6, bytes(rule).length - 6); 289 | target.runRule{value: msg.value}(innerRule, account, paymaster, factory, coin, self, TestRulesTarget(payable(0))); 290 | return 0; 291 | } 292 | revert(string.concat("unknown rule: ", rule)); 293 | } 294 | } 295 | --------------------------------------------------------------------------------