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/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
, + #[serde(skip_serializing_if = "Option::is_none")] pub factory_data: Option, pub call_data: Bytes, pub call_gas_limit: U256, 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/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/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( 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/smart_accounts/safe.rs b/crates/yttrium/src/smart_accounts/safe.rs index 16ee34ed..f4fe1f19 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}, + providers::ReqwestProvider, sol, - sol_types::SolCall, + sol_types::{SolCall, SolValue}, }; sol!( @@ -158,3 +159,36 @@ 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::FixedBytes( + SAFE_ERC_7579_LAUNCHPAD_ADDRESS.into_word(), + 32, + ), + ]) + .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 4bfc9ae4..b5090fa0 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -45,33 +45,35 @@ 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::EthereumWallet, - primitives::{Address, Bytes, FixedBytes, Uint, U128, U256}, - providers::ProviderBuilder, - signers::{local::LocalSigner, SignerSync}, + network::Ethereum, + primitives::{ + aliases::U48, Address, Bytes, FixedBytes, Uint, U128, U256, + }, + providers::{ext::AnvilApi, Provider, ReqwestProvider}, + signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, 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, + execution_calldata: Vec, + owner: LocalSigner, ) -> eyre::Result { let config = crate::config::Config::local(); @@ -94,71 +96,26 @@ 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::::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 sender_address = + get_account_address(provider.clone(), owners.clone()).await; - 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(), - factory_data_value.clone().into(), - entry_point_address, - ) - .await?; - - println!("Calculated sender address: {:?}", sender_address); - - 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 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; + 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,30 +136,23 @@ 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(); - let deployed = false; + let deployed = provider.get_code_at(sender_address).await?.len() > 0; // permissionless: signerToSafeSmartAccount -> encodeCallData let call_data = if deployed { call_data @@ -232,15 +182,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), @@ -282,8 +228,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(); @@ -301,12 +245,8 @@ mod tests { op }; - println!("Received paymaster sponsor result: {:?}", sponsored_user_op); - - // Sign the UserOperation - - let valid_after = 0; - let valid_until = 0; + let valid_after = U48::from(0); + let valid_until = U48::from(0); sol!( struct SafeOp { @@ -405,11 +345,11 @@ 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 - let signature = alloy_signer.sign_typed_data_sync( + let signature = owner.sign_typed_data_sync( &message, &Eip712Domain { chain_id: Some(Uint::from(chain_id)), @@ -456,17 +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::::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; - let transaction_hash = send_transaction(transaction).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? } 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,