diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24c8a191..a95b5edd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ jobs: - name: Setup third party dependencies run: | make fetch-thirdparty + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly - name: Install solc run: | sudo add-apt-repository ppa:ethereum/ethereum diff --git a/.gitmodules b/.gitmodules index e96c9da1..1a2fe4b5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "thirdparty/account-abstraction"] path = thirdparty/account-abstraction url = https://github.com/eth-infinitism/account-abstraction.git +[submodule "thirdparty/bundler"] + path = thirdparty/bundler + url = https://github.com/eth-infinitism/bundler.git diff --git a/Cargo.lock b/Cargo.lock index b5eccfda..84e714ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,7 +1019,7 @@ dependencies = [ [[package]] name = "ethers" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -1034,7 +1034,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "ethers-core", "once_cell", @@ -1045,7 +1045,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", @@ -1063,7 +1063,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "Inflector", "cfg-if", @@ -1087,7 +1087,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "ethers-contract-abigen", "ethers-core", @@ -1101,7 +1101,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "arrayvec", "bytes", @@ -1132,7 +1132,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "ethers-core", "ethers-solc", @@ -1149,7 +1149,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "async-trait", "auto_impl 0.5.0", @@ -1174,7 +1174,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "async-trait", "auto_impl 1.0.1", @@ -1209,7 +1209,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "async-trait", "coins-bip32", @@ -1226,7 +1226,7 @@ dependencies = [ [[package]] name = "ethers-solc" version = "1.0.2" -source = "git+https://github.com/gakonst/ethers-rs?rev=b27c7b0#b27c7b0773d7ba329e0e1eebb37db652ac8fa601" +source = "git+https://github.com/gakonst/ethers-rs.git?rev=3ed83d5dd38e7819605511631aaddcc3e472427e#3ed83d5dd38e7819605511631aaddcc3e472427e" dependencies = [ "cfg-if", "dunce", diff --git a/Cargo.toml b/Cargo.toml index 00ff8ada..6893c3c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Vid Kersic "] edition = "2021" description = "AA - Bundler implementation" default-run = "bundler" +rust-version = "1.66.1" [dependencies] anyhow = "1" @@ -13,7 +14,7 @@ clap = { version = "4", features = ["derive"] } dirs = "4.0" educe = { version = "0.4", features = ["Debug", "Default"] } ethereum-interfaces = { git = "https://github.com/ledgerwatch/interfaces" } -ethers = { git = "https://github.com/gakonst/ethers-rs", rev = "b27c7b0", features = ["ethers-solc"] } +ethers = { git = "https://github.com/gakonst/ethers-rs.git", rev = "3ed83d5dd38e7819605511631aaddcc3e472427e", features = ["ethers-solc"] } expanded-pathbuf = "0.1" hex = { version = "0.4.3", default-features = false, features = ["std"] } jsonrpsee = { version = "0.16", features = ["server", "macros"] } @@ -34,7 +35,7 @@ tracing-subscriber = "0.3" [build-dependencies] anyhow = "1" -ethers = { git = "https://github.com/gakonst/ethers-rs", rev = "b27c7b0", features = ["ethers-solc"] } +ethers = { git = "https://github.com/gakonst/ethers-rs.git", rev = "3ed83d5dd38e7819605511631aaddcc3e472427e", features = ["ethers-solc"] } protobuf-src = "1.1.0" prost-build = "0.11" tonic-build = "0.8" diff --git a/Makefile b/Makefile index 4142a119..a668d04c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ build: + make fetch-thirdparty + cd thirdparty/account-abstraction && yarn install && yarn compile && cd ../.. cargo build run-bundler: @@ -14,14 +16,21 @@ run-create-wallet: cargo run --bin create-wallet -- --output-path ${HOME}/.aa-bundler fetch-thirdparty: - git submodule update --init + git submodule update --init -test: +test: + cd thirdparty/bundler && yarn install && yarn preprocess && cd ../.. cargo test format: cargo fmt --all lint: + cd thirdparty/bundler && yarn install && yarn preprocess && cd ../.. cargo fmt --all -- --check - cargo clippy -- -D warnings -A clippy::derive_partial_eq_without_eq \ No newline at end of file + cargo clippy --tests -- -D warnings -A clippy::derive_partial_eq_without_eq + +clean: + cd thirdparty/account-abstraction && yarn clean && cd ../.. + cd thirdparty/bundler && yarn clear && cd ../.. + cargo clean diff --git a/README.md b/README.md index 10d95669..90a677f1 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ For more information: https://hackmd.io/@Vid201/aa-bundler-rust ## Prerequisites -1. Ethereum JSON-RPC API with enabled [`debug_traceCall`](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debug_tracecall) (currently implemented only in [Geth](https://github.com/ethereum/go-ethereum) and [Erigon](https://github.com/ledgerwatch/erigon)). For testing purposes, you can setup [private Geth node](https://github.com/krzkaczor/geth-private-node). -1. [solc](https://docs.soliditylang.org/en/v0.8.17/installing-solidity.html) >=0.8.12 +1. Ethereum execution client JSON-RPC API with enabled [`debug_traceCall`](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debug_tracecall). For production, you can use [Geth](https://github.com/ethereum/go-ethereum) or [Erigon](https://github.com/ledgerwatch/erigon). For testing purposes, you can use [Anvil](https://github.com/foundry-rs/foundry/tree/master/anvil#anvil), which provides enough functionalities of execution clients like Geth. To install Anvil, use [foundryup](https://getfoundry.sh/) +2. [solc](https://docs.soliditylang.org/en/v0.8.17/installing-solidity.html) >=0.8.12 ## How to run? diff --git a/src/types/user_operation.rs b/src/types/user_operation.rs index 19ab3e8f..7b6d9324 100644 --- a/src/types/user_operation.rs +++ b/src/types/user_operation.rs @@ -135,7 +135,7 @@ mod tests { call_gas_limit: U256::from(200000), verification_gas_limit: U256::from(100000), pre_verification_gas: U256::from(21000), - max_fee_per_gas: U256::from(3000000000 as u64), + max_fee_per_gas: U256::from(3000000000_u64), max_priority_fee_per_gas: U256::from(1000000000), paymaster_and_data: Bytes::default(), signature: Bytes::from_str("0x7cb39607585dee8e297d0d7a669ad8c5e43975220b6773c10a138deadbc8ec864981de4b9b3c735288a217115fb33f8326a61ddabc60a534e3b5536515c70f931c").unwrap(), @@ -169,7 +169,7 @@ mod tests { call_gas_limit: U256::from(200000), verification_gas_limit: U256::from(100000), pre_verification_gas: U256::from(21000), - max_fee_per_gas: U256::from(3000000000 as u64), + max_fee_per_gas: U256::from(3000000000_u64), max_priority_fee_per_gas: U256::from(1000000000), paymaster_and_data: Bytes::default(), signature: Bytes::from_str("0x7cb39607585dee8e297d0d7a669ad8c5e43975220b6773c10a138deadbc8ec864981de4b9b3c735288a217115fb33f8326a61ddabc60a534e3b5536515c70f931c").unwrap(), @@ -203,7 +203,7 @@ mod tests { call_gas_limit: U256::from(200000), verification_gas_limit: U256::from(100000), pre_verification_gas: U256::from(21000), - max_fee_per_gas: U256::from(3000000000 as u64), + max_fee_per_gas: U256::from(3000000000_u64), max_priority_fee_per_gas: U256::from(1000000000), paymaster_and_data: Bytes::default(), signature: Bytes::from_str("0x7cb39607585dee8e297d0d7a669ad8c5e43975220b6773c10a138deadbc8ec864981de4b9b3c735288a217115fb33f8326a61ddabc60a534e3b5536515c70f931c").unwrap(), diff --git a/src/uopool/memory.rs b/src/uopool/memory.rs index 7463a5c3..153c74f3 100644 --- a/src/uopool/memory.rs +++ b/src/uopool/memory.rs @@ -109,6 +109,7 @@ mod tests { use super::*; use ethers::types::{H256, U256}; + #[allow(clippy::unit_cmp)] #[tokio::test] async fn memory_mempool() { let entry_point = Address::random(); diff --git a/tests/common/gen.rs b/tests/common/gen.rs new file mode 100644 index 00000000..0188dcdd --- /dev/null +++ b/tests/common/gen.rs @@ -0,0 +1,40 @@ +use ethers::prelude::abigen; + +abigen!(SimpleAccountFactory, + "$CARGO_MANIFEST_DIR/thirdparty/account-abstraction/artifacts/contracts/samples/SimpleAccountFactory.sol/SimpleAccountFactory.json"); + +abigen!(SimpleAccount, + "$CARGO_MANIFEST_DIR/thirdparty/account-abstraction/artifacts/contracts/samples/SimpleAccount.sol/SimpleAccount.json"); + +abigen!( + EntryPointContract, + "$CARGO_MANIFEST_DIR/thirdparty/account-abstraction/artifacts/contracts/core/EntryPoint.sol/EntryPoint.json" +); +abigen!( + TestOpcodesAccountFactory, + "$CARGO_MANIFEST_DIR/thirdparty/bundler/packages/bundler/artifacts/contracts/tests/TestOpcodesAccount.sol/TestOpcodesAccountFactory.json" +); +abigen!( + TestOpcodesAccount, + "$CARGO_MANIFEST_DIR/thirdparty/bundler/packages/bundler/artifacts/contracts/tests/TestOpcodesAccount.sol/TestOpcodesAccount.json" +); +abigen!( + TestStorageAccount, + "$CARGO_MANIFEST_DIR/thirdparty/bundler/packages/bundler/artifacts/contracts/tests/TestStorageAccount.sol/TestStorageAccount.json" +); +abigen!( + TestRecursionAccount, + "$CARGO_MANIFEST_DIR/thirdparty/bundler/packages/bundler/artifacts/contracts/tests/TestRecursionAccount.sol/TestRecursionAccount.json" +); +abigen!( + TestStorageAccountFactory, + "$CARGO_MANIFEST_DIR/thirdparty/bundler/packages/bundler/artifacts/contracts/tests/TestStorageAccount.sol/TestStorageAccountFactory.json" +); +abigen!( + TestRulesAccount, + "$CARGO_MANIFEST_DIR/thirdparty/bundler/packages/bundler/artifacts/contracts/tests/TestRulesAccount.sol/TestRulesAccount.json" +); +abigen!( + TestRulesAccountFactory, + "$CARGO_MANIFEST_DIR/thirdparty/bundler/packages/bundler/artifacts/contracts/tests/TestRulesAccount.sol/TestRulesAccountFactory.json" +); diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..d16ad027 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,115 @@ +use std::sync::Arc; + +use aa_bundler::types::user_operation::UserOperation; +use ethers::{ + prelude::k256::ecdsa::SigningKey, + providers::Middleware, + signers::{Signer, Wallet}, + types::{Address, Bytes, U256}, +}; + +use self::gen::{ + EntryPointContract, TestOpcodesAccount, TestOpcodesAccountFactory, TestRecursionAccount, + TestRulesAccountFactory, TestStorageAccount, TestStorageAccountFactory, +}; +pub mod gen; + +pub const ANVIL_TEST_KEY_PHRASE: &str = + "test test test test test test test test test test test junk"; + +pub struct DeployedContract { + contract: C, + pub address: Address, +} +impl DeployedContract { + pub fn new(contract: C, address: Address) -> Self { + Self { contract, address } + } + + pub fn contract(&self) -> &C { + &self.contract + } +} + +pub async fn deploy_entry_point( + client: Arc, +) -> anyhow::Result>> { + let (entry_point, receipt) = EntryPointContract::deploy(client, ())? + .send_with_receipt() + .await?; + let address = receipt.contract_address.unwrap(); + Ok(DeployedContract::new(entry_point, address)) +} + +pub async fn deploy_test_opcode_account( + client: Arc, +) -> anyhow::Result>> { + let (account, receipt) = TestOpcodesAccount::deploy(client, ())? + .send_with_receipt() + .await?; + let address = receipt.contract_address.unwrap(); + Ok(DeployedContract::new(account, address)) +} + +pub async fn deploy_test_opcode_account_factory( + client: Arc, +) -> anyhow::Result>> { + let (factory, receipt) = TestOpcodesAccountFactory::deploy(client, ())? + .send_with_receipt() + .await?; + let address = receipt.contract_address.unwrap(); + Ok(DeployedContract::new(factory, address)) +} + +pub async fn deploy_test_storage_account_factory( + client: Arc, +) -> anyhow::Result>> { + let (factory, receipt) = TestStorageAccountFactory::deploy(client, ())? + .send_with_receipt() + .await?; + let address = receipt.contract_address.unwrap(); + Ok(DeployedContract::new(factory, address)) +} + +pub async fn deploy_test_storage_account( + client: Arc, +) -> anyhow::Result>> { + let (account, receipt) = TestStorageAccount::deploy(client, ())? + .send_with_receipt() + .await?; + let address = receipt.contract_address.unwrap(); + Ok(DeployedContract::new(account, address)) +} + +pub async fn deploy_test_recursion_account( + client: Arc, + entry_point_address: Address, +) -> anyhow::Result>> { + let (account, receipt) = TestRecursionAccount::deploy(client, entry_point_address)? + .send_with_receipt() + .await?; + let address = receipt.contract_address.unwrap(); + Ok(DeployedContract::new(account, address)) +} + +pub async fn deploy_test_rules_account_factory( + client: Arc, +) -> anyhow::Result>> { + let (factory, receipt) = TestRulesAccountFactory::deploy(client, ())? + .send_with_receipt() + .await?; + let address = receipt.contract_address.unwrap(); + Ok(DeployedContract::new(factory, address)) +} + +pub async fn sign( + user_op: &mut UserOperation, + entry_point_address: Address, + chain_id: U256, + key: Wallet, +) -> anyhow::Result<()> { + let user_op_hash = user_op.hash(entry_point_address, chain_id); + let signature = key.sign_message(user_op_hash.as_bytes()).await?; + user_op.signature = Bytes::from(signature.to_vec()); + Ok(()) +} diff --git a/tests/validate_tests.rs b/tests/validate_tests.rs new file mode 100644 index 00000000..d0c281d6 --- /dev/null +++ b/tests/validate_tests.rs @@ -0,0 +1,518 @@ +pub mod common; + +use aa_bundler::types::user_operation::UserOperation; +use common::gen::{ + EntryPointContract, TestOpcodesAccount, TestOpcodesAccountFactory, TestRulesAccount, + TestRulesAccountFactory, TestStorageAccountFactory, +}; +use common::DeployedContract; +use ethers::abi::Token; +use ethers::prelude::BaseContract; +use ethers::providers::Http; +use ethers::types::transaction::eip2718::TypedTransaction; +use ethers::types::Address; +use ethers::utils::{parse_units, AnvilInstance}; +use ethers::{ + core::utils::Anvil, + prelude::SignerMiddleware, + providers::{Middleware, Provider}, + signers::{LocalWallet, Signer}, + types::{Bytes, U256}, +}; +use std::ops::Deref; +use std::{convert::TryFrom, sync::Arc, time::Duration}; + +use crate::common::{ + deploy_entry_point, deploy_test_opcode_account, deploy_test_opcode_account_factory, + deploy_test_recursion_account, deploy_test_rules_account_factory, deploy_test_storage_account, + deploy_test_storage_account_factory, +}; + +struct TestContext { + pub client: Arc, + pub _anvil: AnvilInstance, + pub entry_point: DeployedContract>, + pub paymaster: DeployedContract>, + pub opcodes_factory: DeployedContract>, + pub storage_factory: DeployedContract>, + pub _rules_factory: DeployedContract>, + pub storage_account: DeployedContract>, +} + +type ClientType = SignerMiddleware, LocalWallet>; + +async fn setup() -> anyhow::Result> { + let anvil = Anvil::new().spawn(); + let provider = + Provider::::try_from(anvil.endpoint())?.interval(Duration::from_millis(10u64)); + let wallet: LocalWallet = anvil.keys()[0].clone().into(); + + let client = Arc::new(SignerMiddleware::new( + provider, + wallet.clone().with_chain_id(anvil.chain_id()), + )); + let entry_point = deploy_entry_point(client.clone()).await?; + let paymaster = deploy_test_opcode_account(client.clone()).await?; + entry_point + .contract() + .deposit_to(paymaster.address) + .value(parse_units("0.1", "ether").unwrap()) + .send() + .await?; + paymaster + .contract() + .add_stake(entry_point.address) + .value(parse_units("0.1", "ether").unwrap()) + .send() + .await?; + + let opcodes_factory = deploy_test_opcode_account_factory(client.clone()).await?; + let storage_factory = deploy_test_storage_account_factory(client.clone()).await?; + let rules_factory = deploy_test_rules_account_factory(client.clone()).await?; + + let storage_account_call = rules_factory.contract().create("".to_string()); + let storage_account_address = storage_account_call.call().await?; + + storage_account_call.send().await?; + + entry_point + .contract() + .deposit_to(storage_account_address) + .value(parse_units("1", "ether").unwrap()) + .send() + .await?; + Ok(TestContext:: { + client: client.clone(), + _anvil: anvil, + entry_point, + paymaster, + opcodes_factory, + storage_factory, + _rules_factory: rules_factory, + storage_account: DeployedContract::new( + TestRulesAccount::new(storage_account_address, client.clone()), + storage_account_address, + ), + }) +} + +async fn create_storage_factory_init_code( + salt: u64, + init_func: String, +) -> anyhow::Result<(Bytes, Bytes)> { + let context = setup().await?; + let contract: &BaseContract = context.storage_factory.contract().deref().deref(); + + let function = contract.abi().function("create")?; + let init_func = + function.encode_input(&[Token::Uint(U256::from(salt)), Token::String(init_func)])?; + let mut init_code = vec![]; + init_code.extend_from_slice(context.storage_factory.address.as_bytes()); + init_code.extend_from_slice(init_func.as_ref()); + Ok((Bytes::from(init_code), Bytes::from(init_func))) +} +async fn create_opcode_factory_init_code(init_func: String) -> anyhow::Result<(Bytes, Bytes)> { + let context = setup().await?; + let contract: &BaseContract = context.opcodes_factory.contract().deref().deref(); + + let token = vec![Token::String(init_func)]; + let function = contract.abi().function("create")?; + let init_func = function.encode_input(&token)?; + let mut init_code = vec![]; + init_code.extend_from_slice(context.opcodes_factory.address.as_bytes()); + init_code.extend_from_slice(&init_func); + Ok((Bytes::from(init_code), Bytes::from(init_func))) +} + +async fn create_test_user_op( + validate_rule: String, + pm_rule: Option, + init_code: Bytes, + init_func: Bytes, + factory_address: Address, +) -> anyhow::Result { + let context = setup().await?; + + let paymaster_and_data = if let Some(rule) = pm_rule { + let mut data = vec![]; + data.extend_from_slice(context.paymaster.address.as_bytes()); + data.extend_from_slice(rule.as_bytes()); + Bytes::from(data) + } else { + Bytes::default() + }; + + let signature = Bytes::from(validate_rule.as_bytes().to_vec()); + + let mut tx = TypedTransaction::default(); + tx.set_to(factory_address); + tx.set_data(init_func); + + let call_init_code_for_addr = context.client.call(&tx, None).await?; + + let (head, address) = call_init_code_for_addr.split_at(12); + anyhow::ensure!( + !head.iter().any(|i| *i != 0), + format!( + "call init code returns non address data : {:?}", + call_init_code_for_addr + ) + ); + + let sender = Address::from_slice(address); + + Ok(UserOperation { + sender, + nonce: U256::zero(), + init_code, + call_data: Bytes::default(), + call_gas_limit: U256::from(1000000), + verification_gas_limit: U256::from(1000000), + pre_verification_gas: U256::from(50000), + max_fee_per_gas: U256::from(0), + max_priority_fee_per_gas: U256::from(0), + paymaster_and_data, + signature, + }) +} + +async fn existing_storage_account_user_op( + validate_rule: String, + pm_rule: String, +) -> anyhow::Result { + let context = setup().await?; + + let mut paymaster_and_data = vec![]; + paymaster_and_data.extend_from_slice(context.paymaster.address.as_bytes()); + paymaster_and_data.extend_from_slice(pm_rule.as_bytes()); + + let signature = Bytes::from(validate_rule.as_bytes().to_vec()); + Ok(UserOperation { + sender: context.storage_account.address, + nonce: U256::zero(), + init_code: Bytes::default(), + call_data: Bytes::default(), + call_gas_limit: U256::from(1000000), + verification_gas_limit: U256::from(1000000), + pre_verification_gas: U256::from(50000), + max_fee_per_gas: U256::from(0), + max_priority_fee_per_gas: U256::from(0), + paymaster_and_data: Bytes::from(paymaster_and_data), + signature, + }) +} + +fn validate(_user_op: UserOperation) -> anyhow::Result<()> { + // TODO + Ok(()) +} + +async fn test_user_op( + validate_rule: String, + pm_rule: Option, + init_code: Bytes, + init_func: Bytes, + factory_address: Address, +) -> anyhow::Result<()> { + let user_op = create_test_user_op( + validate_rule, + pm_rule, + init_code, + init_func, + factory_address, + ) + .await?; + validate(user_op) +} + +async fn test_existing_user_op(validate_rule: String, pm_rule: String) -> anyhow::Result<()> { + let user_op = existing_storage_account_user_op(validate_rule, pm_rule).await?; + validate(user_op) +} + +#[tokio::test] +async fn accept_plain_request() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_opcode_factory_init_code("".to_string()) + .await + .unwrap(); + test_user_op( + "".to_string(), + None, + init_code, + init_func, + context.opcodes_factory.address, + ) + .await + .expect("succeed"); + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn reject_unkown_rule() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_opcode_factory_init_code("".to_string()) + .await + .unwrap(); + test_user_op( + "".to_string(), + None, + init_code, + init_func, + context.opcodes_factory.address, + ) + .await + .expect_err("unknown rule"); + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn fail_with_bad_opcode_in_ctr() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_opcode_factory_init_code("coinbase".to_string()) + .await + .unwrap(); + test_user_op( + "".to_string(), + None, + init_code, + init_func, + context.opcodes_factory.address, + ) + .await + .expect_err("factory uses banned opcode: COINBASE"); + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn fail_with_bad_opcode_in_paymaster() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_opcode_factory_init_code("".to_string()) + .await + .unwrap(); + test_user_op( + "".to_string(), + Some("coinbase".to_string()), + init_code, + init_func, + context.opcodes_factory.address, + ) + .await + .expect_err("paymaster uses banned opcode: COINBASE"); + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn fail_with_bad_opcode_in_validation() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_opcode_factory_init_code("".to_string()) + .await + .unwrap(); + test_user_op( + "blockhash".to_string(), + None, + init_code, + init_func, + context.opcodes_factory.address, + ) + .await + .expect_err("account uses banned opcode: BLOCKHASH"); + Ok(()) +} + +#[tokio::test] +async fn fail_if_create_too_many() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_opcode_factory_init_code("".to_string()) + .await + .unwrap(); + test_user_op( + "create2".to_string(), + None, + init_code, + init_func, + context.opcodes_factory.address, + ) + .await + .expect("account uses banned opcode: CREATE2"); + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn fail_referencing_self_token() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_storage_factory_init_code(0, "".to_string()) + .await + .unwrap(); + test_user_op( + "balance-self".to_string(), + None, + init_code, + init_func, + context.storage_factory.address, + ) + .await + .expect_err("unstaked account accessed"); + Ok(()) +} + +#[tokio::test] +async fn account_succeeds_referecing_its_own_balance() { + test_existing_user_op("balance-self".to_string(), "".to_string()) + .await + .expect("succeed"); +} + +#[tokio::test] +#[ignore] +async fn account_fail_to_read_allowance_of_address() { + test_existing_user_op("allowance-self-1".to_string(), "".to_string()) + .await + .expect_err("account has forbidden read"); +} + +#[tokio::test] +async fn account_can_reference_its_own_allowance_on_other_contract_balance() { + test_existing_user_op("allowance-1-self".to_string(), "".to_string()) + .await + .expect("succeed"); +} + +#[tokio::test] +async fn access_self_struct_data() { + test_existing_user_op("struct-self".to_string(), "".to_string()) + .await + .expect("succeed"); +} + +#[tokio::test] +#[ignore] +async fn fail_to_access_other_address_struct_data() { + test_existing_user_op("struct-1".to_string(), "".to_string()) + .await + .expect_err("account has forbidden read"); +} + +#[tokio::test] +#[ignore] +async fn fail_if_referencing_other_token_balance() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_storage_factory_init_code(0, "".to_string()) + .await + .unwrap(); + test_user_op( + "balance-1".to_string(), + None, + init_code, + init_func, + context.storage_factory.address, + ) + .await + .expect_err("account has forbidden read"); + Ok(()) +} + +#[tokio::test] +async fn fail_if_referencing_self_token_balance_after_wallet_creation() { + test_existing_user_op("balance-self".to_string(), "".to_string()) + .await + .expect("succeed"); +} + +#[tokio::test] +#[ignore] +async fn fail_with_unstaked_paymaster_returning_context() -> anyhow::Result<()> { + let context = setup().await?; + let pm = deploy_test_storage_account(context.client.clone()) + .await + .expect("deploy succeed"); + let acct = deploy_test_recursion_account(context.client.clone(), context.entry_point.address) + .await + .expect("deploy succeed"); + + let mut paymaster_and_data = vec![]; + paymaster_and_data.extend_from_slice(pm.address.as_bytes()); + paymaster_and_data.extend_from_slice("postOp-context".as_bytes()); + + let user_op = UserOperation { + sender: acct.address, + nonce: U256::zero(), + init_code: Bytes::default(), + call_data: Bytes::default(), + call_gas_limit: U256::from(0), + verification_gas_limit: U256::from(50000), + pre_verification_gas: U256::from(0), + max_fee_per_gas: U256::from(0), + max_priority_fee_per_gas: U256::from(0), + paymaster_and_data: Bytes::from(paymaster_and_data), + signature: Bytes::default(), + }; + validate(user_op).expect_err("unstaked paymaster must not return context"); + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn fail_with_validation_recursively_calls_handle_ops() -> anyhow::Result<()> { + let context = setup().await?; + let acct = deploy_test_recursion_account(context.client.clone(), context.entry_point.address) + .await + .expect("deploy succeed"); + let user_op = UserOperation { + sender: acct.address, + nonce: U256::zero(), + init_code: Bytes::default(), + call_data: Bytes::default(), + call_gas_limit: U256::from(0), + verification_gas_limit: U256::from(50000), + pre_verification_gas: U256::from(50000), + max_fee_per_gas: U256::from(0), + max_priority_fee_per_gas: U256::from(0), + paymaster_and_data: Bytes::default(), + signature: Bytes::from("handleOps".as_bytes().to_vec()), + }; + validate(user_op).expect_err("illegal call into EntryPoint"); + Ok(()) +} + +#[tokio::test] +async fn succeed_with_inner_revert() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_storage_factory_init_code(0, "".to_string()) + .await + .unwrap(); + test_user_op( + "inner-revert".to_string(), + None, + init_code, + init_func, + context.storage_factory.address, + ) + .await + .expect("succeed"); + Ok(()) +} + +#[tokio::test] +#[ignore] +async fn fail_with_inner_oog_revert() -> anyhow::Result<()> { + let context = setup().await?; + let (init_code, init_func) = create_storage_factory_init_code(0, "".to_string()) + .await + .unwrap(); + test_user_op( + "oog".to_string(), + None, + init_code, + init_func, + context.storage_factory.address, + ) + .await + .expect_err("oog"); + Ok(()) +} diff --git a/thirdparty/bundler b/thirdparty/bundler new file mode 160000 index 00000000..7db16744 --- /dev/null +++ b/thirdparty/bundler @@ -0,0 +1 @@ +Subproject commit 7db16744aad16bd2960f74b5b503c5c717aee861