From c93caf97f9cda38ea4c9079aefd2a80824b09e18 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Fri, 25 Oct 2024 14:03:54 +0100 Subject: [PATCH] fix: get it working --- crates/ffi/src/account_client.rs | 7 +- crates/yttrium/Cargo.toml | 2 +- crates/yttrium/src/account_client.rs | 24 ++- crates/yttrium/src/entry_point.rs | 30 +-- crates/yttrium/src/smart_accounts/safe.rs | 110 ++++++++-- .../yttrium/src/transaction/send/safe_test.rs | 203 +++++++++++------- .../pack_v07/hashed_paymaster_and_data.rs | 2 +- 7 files changed, 266 insertions(+), 112 deletions(-) diff --git a/crates/ffi/src/account_client.rs b/crates/ffi/src/account_client.rs index fd28956..2690853 100644 --- a/crates/ffi/src/account_client.rs +++ b/crates/ffi/src/account_client.rs @@ -162,7 +162,12 @@ impl FFIAccountClient { }); } - Ok(self.account_client.do_sign_message(signatures2).await.to_string()) + Ok(self + .account_client + .do_sign_message(signatures2) + .await + .map_err(|e| FFIError::Unknown(e.to_string()))? + .to_string()) } pub async fn send_transactions( diff --git a/crates/yttrium/Cargo.toml b/crates/yttrium/Cargo.toml index e84e8a7..46ecb8a 100644 --- a/crates/yttrium/Cargo.toml +++ b/crates/yttrium/Cargo.toml @@ -24,6 +24,7 @@ alloy = { version = "0.3.6", features = [ "eip712", ] } alloy-provider = { version = "0.3.6", features = ["erc4337-api"] } +erc6492 = { git = "https://github.com/reown-com/erc6492.git", branch = "feat/create-6492" } # foundry-block-explorers = "0.2.3" getrandom = { version = "0.2", features = ["js"] } @@ -54,7 +55,6 @@ reqwest.workspace = true [dev-dependencies] # mocking wiremock = "0.6.0" -erc6492 = { git = "https://github.com/reown-com/erc6492.git", branch = "feat/verify-message-hash" } # Networking reqwest.workspace = true diff --git a/crates/yttrium/src/account_client.rs b/crates/yttrium/src/account_client.rs index 40ebe61..6355ebb 100644 --- a/crates/yttrium/src/account_client.rs +++ b/crates/yttrium/src/account_client.rs @@ -1,9 +1,12 @@ use crate::bundler::models::user_operation_receipt::UserOperationReceipt; +use crate::bundler::pimlico::paymaster::client::PaymasterClient; use crate::bundler::{client::BundlerClient, config::BundlerConfig}; use crate::config::Config; use crate::private_key_service::PrivateKeyService; use crate::sign_service::SignService; -use crate::smart_accounts::safe::{prepare_sign, sign, PreparedSignature}; +use crate::smart_accounts::safe::{ + prepare_sign, sign, Owners, PreparedSignature, +}; use crate::transaction::send::safe_test::{ self, DoSendTransactionParams, OwnerSignature, PreparedSendTransaction, }; @@ -14,7 +17,7 @@ use crate::transaction::{send::send_transactions, Transaction}; use alloy::network::Ethereum; use alloy::primitives::{Address, Bytes, B256, U256, U64}; use alloy::providers::ReqwestProvider; -use alloy::signers::local::PrivateKeySigner; +use alloy::signers::local::{LocalSigner, PrivateKeySigner}; use std::sync::Arc; use tokio::sync::Mutex; @@ -156,7 +159,7 @@ impl AccountClient { pub async fn do_sign_message( &self, signatures: Vec, - ) -> Bytes { + ) -> eyre::Result { if !self.safe { unimplemented!( "sign_message is not supported for non-safe accounts" @@ -170,9 +173,22 @@ impl AccountClient { ); sign( - self.owner.parse::
().unwrap().into(), + Owners { + owners: vec![self.owner.parse::
().unwrap()], + threshold: 1, + }, + self.get_address() + .await + .unwrap() + .parse::
() + .unwrap() + .into(), signatures, &provider, + LocalSigner::random(), + PaymasterClient::new(BundlerConfig::new( + self.config.endpoints.paymaster.base_url.parse().unwrap(), + )), ) .await } diff --git a/crates/yttrium/src/entry_point.rs b/crates/yttrium/src/entry_point.rs index 6495fa3..c80d399 100644 --- a/crates/yttrium/src/entry_point.rs +++ b/crates/yttrium/src/entry_point.rs @@ -43,25 +43,27 @@ pub const ENTRYPOINT_ADDRESS_V07: Address = 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! { #[sol(rpc)] contract EntryPoint { + struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; + bytes callData; + bytes32 accountGasLimits; + uint256 preVerificationGas; + bytes32 gasFees; + bytes paymasterAndData; + bytes signature; + } + function getSenderAddress(bytes calldata initCode); function getNonce(address sender, uint192 key) returns (uint256 nonce); + function handleOps( + PackedUserOperation[] calldata ops, + address payable beneficiary + ); } } diff --git a/crates/yttrium/src/smart_accounts/safe.rs b/crates/yttrium/src/smart_accounts/safe.rs index 91f4ffc..f5cfadf 100644 --- a/crates/yttrium/src/smart_accounts/safe.rs +++ b/crates/yttrium/src/smart_accounts/safe.rs @@ -1,21 +1,33 @@ +use crate::bundler::pimlico::paymaster::client::PaymasterClient; +use crate::entry_point::EntryPoint::PackedUserOperation; +use crate::entry_point::{EntryPoint, ENTRYPOINT_ADDRESS_V07}; +use crate::transaction::send::safe_test::{ + encode_send_transactions, prepare_send_transactions_inner, + PreparedSendTransaction, +}; use crate::transaction::Transaction; +use crate::user_operation::hash::pack_v07::combine::combine_and_trim_first_16_bytes; +use crate::user_operation::hash::pack_v07::hashed_paymaster_and_data::get_data; use crate::{ smart_accounts::account_address::AccountAddress, transaction::send::safe_test::OwnerSignature, }; use alloy::network::Network; -use alloy::primitives::B256; +use alloy::primitives::{B256, U128}; use alloy::providers::Provider; +use alloy::signers::k256::ecdsa::SigningKey; +use alloy::signers::local::LocalSigner; +use alloy::signers::SignerSync; use alloy::transports::Transport; use alloy::{ dyn_abi::{DynSolValue, Eip712Domain}, primitives::{ address, bytes, keccak256, Address, Bytes, FixedBytes, Uint, U256, }, - providers::ReqwestProvider, sol, sol_types::{SolCall, SolValue}, }; +use erc6492::create::create_erc6492_signature; use serde::{Deserialize, Serialize}; sol! { @@ -196,12 +208,17 @@ pub fn factory_data( } } -pub async fn get_account_address( - provider: ReqwestProvider, +pub async fn get_account_address( + provider: P, owners: Owners, -) -> AccountAddress { +) -> AccountAddress +where + T: Transport + Clone, + P: Provider, + N: Network, +{ let creation_code = - SafeProxyFactory::new(SAFE_PROXY_FACTORY_ADDRESS, provider.clone()) + SafeProxyFactory::new(SAFE_PROXY_FACTORY_ADDRESS, provider) .proxyCreationCode() .call() .await @@ -303,36 +320,105 @@ pub fn prepare_sign( PreparedSignature { safe_message, domain } } +// TODO refactor to make account_address optional, if not provided it will +// determine it based on Owners TODO refactor to make owners optional, in the +// case where it already being deployed is assumed pub async fn sign( + owners: Owners, account_address: AccountAddress, signatures: Vec, provider: &P, -) -> Bytes + owner: LocalSigner, // TODO remove + paymaster_client: PaymasterClient, +) -> eyre::Result where T: Transport + Clone, P: Provider, N: Network, { if signatures.len() > 1 { - unimplemented!("multi-signature is not supported"); + unimplemented!("multi-signature is not yet supported"); } let signature = Bytes::from(signatures[0].signature.as_bytes()); + // Null validator address for regular Safe signature + let signature = (Address::ZERO, signature).abi_encode_packed().into(); + let signature = if provider .get_code_at(account_address.into()) .await .unwrap() // TODO handle error .is_empty() { - // TODO check if deployed, if so do ERC-6492 - signature + let eip1559_est = provider.estimate_eip1559_fees(None).await?; + let PreparedSendTransaction { + safe_op, + domain, + hash: _, + do_send_transaction_params, + } = prepare_send_transactions_inner( + vec![], + owners, + Some(account_address), + None, + provider, + U128::from(eip1559_est.max_fee_per_gas).to(), + U128::from(eip1559_est.max_priority_fee_per_gas).to(), + paymaster_client, + ) + .await?; + + // TODO don't do the signing here, allow wallet to do it + let user_op_signature = vec![OwnerSignature { + owner: owner.address(), + signature: owner.sign_typed_data_sync(&safe_op, &domain).unwrap(), + }]; + + let user_op = encode_send_transactions( + user_op_signature, + do_send_transaction_params, + ) + .await + .unwrap(); + + let factory_address = ENTRYPOINT_ADDRESS_V07; + let factory_data = EntryPoint::handleOpsCall { + ops: vec![PackedUserOperation { + paymasterAndData: get_data(&user_op), + sender: user_op.sender.into(), + nonce: user_op.nonce, + initCode: [ + // TODO refactor to remove unwrap() + // This code double-checks for code deployed unnecessesarly + user_op.factory.unwrap().to_vec().into(), + user_op.factory_data.unwrap(), + ] + .concat() + .into(), + callData: user_op.call_data, + accountGasLimits: combine_and_trim_first_16_bytes( + user_op.verification_gas_limit, + user_op.call_gas_limit, + ), + preVerificationGas: user_op.pre_verification_gas, + gasFees: combine_and_trim_first_16_bytes( + user_op.max_priority_fee_per_gas, + user_op.max_fee_per_gas, + ), + signature: user_op.signature, + }], + beneficiary: user_op.sender.into(), + } + .abi_encode() + .into(); + + create_erc6492_signature(factory_address, factory_data, signature) } else { signature }; - // Null validator address for regular Safe signature - (Address::ZERO, signature).abi_encode_packed().into() + Ok(signature) } #[cfg(test)] diff --git a/crates/yttrium/src/transaction/send/safe_test.rs b/crates/yttrium/src/transaction/send/safe_test.rs index 8dfac7b..51c350a 100644 --- a/crates/yttrium/src/transaction/send/safe_test.rs +++ b/crates/yttrium/src/transaction/send/safe_test.rs @@ -32,10 +32,11 @@ use alloy::{ providers::{Provider, ReqwestProvider}, signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}, sol_types::{SolCall, SolStruct}, + transports::Transport, }; +use alloy_provider::Network; use core::fmt; use serde::{Deserialize, Serialize}; -use serde_json::json; use std::ops::Not; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -165,21 +166,22 @@ pub struct DoSendTransactionParams { pub valid_until: U48, } -pub async fn prepare_send_transactions( +#[allow(clippy::too_many_arguments)] +pub async fn prepare_send_transactions_inner( execution_calldata: Vec, - owner: Address, + owners: Owners, address: Option, authorization_list: Option>, - config: Config, -) -> eyre::Result { - let pimlico_client: PimlicoBundlerClient = PimlicoBundlerClient::new( - BundlerConfig::new(config.endpoints.bundler.base_url.clone()), - ); - - let provider = ReqwestProvider::::new_http( - config.endpoints.rpc.base_url.parse()?, - ); - + provider: &P, + max_fee_per_gas: U256, + max_priority_fee_per_gas: U256, + paymaster_client: PaymasterClient, +) -> eyre::Result +where + T: Transport + Clone, + P: Provider, + N: Network, +{ let chain_id = provider.get_chain_id().await?; let chain = crate::chain::Chain::new( ChainId::new_eip155(chain_id), @@ -190,12 +192,9 @@ pub async fn prepare_send_transactions( let entry_point_config = chain.entry_point_config(); let entry_point_address = entry_point_config.address(); - let owners = Owners { owners: vec![owner], threshold: 1 }; - let factory_data_value = factory_data(owners.clone()).abi_encode(); - let contract_address = - get_account_address(provider.clone(), owners.clone()).await; + let contract_address = get_account_address(provider, owners.clone()).await; let account_address = if let Some(address) = address { address } else { contract_address }; @@ -245,10 +244,6 @@ pub async fn prepare_send_transactions( .into() }; - let gas_price = pimlico_client.estimate_user_operation_gas_price().await?; - - assert!(gas_price.fast.max_fee_per_gas > U256::from(1)); - let nonce = get_nonce(&provider, account_address, &entry_point_address).await?; @@ -261,8 +256,8 @@ pub async fn prepare_send_transactions( call_gas_limit: U256::ZERO, verification_gas_limit: U256::ZERO, pre_verification_gas: U256::ZERO, - max_fee_per_gas: gas_price.fast.max_fee_per_gas, - max_priority_fee_per_gas: gas_price.fast.max_priority_fee_per_gas, + max_fee_per_gas, + max_priority_fee_per_gas, paymaster: None, paymaster_verification_gas_limit: None, paymaster_post_op_gas_limit: None, @@ -271,32 +266,29 @@ pub async fn prepare_send_transactions( signature: DUMMY_SIGNATURE, }; - if let Some(authorization_list) = authorization_list { - let response = reqwest::Client::new() - .post(config.endpoints.paymaster.base_url.clone()) - .json(&json!({ - "jsonrpc": "2.0", - "id": 1, - "method": "eth_prepareSendUserOperation7702", - "params": [ - format!("{}:{}:{}", user_op.sender, user_op.nonce, user_op.call_data), - authorization_list, - ], - })) - .send() - .await - .unwrap(); - let success = response.status().is_success(); - println!("response: {:?}", response.text().await); + if let Some(_authorization_list) = authorization_list { + // let response = reqwest::Client::new() + // .post(config.endpoints.paymaster.base_url.clone()) + // .json(&json!({ + // "jsonrpc": "2.0", + // "id": 1, + // "method": "eth_prepareSendUserOperation7702", + // "params": [ + // format!("{}:{}:{}", user_op.sender, + // user_op.nonce, user_op.call_data), + // authorization_list, ], + // })) + // .send() + // .await + // .unwrap(); + // let success = response.status().is_success(); + // println!("response: {:?}", response.text().await); - assert!(success); + // assert!(success); + unimplemented!("need to refactor provider config to re-enable this") } let user_op = { - let paymaster_client = PaymasterClient::new(BundlerConfig::new( - config.endpoints.paymaster.base_url.clone(), - )); - let sponsor_user_op_result = paymaster_client .sponsor_user_operation_v07( &user_op.clone().into(), @@ -413,6 +405,39 @@ pub async fn prepare_send_transactions( }) } +pub async fn prepare_send_transactions( + execution_calldata: Vec, + owner: Address, + address: Option, + authorization_list: Option>, + config: Config, +) -> eyre::Result { + let provider = ReqwestProvider::::new_http( + config.endpoints.rpc.base_url.parse()?, + ); + + let paymaster_client = PaymasterClient::new(BundlerConfig::new( + config.endpoints.paymaster.base_url.clone(), + )); + let pimlico_client = PimlicoBundlerClient::new(BundlerConfig::new( + config.endpoints.bundler.base_url.clone(), + )); + let gas_price = pimlico_client.estimate_user_operation_gas_price().await?; + assert!(gas_price.fast.max_fee_per_gas > U256::from(1)); + + prepare_send_transactions_inner( + execution_calldata, + Owners { owners: vec![owner], threshold: 1 }, + address, + authorization_list, + &provider, + gas_price.fast.max_fee_per_gas, + gas_price.fast.max_priority_fee_per_gas, + paymaster_client, + ) + .await +} + pub use alloy::primitives::Address; pub use alloy::primitives::Signature; pub struct OwnerSignature { @@ -420,36 +445,43 @@ pub struct OwnerSignature { pub signature: Signature, } -pub async fn do_send_transactions( +pub async fn encode_send_transactions( signatures: Vec, DoSendTransactionParams { user_op, valid_after, valid_until, }: DoSendTransactionParams, - config: Config, -) -> eyre::Result { +) -> eyre::Result { if signatures.len() != 1 { return Err(eyre::eyre!("Only one signature is supported for now")); } - let user_op = { - // TODO sort by (lowercase) owner address not signature data - let mut signatures = signatures - .iter() - .map(|sig| sig.signature.as_bytes()) - .collect::>(); - signatures.sort(); - let signature_bytes = signatures.concat(); - - let signature = DynSolValue::Tuple(vec![ - DynSolValue::Uint(Uint::from(valid_after), 48), - DynSolValue::Uint(Uint::from(valid_until), 48), - DynSolValue::Bytes(signature_bytes), - ]) - .abi_encode_packed() - .into(); - UserOperationV07 { signature, ..user_op } - }; + + // TODO sort by (lowercase) owner address not signature data + let mut signatures = signatures + .iter() + .map(|sig| sig.signature.as_bytes()) + .collect::>(); + signatures.sort(); + let signature_bytes = signatures.concat(); + + let signature = DynSolValue::Tuple(vec![ + DynSolValue::Uint(Uint::from(valid_after), 48), + DynSolValue::Uint(Uint::from(valid_until), 48), + DynSolValue::Bytes(signature_bytes), + ]) + .abi_encode_packed() + .into(); + + Ok(UserOperationV07 { signature, ..user_op }) +} + +pub async fn do_send_transactions( + signatures: Vec, + params: DoSendTransactionParams, + config: Config, +) -> eyre::Result { + let user_op = encode_send_transactions(signatures, params).await?; let provider = ReqwestProvider::::new_http( config.endpoints.rpc.base_url.parse()?, @@ -880,12 +912,10 @@ mod tests { let owner = LocalSigner::random(); let owner_address = owner.address(); + let owners = Owners { owners: vec![owner.address()], threshold: 1 }; - let sender_address = get_account_address( - provider.clone(), - Owners { owners: vec![owner.address()], threshold: 1 }, - ) - .await; + let sender_address = + get_account_address(provider.clone(), owners.clone()).await; let receipt = send_transactions( vec![], @@ -914,11 +944,17 @@ mod tests { owner.sign_typed_data_sync(&safe_message, &domain).unwrap(); let signature = sign( + owners, sender_address, vec![OwnerSignature { owner: owner_address, signature }], &provider, + owner, + PaymasterClient::new(BundlerConfig::new( + config.endpoints.paymaster.base_url.parse().unwrap(), + )), ) - .await; + .await + .unwrap(); sol! { #[sol(rpc)] @@ -946,7 +982,6 @@ mod tests { } #[tokio::test] - #[ignore = "not implemented yet"] async fn test_sign_message_not_deployed() { let config = Config::local(); let provider = ReqwestProvider::::new_http( @@ -955,12 +990,10 @@ mod tests { let owner = LocalSigner::random(); let owner_address = owner.address(); + let owners = Owners { owners: vec![owner.address()], threshold: 1 }; - let sender_address = get_account_address( - provider.clone(), - Owners { owners: vec![owner.address()], threshold: 1 }, - ) - .await; + let sender_address = + get_account_address(provider.clone(), owners.clone()).await; assert!(provider .get_code_at(sender_address.into()) @@ -979,11 +1012,17 @@ mod tests { owner.sign_typed_data_sync(&safe_message, &domain).unwrap(); let signature = sign( + owners, sender_address, vec![OwnerSignature { owner: owner_address, signature }], &provider, + owner, + PaymasterClient::new(BundlerConfig::new( + config.endpoints.paymaster.base_url.parse().unwrap(), + )), ) - .await; + .await + .unwrap(); assert!(provider .get_code_at(sender_address.into()) @@ -1000,6 +1039,12 @@ mod tests { .await .unwrap() .is_valid()); + + assert!(provider + .get_code_at(sender_address.into()) + .await + .unwrap() + .is_empty()); } #[tokio::test] diff --git a/crates/yttrium/src/user_operation/hash/pack_v07/hashed_paymaster_and_data.rs b/crates/yttrium/src/user_operation/hash/pack_v07/hashed_paymaster_and_data.rs index 77b9e38..330ef1e 100644 --- a/crates/yttrium/src/user_operation/hash/pack_v07/hashed_paymaster_and_data.rs +++ b/crates/yttrium/src/user_operation/hash/pack_v07/hashed_paymaster_and_data.rs @@ -1,7 +1,7 @@ use crate::user_operation::UserOperationV07; use alloy::primitives::{keccak256, Bytes, B256}; -fn get_data(user_operation: &UserOperationV07) -> Bytes { +pub fn get_data(user_operation: &UserOperationV07) -> Bytes { if let Some(paymaster) = user_operation.paymaster { let address = paymaster.into_iter();