├── 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 |
--------------------------------------------------------------------------------