Skip to content

Commit

Permalink
chore: multi-transaction send balance tests
Browse files Browse the repository at this point in the history
  • Loading branch information
chris13524 committed Sep 9, 2024
1 parent 5300785 commit 12ca2ab
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 27 deletions.
2 changes: 2 additions & 0 deletions crates/yttrium/src/bundler/pimlico/paymaster/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions crates/yttrium/src/entry_point/get_sender_address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
40 changes: 38 additions & 2 deletions crates/yttrium/src/smart_accounts/safe.rs
Original file line number Diff line number Diff line change
@@ -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!(
Expand Down Expand Up @@ -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))
}
92 changes: 67 additions & 25 deletions crates/yttrium/src/transaction/send/safe_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,34 @@ 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},
};
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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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?
}

0 comments on commit 12ca2ab

Please sign in to comment.