From 5e2358bc801eec5c091e76d745fc796b94f35949 Mon Sep 17 00:00:00 2001 From: Chris Smith <chris@walletconnect.com> Date: Mon, 9 Sep 2024 11:54:27 -0700 Subject: [PATCH 1/5] chore: revert to original code --- .../yttrium/src/transaction/send/safe_test.rs | 50 +++++++------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index 4bfc9ae4..a6fb86bf 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -68,7 +68,7 @@ mod tests { sol, sol_types::{SolCall, SolValue}, }; - use std::{ops::Not, str::FromStr, time::Duration}; + use std::{ops::Not, str::FromStr}; async fn send_transaction( transaction: Transaction, @@ -144,21 +144,14 @@ mod tests { let data_hex = transaction.data.strip_prefix("0x").unwrap(); let data: Bytes = Bytes::from_str(data_hex)?; - // let execution_calldata = - // vec![Execution { target: to, value, callData: data }]; - // let execution_calldata = - // Execution { target: to, value, callData: data }; let execution_calldata = - [to.to_vec(), value.to_be_bytes_vec(), data.to_vec()] - .concat() - .into(); - - // let call_type = if execution_calldata.len() > 1 { - // CallType::BatchCall - // } else { - // CallType::Call - // }; - let call_type = CallType::Call; + vec![Execution { target: to, value, callData: data }]; + + let call_type = if execution_calldata.len() > 1 { + CallType::BatchCall + } else { + CallType::Call + }; let revert_on_error = false; let selector = [0u8; 4]; let context = [0u8; 22]; @@ -179,25 +172,18 @@ mod tests { } } - let mut mode = Vec::with_capacity(32); - mode.push(call_type.as_byte()); - mode.push(if revert_on_error { 0x01 } else { 0x00 }); - mode.extend_from_slice(&[0u8; 4]); - mode.extend_from_slice(&selector); - mode.extend_from_slice(&context); - let mode = FixedBytes::from_slice(&mode); - - // let mode = DynSolValue::Tuple(vec![ - // DynSolValue::Uint(Uint::from(call_type.as_byte()), 8), - // DynSolValue::Uint(Uint::from(revert_on_error as u8), 8), - // DynSolValue::Bytes(selector.to_vec().into()), - // DynSolValue::Bytes(context.to_vec().into()), - // ]).abi_encode_packed().into(); + let mode = DynSolValue::Tuple(vec![ + DynSolValue::Uint(Uint::from(call_type.as_byte()), 8), + DynSolValue::Uint(Uint::from(revert_on_error as u8), 8), + DynSolValue::Bytes(vec![0u8; 4]), + DynSolValue::Bytes(selector.to_vec()), + DynSolValue::Bytes(context.to_vec()), + ]) + .abi_encode_packed(); let call_data = Safe7579::executeCall { - mode, - // executionCalldata: execution_calldata.abi_encode().into(), - executionCalldata: execution_calldata, + mode: FixedBytes::from_slice(&mode), + executionCalldata: execution_calldata.abi_encode_packed().into(), } .abi_encode() .into(); From 5300785253c87ccc540455f43807197dd2bb311d Mon Sep 17 00:00:00 2001 From: Chris Smith <chris@walletconnect.com> Date: Mon, 9 Sep 2024 12:08:25 -0700 Subject: [PATCH 2/5] chore: various refactors --- .../yttrium/src/transaction/send/safe_test.rs | 53 ++++--------------- 1 file changed, 11 insertions(+), 42 deletions(-) diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index a6fb86bf..42dbad35 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -61,10 +61,10 @@ mod tests { }; use alloy::{ dyn_abi::{DynSolValue, Eip712Domain}, - network::EthereumWallet, + network::Ethereum, primitives::{Address, Bytes, FixedBytes, Uint, U128, U256}, - providers::ProviderBuilder, - signers::{local::LocalSigner, SignerSync}, + providers::{Provider, ReqwestProvider}, + signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, sol, sol_types::{SolCall, SolValue}, }; @@ -72,6 +72,7 @@ mod tests { async fn send_transaction( transaction: Transaction, + owner: LocalSigner<SigningKey>, ) -> eyre::Result<String> { let config = crate::config::Config::local(); @@ -94,40 +95,18 @@ mod tests { let rpc_url = config.endpoints.rpc.base_url; - // Create a provider - - let alloy_signer = LocalSigner::random(); - let ethereum_wallet = EthereumWallet::new(alloy_signer.clone()); - let rpc_url: reqwest::Url = rpc_url.parse()?; - let provider = ProviderBuilder::new() - .with_recommended_fillers() - .wallet(ethereum_wallet.clone()) - .on_http(rpc_url); + let provider = ReqwestProvider::<Ethereum>::new_http(rpc_url); let safe_factory_address_primitives: Address = SAFE_PROXY_FACTORY_ADDRESS; let safe_factory_address = FactoryAddress::new(safe_factory_address_primitives); - let owner = ethereum_wallet.clone().default_signer(); - let owner_address = owner.address(); - let owners = Owners { owners: vec![owner_address], threshold: 1 }; + let owners = Owners { owners: vec![owner.address()], threshold: 1 }; let factory_data_value = factory_data(owners.clone()).abi_encode(); - let factory_data_value_hex = hex::encode(factory_data_value.clone()); - - let factory_data_value_hex_prefixed = - format!("0x{}", factory_data_value_hex); - - println!( - "Generated factory_data: {:?}", - factory_data_value_hex_prefixed.clone() - ); - - // 5. Calculate the sender address - let sender_address = get_sender_address_v07( &provider, safe_factory_address.into(), @@ -136,8 +115,6 @@ mod tests { ) .await?; - println!("Calculated sender address: {:?}", sender_address); - let to: Address = transaction.to.parse()?; let value: alloy::primitives::Uint<256, 4> = transaction.value.parse()?; @@ -188,7 +165,7 @@ mod tests { .abi_encode() .into(); - let deployed = false; + let deployed = provider.get_code_at(sender_address).await?.len() > 0; // permissionless: signerToSafeSmartAccount -> encodeCallData let call_data = if deployed { call_data @@ -218,15 +195,11 @@ mod tests { .into() }; - // Fill out remaining UserOperation values - let gas_price = pimlico_client.estimate_user_operation_gas_price().await?; assert!(gas_price.fast.max_fee_per_gas > U256::from(1)); - println!("Gas price: {:?}", gas_price); - let nonce = get_nonce( &provider, &SimpleAccountAddress::new(sender_address), @@ -268,8 +241,6 @@ mod tests { ) .await?; - println!("sponsor_user_op_result: {:?}", sponsor_user_op_result); - let sponsored_user_op = { let s = sponsor_user_op_result.clone(); let mut op = user_op.clone(); @@ -287,10 +258,6 @@ mod tests { op }; - println!("Received paymaster sponsor result: {:?}", sponsored_user_op); - - // Sign the UserOperation - let valid_after = 0; let valid_until = 0; @@ -395,7 +362,7 @@ mod tests { }; // TODO loop per-owner - let signature = alloy_signer.sign_typed_data_sync( + let signature = owner.sign_typed_data_sync( &message, &Eip712Domain { chain_id: Some(Uint::from(chain_id)), @@ -449,7 +416,9 @@ mod tests { async fn test_send_transaction() -> eyre::Result<()> { let transaction = Transaction::mock(); - let transaction_hash = send_transaction(transaction).await?; + let owner = LocalSigner::random(); + + let transaction_hash = send_transaction(transaction, owner).await?; println!("Transaction sent: {}", transaction_hash); From 12ca2ab318bf8ecc8aeac095b02fc73e210ae4a7 Mon Sep 17 00:00:00 2001 From: Chris Smith <chris@walletconnect.com> Date: Mon, 9 Sep 2024 15:17:44 -0700 Subject: [PATCH 3/5] chore: multi-transaction send balance tests --- .../src/bundler/pimlico/paymaster/models.rs | 2 + .../src/entry_point/get_sender_address.rs | 4 + crates/yttrium/src/smart_accounts/safe.rs | 40 +++++++- .../yttrium/src/transaction/send/safe_test.rs | 92 ++++++++++++++----- 4 files changed, 111 insertions(+), 27 deletions(-) diff --git a/crates/yttrium/src/bundler/pimlico/paymaster/models.rs b/crates/yttrium/src/bundler/pimlico/paymaster/models.rs index bb1f8b9a..3c044e7c 100644 --- a/crates/yttrium/src/bundler/pimlico/paymaster/models.rs +++ b/crates/yttrium/src/bundler/pimlico/paymaster/models.rs @@ -17,7 +17,9 @@ use serde::{Deserialize, Serialize}; pub struct UserOperationPreSponsorshipV07 { pub sender: Address, pub nonce: U256, + #[serde(skip_serializing_if = "Option::is_none")] pub factory: Option<Address>, + #[serde(skip_serializing_if = "Option::is_none")] pub factory_data: Option<Bytes>, pub call_data: Bytes, pub call_gas_limit: U256, diff --git a/crates/yttrium/src/entry_point/get_sender_address.rs b/crates/yttrium/src/entry_point/get_sender_address.rs index a33e3c4c..14d38aa0 100644 --- a/crates/yttrium/src/entry_point/get_sender_address.rs +++ b/crates/yttrium/src/entry_point/get_sender_address.rs @@ -60,6 +60,10 @@ where let call_builder = instance.getSenderAddress(init_code); + // Note: you may need to static call getSenderAddress() not call() as per + // the spec. Leaving as-is for now. + // let call = call_builder.call_raw().await; + let call: Result< crate::entry_point::EntryPoint::getSenderAddressReturn, ContractError, diff --git a/crates/yttrium/src/smart_accounts/safe.rs b/crates/yttrium/src/smart_accounts/safe.rs index 16ee34ed..0bce5907 100644 --- a/crates/yttrium/src/smart_accounts/safe.rs +++ b/crates/yttrium/src/smart_accounts/safe.rs @@ -1,8 +1,9 @@ use alloy::{ dyn_abi::DynSolValue, - primitives::{address, keccak256, Address, Bytes, Uint, U256}, + primitives::{address, keccak256, Address, Bytes, Uint, U160, U256}, + providers::ReqwestProvider, sol, - sol_types::SolCall, + sol_types::{SolCall, SolValue}, }; sol!( @@ -158,3 +159,38 @@ pub fn factory_data( saltNonce: U256::ZERO, } } + +pub async fn get_account_address( + provider: ReqwestProvider, + owners: Owners, +) -> Address { + let creation_code = + SafeProxyFactory::new(SAFE_PROXY_FACTORY_ADDRESS, provider.clone()) + .proxyCreationCode() + .call() + .await + .unwrap() + ._0; + let initializer = get_initializer_code(owners.clone()); + let deployment_code = DynSolValue::Tuple(vec![ + DynSolValue::Bytes(creation_code.to_vec()), + DynSolValue::Uint( + Uint::<256, 4>::from( + U160::try_from(SAFE_ERC_7579_LAUNCHPAD_ADDRESS).unwrap(), + ), + 256, + ), + ]) + .abi_encode_packed(); + let salt = keccak256( + DynSolValue::Tuple(vec![ + DynSolValue::FixedBytes( + keccak256(initializer.abi_encode_packed()), + 32, + ), + DynSolValue::Uint(Uint::from(0), 256), + ]) + .abi_encode_packed(), + ); + SAFE_PROXY_FACTORY_ADDRESS.create2(salt, keccak256(deployment_code)) +} diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index 42dbad35..75db4d6c 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -45,25 +45,26 @@ mod tests { paymaster::client::PaymasterClient, }, }, - entry_point::get_sender_address::get_sender_address_v07, + config::Config, smart_accounts::{ nonce::get_nonce, safe::{ - factory_data, Execution, Owners, Safe7579, Safe7579Launchpad, - SAFE_4337_MODULE_ADDRESS, SAFE_ERC_7579_LAUNCHPAD_ADDRESS, - SAFE_PROXY_FACTORY_ADDRESS, + factory_data, get_account_address, Execution, Owners, Safe7579, + Safe7579Launchpad, SAFE_4337_MODULE_ADDRESS, + SAFE_ERC_7579_LAUNCHPAD_ADDRESS, SAFE_PROXY_FACTORY_ADDRESS, SEPOLIA_SAFE_ERC_7579_SINGLETON_ADDRESS, }, simple_account::{factory::FactoryAddress, SimpleAccountAddress}, }, - transaction::Transaction, user_operation::UserOperationV07, }; use alloy::{ dyn_abi::{DynSolValue, Eip712Domain}, network::Ethereum, primitives::{Address, Bytes, FixedBytes, Uint, U128, U256}, - providers::{Provider, ReqwestProvider}, + providers::{ + ext::AnvilApi, PendingTransactionConfig, Provider, ReqwestProvider, + }, signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, sol, sol_types::{SolCall, SolValue}, @@ -71,7 +72,7 @@ mod tests { use std::{ops::Not, str::FromStr}; async fn send_transaction( - transaction: Transaction, + execution_calldata: Vec<Execution>, owner: LocalSigner<SigningKey>, ) -> eyre::Result<String> { let config = crate::config::Config::local(); @@ -107,22 +108,8 @@ mod tests { let factory_data_value = factory_data(owners.clone()).abi_encode(); - let sender_address = get_sender_address_v07( - &provider, - safe_factory_address.into(), - factory_data_value.clone().into(), - entry_point_address, - ) - .await?; - - let to: Address = transaction.to.parse()?; - let value: alloy::primitives::Uint<256, 4> = - transaction.value.parse()?; - let data_hex = transaction.data.strip_prefix("0x").unwrap(); - let data: Bytes = Bytes::from_str(data_hex)?; - - let execution_calldata = - vec![Execution { target: to, value, callData: data }]; + let sender_address = + get_account_address(provider.clone(), owners.clone()).await; let call_type = if execution_calldata.len() > 1 { CallType::BatchCall @@ -358,7 +345,7 @@ mod tests { let verifying_contract = if erc7579_launchpad_address && !deployed { sponsored_user_op.sender } else { - SAFE_ERC_7579_LAUNCHPAD_ADDRESS + SAFE_4337_MODULE_ADDRESS }; // TODO loop per-owner @@ -409,19 +396,74 @@ mod tests { tx_hash ); + // Some extra calls to wait for/get the actual transaction. But these + // aren't required since eth_getUserOperationReceipt already waits + // let tx_hash = FixedBytes::from_slice( + // &hex::decode(tx_hash.strip_prefix("0x").unwrap()).unwrap(), + // ); + // let pending_txn = provider + // .watch_pending_transaction(PendingTransactionConfig::new(tx_hash)) + // .await?; + // pending_txn.await.unwrap(); + // let transaction = provider.get_transaction_by_hash(tx_hash).await?; + // println!("Transaction included: {:?}", transaction); + // let transaction_receipt = + // provider.get_transaction_receipt(tx_hash).await?; + // println!("Transaction receipt: {:?}", transaction_receipt); + Ok(user_operation_hash) } #[tokio::test] async fn test_send_transaction() -> eyre::Result<()> { - let transaction = Transaction::mock(); + let rpc_url = Config::local().endpoints.rpc.base_url; + let rpc_url: reqwest::Url = rpc_url.parse()?; + let provider = ReqwestProvider::<Ethereum>::new_http(rpc_url); + + let destination = LocalSigner::random(); + let balance = provider.get_balance(destination.address()).await?; + assert_eq!(balance, Uint::from(0)); let owner = LocalSigner::random(); + let sender_address = get_account_address( + provider.clone(), + Owners { owners: vec![owner.address()], threshold: 1 }, + ) + .await; + + provider.anvil_set_balance(sender_address, U256::from(100)).await?; + let transaction = vec![Execution { + target: destination.address(), + value: Uint::from(1), + callData: Bytes::new(), + }]; + + let transaction_hash = + send_transaction(transaction, owner.clone()).await?; + + println!("Transaction sent: {}", transaction_hash); + + let balance = provider.get_balance(destination.address()).await?; + assert_eq!(balance, Uint::from(1)); + + provider.anvil_set_balance(sender_address, U256::from(100)).await?; + let transaction = vec![Execution { + target: destination.address(), + value: Uint::from(1), + callData: Bytes::new(), + }]; let transaction_hash = send_transaction(transaction, owner).await?; println!("Transaction sent: {}", transaction_hash); + let balance = provider.get_balance(destination.address()).await?; + assert_eq!(balance, Uint::from(2)); + Ok(()) } + + // TODO test/fix: if invalid call data (e.g. sending balance that you don't + // have), the account will still be deployed but the transfer won't happen. + // Why can't we detect this? } From d3a0f7e0d9a853699f1ec2e225348ce14894630f Mon Sep 17 00:00:00 2001 From: Chris Smith <chris@walletconnect.com> Date: Mon, 9 Sep 2024 15:33:41 -0700 Subject: [PATCH 4/5] chore: fix clippy --- crates/yttrium/src/smart_accounts/safe.rs | 10 ++++------ crates/yttrium/src/transaction/send/safe_test.rs | 4 +--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/yttrium/src/smart_accounts/safe.rs b/crates/yttrium/src/smart_accounts/safe.rs index 0bce5907..f4fe1f19 100644 --- a/crates/yttrium/src/smart_accounts/safe.rs +++ b/crates/yttrium/src/smart_accounts/safe.rs @@ -1,6 +1,6 @@ use alloy::{ dyn_abi::DynSolValue, - primitives::{address, keccak256, Address, Bytes, Uint, U160, U256}, + primitives::{address, keccak256, Address, Bytes, Uint, U256}, providers::ReqwestProvider, sol, sol_types::{SolCall, SolValue}, @@ -174,11 +174,9 @@ pub async fn get_account_address( let initializer = get_initializer_code(owners.clone()); let deployment_code = DynSolValue::Tuple(vec![ DynSolValue::Bytes(creation_code.to_vec()), - DynSolValue::Uint( - Uint::<256, 4>::from( - U160::try_from(SAFE_ERC_7579_LAUNCHPAD_ADDRESS).unwrap(), - ), - 256, + DynSolValue::FixedBytes( + SAFE_ERC_7579_LAUNCHPAD_ADDRESS.into_word(), + 32, ), ]) .abi_encode_packed(); diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index 75db4d6c..01a67773 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -62,9 +62,7 @@ mod tests { dyn_abi::{DynSolValue, Eip712Domain}, network::Ethereum, primitives::{Address, Bytes, FixedBytes, Uint, U128, U256}, - providers::{ - ext::AnvilApi, PendingTransactionConfig, Provider, ReqwestProvider, - }, + providers::{ext::AnvilApi, Provider, ReqwestProvider}, signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, sol, sol_types::{SolCall, SolValue}, From 81e70e0e68718600324fdeef0bd30b8c6eb6e1e3 Mon Sep 17 00:00:00 2001 From: Chris Smith <1979423+chris13524@users.noreply.github.com> Date: Tue, 10 Sep 2024 03:15:56 -0700 Subject: [PATCH 5/5] chore: upgrade alloy (#16) --- crates/yttrium/Cargo.toml | 3 +-- crates/yttrium/src/entry_point.rs | 14 ++++++++++++++ crates/yttrium/src/smart_accounts/nonce.rs | 4 ++-- crates/yttrium/src/transaction/send/safe_test.rs | 8 +++++--- .../src/transaction/send/simple_account_test.rs | 2 +- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/yttrium/Cargo.toml b/crates/yttrium/Cargo.toml index ade879b7..c0fd8305 100644 --- a/crates/yttrium/Cargo.toml +++ b/crates/yttrium/Cargo.toml @@ -6,7 +6,7 @@ rust-version.workspace = true [dependencies] # Ethereum -alloy = { git = "https://github.com/alloy-rs/alloy", rev = "b000e16", features = [ +alloy = { git = "https://github.com/alloy-rs/alloy", version = "0.3.2", features = [ "contract", "network", "providers", @@ -48,5 +48,4 @@ wiremock = "0.6.0" reqwest.workspace = true [build-dependencies] -alloy-primitives = { version = "0.7.0" } serde_json = "1" diff --git a/crates/yttrium/src/entry_point.rs b/crates/yttrium/src/entry_point.rs index fd7f56c6..54326c09 100644 --- a/crates/yttrium/src/entry_point.rs +++ b/crates/yttrium/src/entry_point.rs @@ -40,6 +40,20 @@ pub const ENTRYPOINT_ADDRESS_V07: &str = pub const ENTRYPOINT_V06_TYPE: &str = "v0.6"; pub const ENTRYPOINT_V07_TYPE: &str = "v0.7"; +sol! ( + struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; + bytes callData; + bytes32 accountGasLimits; + uint256 preVerificationGas; + bytes32 gasFees; + bytes paymasterAndData; + bytes signature; + } +); + sol!( #[allow(missing_docs)] #[sol(rpc)] diff --git a/crates/yttrium/src/smart_accounts/nonce.rs b/crates/yttrium/src/smart_accounts/nonce.rs index f32ee1d8..d0864688 100644 --- a/crates/yttrium/src/smart_accounts/nonce.rs +++ b/crates/yttrium/src/smart_accounts/nonce.rs @@ -1,4 +1,4 @@ -use alloy::primitives::U256; +use alloy::primitives::aliases::U192; pub async fn get_nonce<P, T, N>( provider: &P, @@ -14,7 +14,7 @@ where entry_point_address.to_address(), provider, ); - let key = U256::ZERO; + let key = U192::ZERO; let get_nonce_call = entry_point_instance.getNonce(address.to_address(), key).call().await?; diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index 01a67773..b5090fa0 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -61,7 +61,9 @@ mod tests { use alloy::{ dyn_abi::{DynSolValue, Eip712Domain}, network::Ethereum, - primitives::{Address, Bytes, FixedBytes, Uint, U128, U256}, + primitives::{ + aliases::U48, Address, Bytes, FixedBytes, Uint, U128, U256, + }, providers::{ext::AnvilApi, Provider, ReqwestProvider}, signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, sol, @@ -243,8 +245,8 @@ mod tests { op }; - let valid_after = 0; - let valid_until = 0; + let valid_after = U48::from(0); + let valid_until = U48::from(0); sol!( struct SafeOp { diff --git a/crates/yttrium/src/transaction/send/simple_account_test.rs b/crates/yttrium/src/transaction/send/simple_account_test.rs index bbaa7e74..2a595be3 100644 --- a/crates/yttrium/src/transaction/send/simple_account_test.rs +++ b/crates/yttrium/src/transaction/send/simple_account_test.rs @@ -63,7 +63,7 @@ mod tests { providers::ProviderBuilder, signers::local::LocalSigner, }; - use std::{str::FromStr, time::Duration}; + use std::str::FromStr; async fn send_transaction( transaction: Transaction,