diff --git a/bedrock/src/migration/controller.rs b/bedrock/src/migration/controller.rs index 666e3054..761ba511 100644 --- a/bedrock/src/migration/controller.rs +++ b/bedrock/src/migration/controller.rs @@ -1,6 +1,8 @@ use crate::migration::error::MigrationError; use crate::migration::processor::{MigrationProcessor, ProcessorResult}; +use crate::migration::processors::enable_4337_module_processor::Enable4337ModuleProcessor; use crate::migration::processors::permit2_approval_processor::Permit2ApprovalProcessor; +use crate::migration::processors::safe_upgrade_processor::SafeUpgradeProcessor; use crate::migration::state::{MigrationRecord, MigrationStatus}; use crate::primitives::key_value_store::{DeviceKeyValueStore, KeyValueStoreError}; use crate::smart_account::SafeSmartAccount; @@ -69,6 +71,8 @@ impl MigrationController { /// Create a new [`MigrationController`] with default processors and optional additional ones. /// /// Default processors (loaded automatically): + /// - [`SafeUpgradeProcessor`]: Upgrades Safe wallets from v1.3.0 to v1.4.1 + /// - [`Enable4337ModuleProcessor`]: Enables the Safe4337Module if not already enabled /// - [`Permit2ApprovalProcessor`]: Ensures max ERC20 approval to Permit2 on `WorldChain` /// /// Additional processors passed via `additional_processors` are appended after the defaults. @@ -164,7 +168,11 @@ impl MigrationController { fn default_processors( safe_account: Arc, ) -> Vec> { - vec![Arc::new(Permit2ApprovalProcessor::new(safe_account))] + vec![ + Arc::new(SafeUpgradeProcessor::new(Arc::clone(&safe_account))), + Arc::new(Enable4337ModuleProcessor::new(Arc::clone(&safe_account))), + Arc::new(Permit2ApprovalProcessor::new(safe_account)), + ] } /// Create a controller with processors injected in diff --git a/bedrock/src/migration/mod.rs b/bedrock/src/migration/mod.rs index c9859130..f88342ca 100644 --- a/bedrock/src/migration/mod.rs +++ b/bedrock/src/migration/mod.rs @@ -76,6 +76,9 @@ mod state; /// Example processors showing how to implement migrations pub mod processors; +/// Shared utilities for migration processors +pub mod utils; + // Public API exports pub use controller::{MigrationController, MigrationRunSummary}; pub use error::MigrationError; diff --git a/bedrock/src/migration/processors/enable_4337_module_processor.rs b/bedrock/src/migration/processors/enable_4337_module_processor.rs new file mode 100644 index 00000000..f037189a --- /dev/null +++ b/bedrock/src/migration/processors/enable_4337_module_processor.rs @@ -0,0 +1,89 @@ +use async_trait::async_trait; +use log::info; +use std::sync::Arc; + +use crate::migration::error::MigrationError; +use crate::migration::processor::{MigrationProcessor, ProcessorResult}; +use crate::migration::utils::poll_for_receipt; +use crate::primitives::Network; +use crate::smart_account::{Is4337Encodable, SafeSmartAccount}; +use crate::transactions::contracts::gnosis_safe::{ + GnosisSafe, SafeEnableModule, SAFE_4337_MODULE, +}; +use crate::transactions::rpc::{get_rpc_client, RpcProviderName}; + +/// Migration processor that checks if the Safe4337Module is enabled on the wallet +/// and enables it if not. +/// +/// The 4337 module is required for the wallet to process ERC-4337 UserOperations. +/// Some wallets may have been deployed without it or had it removed. This processor +/// ensures the module is enabled by checking on-chain state and calling `enableModule` +/// if needed. +pub struct Enable4337ModuleProcessor { + safe_account: Arc, +} + +impl Enable4337ModuleProcessor { + /// Creates a new `Enable4337ModuleProcessor` with the given Safe smart account. + #[must_use] + pub fn new(safe_account: Arc) -> Self { + Self { safe_account } + } +} + +#[async_trait] +impl MigrationProcessor for Enable4337ModuleProcessor { + fn migration_id(&self) -> String { + "wallet.safe.enable_4337_module.v1".to_string() + } + + async fn is_applicable(&self) -> Result { + let rpc_client = get_rpc_client() + .map_err(|e| MigrationError::InvalidOperation(e.to_string()))?; + + let safe = GnosisSafe::new(self.safe_account.wallet_address); + let is_enabled = safe + .is_module_enabled(&rpc_client, SAFE_4337_MODULE) + .await + .map_err(|e| MigrationError::InvalidOperation(e.to_string()))?; + + if is_enabled { + info!("Safe4337Module is already enabled"); + } else { + info!("Safe4337Module is NOT enabled, migration needed"); + } + + Ok(!is_enabled) + } + + async fn execute(&self) -> Result { + let enable_module = + SafeEnableModule::new(self.safe_account.wallet_address, SAFE_4337_MODULE); + + let user_op_hash = match enable_module + .sign_and_execute( + &self.safe_account, + Network::WorldChain, + None, + None, + RpcProviderName::Any, + ) + .await + { + Ok(hash) => { + info!("Submitted enableModule for 4337 module, userOpHash: {hash:?}"); + hash + } + Err(e) => { + return Ok(ProcessorResult::Retryable { + error_code: "RPC_ERROR".to_string(), + error_message: format!( + "Failed to submit enableModule transaction: {e}" + ), + }); + } + }; + + poll_for_receipt(user_op_hash, "enableModule").await + } +} diff --git a/bedrock/src/migration/processors/mod.rs b/bedrock/src/migration/processors/mod.rs index e81cc86e..67fd3743 100644 --- a/bedrock/src/migration/processors/mod.rs +++ b/bedrock/src/migration/processors/mod.rs @@ -4,5 +4,11 @@ /// that can be used as templates for actual migrations. mod example_processor; +/// Processor that checks if the Safe4337Module is enabled and enables it if not. +pub mod enable_4337_module_processor; + /// Processor that ensures max ERC20 approval to Permit2 on `WorldChain` for supported tokens. pub mod permit2_approval_processor; + +/// Processor that upgrades Safe wallets from v1.3.0 to v1.4.1. +pub mod safe_upgrade_processor; diff --git a/bedrock/src/migration/processors/permit2_approval_processor.rs b/bedrock/src/migration/processors/permit2_approval_processor.rs index fd91f043..07f4ecef 100644 --- a/bedrock/src/migration/processors/permit2_approval_processor.rs +++ b/bedrock/src/migration/processors/permit2_approval_processor.rs @@ -6,6 +6,7 @@ use tokio::sync::Mutex; use crate::migration::error::MigrationError; use crate::migration::processor::{MigrationProcessor, ProcessorResult}; +use crate::migration::utils::poll_for_receipt; use crate::primitives::Network; use crate::smart_account::{Is4337Encodable, SafeSmartAccount}; use crate::transactions::contracts::erc20::Erc20; @@ -151,54 +152,6 @@ impl MigrationProcessor for Permit2ApprovalProcessor { } }; - // Wait for the user operation to be mined before marking as success. - let rpc_client = get_rpc_client() - .map_err(|e| MigrationError::InvalidOperation(e.to_string()))?; - - let user_op_hash_hex = format!("{user_op_hash:#x}"); - let delay_ms = 4000u64; - - for attempt in 0..5 { - let response = rpc_client - .wa_get_user_operation_receipt(Network::WorldChain, &user_op_hash_hex) - .await - .map_err(|e| MigrationError::InvalidOperation(e.to_string()))?; - - match response.status.as_str() { - "mined_success" => { - info!( - "Permit2 approvals mined successfully for {names:?}, txHash: {:?}", - response.transaction_hash - ); - return Ok(ProcessorResult::Success); - } - "mined_revert" | "error" => { - return Ok(ProcessorResult::Retryable { - error_code: "MINED_REVERT".to_string(), - error_message: format!( - "Permit2 approval transaction failed for {names:?}, txHash: {:?}", - response.transaction_hash - ), - }); - } - _ => { - // Still pending — keep polling unless this is the last attempt - if attempt < 4 { - tokio::time::sleep(tokio::time::Duration::from_millis( - delay_ms, - )) - .await; - } - } - } - } - - // Still pending after all polling attempts — retry the whole migration later - Ok(ProcessorResult::Retryable { - error_code: "PENDING_TIMEOUT".to_string(), - error_message: format!( - "Permit2 approval for {names:?} still pending after polling, will retry" - ), - }) + poll_for_receipt(user_op_hash, &format!("Permit2 approval for {names:?}")).await } } diff --git a/bedrock/src/migration/processors/safe_upgrade_processor.rs b/bedrock/src/migration/processors/safe_upgrade_processor.rs new file mode 100644 index 00000000..c4c1ced1 --- /dev/null +++ b/bedrock/src/migration/processors/safe_upgrade_processor.rs @@ -0,0 +1,86 @@ +use async_trait::async_trait; +use log::info; +use std::sync::Arc; + +use crate::migration::error::MigrationError; +use crate::migration::processor::{MigrationProcessor, ProcessorResult}; +use crate::migration::utils::poll_for_receipt; +use crate::primitives::Network; +use crate::smart_account::{Is4337Encodable, SafeSmartAccount}; +use crate::transactions::contracts::gnosis_safe::{ + GnosisSafe, SafeWalletVersionUpgrade, SAFE_VERSION_130, +}; +use crate::transactions::rpc::{get_rpc_client, RpcProviderName}; + +/// Migration processor that checks the Gnosis Safe wallet version and upgrades +/// from v1.3.0 to v1.4.1 if needed. +/// +/// Uses a `delegatecall` to the `WC_MIGRATION_WALLET_UPGRADE` contract which +/// handles updating the Safe proxy's singleton address. +pub struct SafeUpgradeProcessor { + safe_account: Arc, +} + +impl SafeUpgradeProcessor { + /// Creates a new `SafeUpgradeProcessor` with the given Safe smart account. + #[must_use] + pub fn new(safe_account: Arc) -> Self { + Self { safe_account } + } +} + +#[async_trait] +impl MigrationProcessor for SafeUpgradeProcessor { + fn migration_id(&self) -> String { + "wallet.safe.upgrade.v1".to_string() + } + + async fn is_applicable(&self) -> Result { + let rpc_client = get_rpc_client() + .map_err(|e| MigrationError::InvalidOperation(e.to_string()))?; + + let safe = GnosisSafe::new(self.safe_account.wallet_address); + let version = safe + .fetch_version(&rpc_client) + .await + .map_err(|e| MigrationError::InvalidOperation(e.to_string()))?; + + if version == SAFE_VERSION_130 { + info!("Safe is on v1.3.0, upgrade to v1.4.1 needed"); + Ok(true) + } else { + info!("Safe is on v{version}, no upgrade needed"); + Ok(false) + } + } + + async fn execute(&self) -> Result { + let upgrade = SafeWalletVersionUpgrade; + + let user_op_hash = match upgrade + .sign_and_execute( + &self.safe_account, + Network::WorldChain, + None, + None, + RpcProviderName::Any, + ) + .await + { + Ok(hash) => { + info!("Submitted Safe upgrade transaction, userOpHash: {hash:?}"); + hash + } + Err(e) => { + return Ok(ProcessorResult::Retryable { + error_code: "RPC_ERROR".to_string(), + error_message: format!( + "Failed to submit Safe upgrade transaction: {e}" + ), + }); + } + }; + + poll_for_receipt(user_op_hash, "Safe upgrade").await + } +} diff --git a/bedrock/src/migration/utils/mod.rs b/bedrock/src/migration/utils/mod.rs new file mode 100644 index 00000000..0748745a --- /dev/null +++ b/bedrock/src/migration/utils/mod.rs @@ -0,0 +1,79 @@ +//! Shared utilities for migration processors. + +use alloy::primitives::FixedBytes; +use log::info; + +use crate::migration::error::MigrationError; +use crate::migration::processor::ProcessorResult; +use crate::primitives::Network; +use crate::transactions::rpc::get_rpc_client; + +/// Number of polling attempts before giving up. +const POLL_ATTEMPTS: u32 = 5; + +/// Delay between polling attempts in milliseconds. +const POLL_DELAY_MS: u64 = 4000; + +/// Polls `wa_getUserOperationReceipt` on WorldChain until the operation is mined +/// or the maximum number of attempts is exhausted. +/// +/// # Arguments +/// * `user_op_hash` - The hash returned by `sign_and_execute`. +/// * `label` - A human-readable label for log messages (e.g. `"enableModule"`). +/// +/// # Returns +/// - `ProcessorResult::Success` if the operation was mined successfully. +/// - `ProcessorResult::Retryable` if the operation reverted, errored, or is still pending. +/// +/// # Errors +/// Returns a `MigrationError` if the RPC client cannot be obtained or an RPC call fails. +pub async fn poll_for_receipt( + user_op_hash: FixedBytes<32>, + label: &str, +) -> Result { + let rpc_client = get_rpc_client() + .map_err(|e| MigrationError::InvalidOperation(e.to_string()))?; + + let user_op_hash_hex = format!("{user_op_hash:#x}"); + + for attempt in 0..POLL_ATTEMPTS { + let response = rpc_client + .wa_get_user_operation_receipt(Network::WorldChain, &user_op_hash_hex) + .await + .map_err(|e| MigrationError::InvalidOperation(e.to_string()))?; + + match response.status.as_str() { + "mined_success" => { + info!( + "{label} mined successfully, txHash: {:?}", + response.transaction_hash + ); + return Ok(ProcessorResult::Success); + } + "mined_revert" | "error" => { + return Ok(ProcessorResult::Retryable { + error_code: "MINED_REVERT".to_string(), + error_message: format!( + "{label} transaction reverted, txHash: {:?}", + response.transaction_hash + ), + }); + } + _ => { + if attempt < POLL_ATTEMPTS - 1 { + tokio::time::sleep(tokio::time::Duration::from_millis( + POLL_DELAY_MS, + )) + .await; + } + } + } + } + + Ok(ProcessorResult::Retryable { + error_code: "PENDING_TIMEOUT".to_string(), + error_message: format!( + "{label} still pending after polling, will retry" + ), + }) +} diff --git a/bedrock/src/smart_account/nonce.rs b/bedrock/src/smart_account/nonce.rs index ca2fccee..3721a10d 100644 --- a/bedrock/src/smart_account/nonce.rs +++ b/bedrock/src/smart_account/nonce.rs @@ -48,6 +48,10 @@ pub enum TransactionTypeId { WLDVaultMigration = 139, /// USD Vault migration to ERC-4626 vault USDVaultMigration = 140, + /// Safe `enableModule for 4337` call + SafeEnable4337Module = 141, + /// Safe implementation upgrade (v1.3.0 → v1.4.1) + SafeWalletVersionUpgrade = 142, } impl TransactionTypeId { diff --git a/bedrock/src/transactions/contracts/gnosis_safe.rs b/bedrock/src/transactions/contracts/gnosis_safe.rs new file mode 100644 index 00000000..2e878c0b --- /dev/null +++ b/bedrock/src/transactions/contracts/gnosis_safe.rs @@ -0,0 +1,358 @@ +//! Gnosis Safe (Safe Smart Account) contract interface. +//! +//! Provides read helpers for querying Safe contract state and transaction types +//! for `enableModule` and singleton upgrade operations. + +use alloy::{ + primitives::{Address, Bytes, FixedBytes, U256}, + sol, + sol_types::SolCall, +}; + +use alloy::primitives::address; + +use crate::primitives::{Network, PrimitiveError}; + +// --------------------------------------------------------------------------- +// Safe v1.4.1 addresses +// --------------------------------------------------------------------------- + +/// Safe v1.4.1 L2 singleton address (multichain deployment). +pub const SAFE_V141_L2_SINGLETON: Address = + address!("0x29fcB43b46531BcA003ddC8FCB67FFE91900C762"); + +/// Safe v1.4.1 proxy factory address (multichain deployment). +pub const SAFE_V141_PROXY_FACTORY: Address = + address!("0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67"); + +/// Safe v1.4.1 helper/batch contract address. +pub const SAFE_V141_HELPER_BATCH: Address = + address!("0x866087c23a7eE1fD5498ef84D59aF742f3d4b322"); + +/// Safe v1.4.1 module setup contract address. +pub const SAFE_V141_MODULE_SETUP: Address = + address!("0x2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47"); + +// --------------------------------------------------------------------------- +// Safe v1.3.0 addresses +// --------------------------------------------------------------------------- + +/// Safe v1.3.0 L1 singleton address (multichain deployment). +pub const SAFE_V130_L1_SINGLETON: Address = + address!("0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552"); + +/// Safe v1.3.0 L2 singleton address (multichain deployment). +pub const SAFE_V130_L2_SINGLETON: Address = + address!("0x3E5c63644E683549055b9Be8653de26E0B4CD36E"); + +/// Safe v1.3.0 proxy factory address (multichain deployment). +pub const SAFE_V130_PROXY_FACTORY: Address = + address!("0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2"); + +/// Safe v1.3.0 helper/batch contract address. +pub const SAFE_V130_HELPER_BATCH: Address = + address!("0x8d98006269238CAEd033b2d94661B29312AD09b7"); + +/// The Safe version string for v1.3.0. +pub const SAFE_VERSION_130: &str = "1.3.0"; + +// --------------------------------------------------------------------------- +// Shared / module addresses +// --------------------------------------------------------------------------- + +/// The Safe4337Module address that must be enabled on the wallet. +/// +/// Multichain address for the v0.3.0 `Safe4337Module`. +/// Reference: +pub const SAFE_4337_MODULE: Address = address!("0x75cf11467937ce3f2f357ce24ffc3dbf8fd5c226"); + +/// WorldChain migration contract for upgrading the Safe singleton from v1.3.0 to v1.4.1. +pub const WC_MIGRATION_WALLET_UPGRADE: Address = + address!("0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6"); + +/// Storage slot for the Safe fallback handler. +/// +/// Reference: +pub const SAFE_FALLBACK_HANDLER_SLOT: U256 = U256::from_be_bytes( + FixedBytes::new(hex_literal::hex!( + "6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5" + )) + .0, +); +use crate::smart_account::{ + ISafe4337Module, InstructionFlag, Is4337Encodable, NonceKeyV1, SafeOperation, + TransactionTypeId, UserOperation, +}; +use crate::transactions::{RpcClient, RpcError}; + +sol! { + /// Safe smart-account interface. + /// + /// Reference: + #[allow(clippy::too_many_arguments)] + #[sol(rpc)] + interface ISafe { + function setup( + address[] calldata _owners, + uint256 _threshold, + address to, + bytes calldata data, + address fallbackHandler, + address paymentToken, + uint256 payment, + address payable paymentReceiver + ) external; + + function isModuleEnabled(address module) external view returns (bool); + function enableModule(address module) external; + function enableModules(address[] memory modules) external; + function VERSION() external view returns (string); + + /// EIP-1271 validation + function isValidSignature(bytes32 dataHash, bytes memory signature) external view returns (bytes4); + + /// Execute Safe transaction + function execTransaction( + address to, + uint256 value, + bytes calldata data, + uint8 operation, + uint256 safeTxGas, + uint256 baseGas, + uint256 gasPrice, + address gasToken, + address payable refundReceiver, + bytes memory signatures + ) external payable returns (bool success); + } + + /// Safe proxy factory interface. + /// + /// Reference: + #[sol(rpc)] + interface ISafeProxyFactory { + event ProxyCreation(address indexed proxy, address singleton); + + function createProxyWithNonce( + address _singleton, + bytes memory initializer, + uint256 saltNonce + ) external returns (address proxy); + } +} + +// --------------------------------------------------------------------------- +// GnosisSafe — read helpers +// --------------------------------------------------------------------------- + +/// Helpers for querying Safe contract state. +pub struct GnosisSafe { + /// The Safe wallet address to query against. + safe_address: Address, +} + +impl GnosisSafe { + /// Creates a new `GnosisSafe` instance for the given wallet address. + #[must_use] + pub fn new(safe_address: Address) -> Self { + Self { safe_address } + } + + /// Returns the Safe wallet address. + #[must_use] + pub fn safe_address(&self) -> Address { + self.safe_address + } + + /// Checks whether a module is enabled on this Safe contract. + /// + /// # Returns + /// `true` if the module is enabled, `false` otherwise. + /// + /// # Errors + /// Returns an `RpcError` if the RPC call fails or the response is invalid. + pub async fn is_module_enabled( + &self, + rpc_client: &RpcClient, + module: Address, + ) -> Result { + let call_data = ISafe::isModuleEnabledCall { module }.abi_encode(); + + let result = rpc_client + .eth_call(Network::WorldChain, self.safe_address, call_data.into()) + .await?; + + if result.len() < 32 { + return Err(RpcError::InvalidResponse { + error_message: format!( + "Invalid isModuleEnabled response: expected 32 bytes, got {}", + result.len() + ), + }); + } + + Ok(result[31] == 1) + } + + /// Fetches the fallback handler address from the Safe contract's storage. + /// + /// Reads the `SAFE_FALLBACK_HANDLER_SLOT` directly via `eth_getStorageAt`. + /// + /// # Errors + /// Returns an `RpcError` if the RPC call fails. + pub async fn fetch_fallback_handler( + &self, + rpc_client: &RpcClient, + ) -> Result { + let slot_value = rpc_client + .eth_get_storage_at( + Network::WorldChain, + self.safe_address, + SAFE_FALLBACK_HANDLER_SLOT, + ) + .await?; + + // The address is stored in the lower 20 bytes of the 32-byte slot + Ok(Address::from_slice(&slot_value[12..])) + } + + /// Fetches the version string from the Safe contract. + /// + /// Calls the `VERSION()` getter on the Safe implementation contract. + /// For example, returns `"1.3.0"` or `"1.4.1"`. + /// + /// # Errors + /// Returns an `RpcError` if the RPC call fails or the response cannot be decoded. + pub async fn fetch_version( + &self, + rpc_client: &RpcClient, + ) -> Result { + let call_data = ISafe::VERSIONCall {}.abi_encode(); + + let result = rpc_client + .eth_call(Network::WorldChain, self.safe_address, call_data.into()) + .await?; + + let decoded = ISafe::VERSIONCall::abi_decode_returns(&result) + .map_err(|e| RpcError::InvalidResponse { + error_message: format!("Failed to decode VERSION response: {e}"), + })?; + + Ok(decoded) + } +} + +// --------------------------------------------------------------------------- +// SafeEnableModule — Is4337Encodable for enableModule calls +// --------------------------------------------------------------------------- + +/// Represents a Safe `enableModule` call. +/// +/// The Safe contract calls itself to enable a module, so `safe_address` is +/// both the sender and the target of the inner `executeUserOp` call. +pub struct SafeEnableModule { + /// The Safe contract address (target of the `enableModule` call). + safe_address: Address, + /// The ABI-encoded calldata for `enableModule(module)`. + call_data: Vec, +} + +impl SafeEnableModule { + /// Creates a new `enableModule` operation. + /// + /// # Arguments + /// * `safe_address` - The Safe wallet address (target of the call). + /// * `module` - The module address to enable. + #[must_use] + pub fn new(safe_address: Address, module: Address) -> Self { + let call_data = ISafe::enableModuleCall { module }.abi_encode(); + Self { + safe_address, + call_data, + } + } +} + +impl Is4337Encodable for SafeEnableModule { + type MetadataArg = (); + + fn as_execute_user_op_call_data(&self) -> Bytes { + ISafe4337Module::executeUserOpCall { + to: self.safe_address, + value: U256::ZERO, + data: self.call_data.clone().into(), + operation: SafeOperation::Call as u8, + } + .abi_encode() + .into() + } + + fn as_preflight_user_operation( + &self, + wallet_address: Address, + _metadata: Option, + ) -> Result { + let call_data = self.as_execute_user_op_call_data(); + + let key = NonceKeyV1::new( + TransactionTypeId::SafeEnable4337Module, + InstructionFlag::Default, + [0u8; 10], + ); + let nonce = key.encode_with_sequence(0); + + Ok(UserOperation::new_with_defaults( + wallet_address, + nonce, + call_data, + )) + } +} + +// --------------------------------------------------------------------------- +// SafeWalletVersionUpgrade — Is4337Encodable for singleton upgrade via delegatecall +// --------------------------------------------------------------------------- + +/// Represents a Safe singleton upgrade via `delegatecall` to the +/// `WC_MIGRATION_WALLET_UPGRADE` contract. +/// +/// The migration contract's function selector `0x68cb3d94` handles upgrading +/// the Safe proxy's singleton from v1.3.0 to v1.4.1 and registers the 4337 module as the fallback handler. +pub struct SafeWalletVersionUpgrade; + +impl Is4337Encodable for SafeWalletVersionUpgrade { + type MetadataArg = (); + + fn as_execute_user_op_call_data(&self) -> Bytes { + ISafe4337Module::executeUserOpCall { + to: WC_MIGRATION_WALLET_UPGRADE, + value: U256::ZERO, + // Function selector for the migration contract's upgrade function + data: Bytes::from_static(&[0x68, 0xcb, 0x3d, 0x94]), + operation: SafeOperation::DelegateCall as u8, + } + .abi_encode() + .into() + } + + fn as_preflight_user_operation( + &self, + wallet_address: Address, + _metadata: Option, + ) -> Result { + let call_data = self.as_execute_user_op_call_data(); + + let key = NonceKeyV1::new( + TransactionTypeId::SafeWalletVersionUpgrade, + InstructionFlag::Default, + [0u8; 10], + ); + let nonce = key.encode_with_sequence(0); + + Ok(UserOperation::new_with_defaults( + wallet_address, + nonce, + call_data, + )) + } +} \ No newline at end of file diff --git a/bedrock/src/transactions/contracts/mod.rs b/bedrock/src/transactions/contracts/mod.rs index 38b666f9..4b02ebac 100644 --- a/bedrock/src/transactions/contracts/mod.rs +++ b/bedrock/src/transactions/contracts/mod.rs @@ -2,6 +2,7 @@ //! that power the common transactions for the crypto wallet. pub mod erc20; pub mod erc4626; +pub mod gnosis_safe; pub mod multisend; pub mod permit2; pub mod usd_legacy_vault; diff --git a/bedrock/src/transactions/rpc.rs b/bedrock/src/transactions/rpc.rs index bd137f6a..342054b6 100644 --- a/bedrock/src/transactions/rpc.rs +++ b/bedrock/src/transactions/rpc.rs @@ -51,6 +51,9 @@ pub enum RpcMethod { /// Make a read call to a smart contract #[serde(rename = "eth_call")] EthCall, + /// Read a storage slot from a contract + #[serde(rename = "eth_getStorageAt")] + EthGetStorageAt, /// Query supported ERC-4337 entry points #[serde(rename = "eth_supportedEntryPoints")] SupportedEntryPoints, @@ -89,6 +92,7 @@ impl RpcMethod { Self::WaGetUserOperationReceipt => "wa_getUserOperationReceipt", Self::SendUserOperation => "eth_sendUserOperation", Self::EthCall => "eth_call", + Self::EthGetStorageAt => "eth_getStorageAt", Self::SupportedEntryPoints => "eth_supportedEntryPoints", } } @@ -503,6 +507,36 @@ impl RpcClient { error_message: format!("Invalid eth_call result format: {e}"), }) } + + /// Reads a raw storage slot from a contract via `eth_getStorageAt`. + /// + /// # Errors + /// Returns an error if the RPC call fails or the response cannot be parsed. + pub async fn eth_get_storage_at( + &self, + network: Network, + address: Address, + slot: U256, + ) -> Result, RpcError> { + let params = vec![ + serde_json::Value::String(format!("{address:?}")), + serde_json::Value::String(format!("{slot:#066x}")), + serde_json::Value::String("latest".to_string()), + ]; + + let result: String = self + .rpc_call( + network, + RpcMethod::EthGetStorageAt, + params, + RpcProviderName::Any, + ) + .await?; + + FixedBytes::from_hex(&result).map_err(|e| RpcError::InvalidResponse { + error_message: format!("Invalid eth_getStorageAt result format: {e}"), + }) + } } /// Gets the global RPC client, initializing it on first access. diff --git a/bedrock/tests/common.rs b/bedrock/tests/common.rs index b8b059dd..99cff9d8 100644 --- a/bedrock/tests/common.rs +++ b/bedrock/tests/common.rs @@ -4,64 +4,17 @@ use std::str::FromStr; use alloy::{ network::Ethereum, node_bindings::AnvilInstance, - primitives::{address, keccak256, Address, Log, U256}, + primitives::{keccak256, Address, Log, U256}, providers::{ext::AnvilApi, Provider}, sol, sol_types::{SolCall, SolEvent}, }; use bedrock::primitives::config::{current_environment_or_default, BedrockEnvironment}; - -sol!( - #[allow(missing_docs)] - #[sol(rpc)] - interface ISafeProxyFactory { - event ProxyCreation(address indexed proxy, address singleton); - - function createProxyWithNonce( - address _singleton, - bytes memory initializer, - uint256 saltNonce - ) external returns (address proxy); - } -); - -// https://github.com/safe-global/safe-smart-account/blob/v1.5.0/contracts/interfaces/ISafe.sol -sol!( - #[allow(clippy::too_many_arguments)] - #[sol(rpc)] - interface ISafe { - function setup( - address[] calldata _owners, - uint256 _threshold, - address to, - bytes calldata data, - address fallbackHandler, - address paymentToken, - uint256 payment, - address payable paymentReceiver - ) external; - - function enableModules(address[] memory modules) external; - - /// EIP-1271 validation - function isValidSignature(bytes32 dataHash, bytes memory signature) external view returns (bytes4); - - /// Execute Safe transaction - function execTransaction( - address to, - uint256 value, - bytes calldata data, - uint8 operation, - uint256 safeTxGas, - uint256 baseGas, - uint256 gasPrice, - address gasToken, - address payable refundReceiver, - bytes memory signatures - ) external payable returns (bool success); - } -); +pub use bedrock::transactions::contracts::gnosis_safe::{ + ISafe, ISafeProxyFactory, SAFE_4337_MODULE, SAFE_V130_L2_SINGLETON, SAFE_V130_PROXY_FACTORY, + SAFE_V141_L2_SINGLETON, SAFE_V141_MODULE_SETUP, SAFE_V141_PROXY_FACTORY, +}; sol! { @@ -85,16 +38,6 @@ sol! { } } -// Safe contract addresses on Worldchain -pub const SAFE_PROXY_FACTORY_ADDRESS: Address = - address!("4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67"); -pub const SAFE_L2_SINGLETON_ADDRESS: Address = - address!("29fcB43b46531BcA003ddC8FCB67FFE91900C762"); -pub const SAFE_4337_MODULE_ADDRESS: Address = - address!("75cf11467937ce3F2f357CE24ffc3DBF8fD5c226"); -pub const SAFE_MODULE_SETUP_ADDRESS: Address = - address!("2dd68b007B46fBe91B9A7c3EDa5A7a1063cB5b47"); - pub fn setup_anvil() -> AnvilInstance { dotenvy::dotenv().ok(); let rpc_url = std::env::var("WORLDCHAIN_RPC_URL").unwrap_or_else(|_| { @@ -105,7 +48,7 @@ pub fn setup_anvil() -> AnvilInstance { alloy::node_bindings::Anvil::new().fork(rpc_url).spawn() } -pub async fn deploy_safe

( +pub async fn deploy_safe_v141

( provider: &P, owner: Address, deploy_nonce: U256, @@ -119,19 +62,19 @@ where .await .unwrap(); - let proxy_factory = ISafeProxyFactory::new(SAFE_PROXY_FACTORY_ADDRESS, provider); + let proxy_factory = ISafeProxyFactory::new(SAFE_V141_PROXY_FACTORY, provider); // Encode the Safe setup call let setup_data = ISafe::setupCall { _owners: vec![owner], _threshold: U256::from(1), - to: SAFE_MODULE_SETUP_ADDRESS, + to: SAFE_V141_MODULE_SETUP, data: ISafe::enableModulesCall { - modules: vec![SAFE_4337_MODULE_ADDRESS], + modules: vec![SAFE_4337_MODULE], } .abi_encode() .into(), - fallbackHandler: SAFE_4337_MODULE_ADDRESS, + fallbackHandler: SAFE_4337_MODULE, paymentToken: Address::ZERO, payment: U256::ZERO, paymentReceiver: Address::ZERO, @@ -141,7 +84,7 @@ where // Deploy Safe via proxy factory let deploy_tx = proxy_factory .createProxyWithNonce( - SAFE_L2_SINGLETON_ADDRESS, + SAFE_V141_L2_SINGLETON, setup_data.into(), deploy_nonce, ) @@ -271,3 +214,59 @@ where Ok(()) } + +/// Deploy a bare Safe v1.3.0 (no 4337 module, no fallback handler). +/// +/// This mirrors how v1.3.0 wallets exist pre-migration: the proxy points at +/// the v1.3.0 singleton and has no modules or fallback handler configured. +/// The migration processors are responsible for enabling modules and upgrading. +pub async fn deploy_safe_v130

( + provider: &P, + owner: Address, + deploy_nonce: U256, +) -> anyhow::Result

+where + P: Provider, +{ + provider + .anvil_set_balance(owner, U256::from(1e19 as u64)) + .await + .unwrap(); + + let proxy_factory = ISafeProxyFactory::new(SAFE_V130_PROXY_FACTORY, provider); + + let setup_data = ISafe::setupCall { + _owners: vec![owner], + _threshold: U256::from(1), + to: Address::ZERO, + data: Default::default(), + fallbackHandler: Address::ZERO, + paymentToken: Address::ZERO, + payment: U256::ZERO, + paymentReceiver: Address::ZERO, + } + .abi_encode(); + + let deploy_tx = proxy_factory + .createProxyWithNonce(SAFE_V130_L2_SINGLETON, setup_data.into(), deploy_nonce) + .from(owner) + .send() + .await?; + + let receipt = deploy_tx.get_receipt().await?; + + let proxy_creation_event = receipt + .inner + .logs() + .iter() + .find_map(|log| { + let raw_log = Log { + address: log.address(), + data: log.data().clone(), + }; + ISafeProxyFactory::ProxyCreation::decode_log(&raw_log).ok() + }) + .expect("ProxyCreation event not found"); + + Ok(proxy_creation_event.proxy) +} diff --git a/bedrock/tests/test_enable_4337_module_processor.rs b/bedrock/tests/test_enable_4337_module_processor.rs new file mode 100644 index 00000000..422ae358 --- /dev/null +++ b/bedrock/tests/test_enable_4337_module_processor.rs @@ -0,0 +1,108 @@ +use std::sync::Arc; + +mod common; +use alloy::{ + primitives::U256, + providers::{ext::AnvilApi, ProviderBuilder}, + signers::local::PrivateKeySigner, +}; +use common::{deploy_safe_v130, setup_anvil, ISafe, SAFE_4337_MODULE}; + +use bedrock::{ + migration::{ + processors::enable_4337_module_processor::Enable4337ModuleProcessor, + MigrationProcessor, + }, + primitives::http_client::set_http_client, + smart_account::{SafeSmartAccount, ENTRYPOINT_4337}, + test_utils::{AnvilBackedHttpClient, IEntryPoint}, +}; + +#[tokio::test] +async fn test_enable_4337_module_processor_full_flow() -> anyhow::Result<()> { + // 1) Spin up anvil fork of WorldChain + let anvil = setup_anvil(); + + // 2) Owner signer and provider + let owner_signer = PrivateKeySigner::random(); + let owner_key_hex = hex::encode(owner_signer.to_bytes()); + let owner = owner_signer.address(); + + let provider = ProviderBuilder::new() + .wallet(owner_signer.clone()) + .connect_http(anvil.endpoint_url()); + + provider + .anvil_set_balance(owner, U256::from(1e18 as u64)) + .await?; + + // 3) Deploy Safe v1.3.0 without 4337 module + let safe_address = + deploy_safe_v130(&provider, owner, U256::ZERO).await?; + + // 4) Verify module is NOT enabled + let safe_contract = ISafe::new(safe_address, &provider); + let is_enabled = safe_contract + .isModuleEnabled(SAFE_4337_MODULE) + .call() + .await?; + assert!(!is_enabled, "4337 module should NOT be enabled initially"); + + // 5) Fund EntryPoint deposit for Safe + let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); + let deposit_tx = entry_point + .depositTo(safe_address) + .value(U256::from(1e18 as u64)) + .send() + .await?; + let _ = deposit_tx.get_receipt().await?; + + // 6) We need the 4337 module enabled as fallback handler for UserOp execution. + // Since this is a chicken-and-egg problem (we need the module to execute UserOps, + // but we want to test enabling the module via UserOp), we enable the module + // via a direct execTransaction first, then disable it, then test the processor. + // + // Alternative approach: test is_applicable only (which is the main on-chain check), + // and trust that execute follows the same pattern as the permit2 processor. + + // 6) Install mocked HTTP client that routes RPC calls to Anvil + let client = AnvilBackedHttpClient::new(provider.clone()); + set_http_client(Arc::new(client)); + + // 7) Create the processor + let safe_account = Arc::new(SafeSmartAccount::new( + owner_key_hex, + &safe_address.to_string(), + )?); + let processor = Enable4337ModuleProcessor::new(safe_account.clone()); + + // 8) Verify migration ID + assert_eq!( + processor.migration_id(), + "wallet.safe.enable_4337_module.v1" + ); + + // 9) Verify is_applicable returns true (module not enabled) + assert!( + processor.is_applicable().await?, + "Processor should be applicable when 4337 module is NOT enabled" + ); + + // 10) Now deploy a Safe WITH module enabled and verify is_applicable returns false + let safe_address_with_module = + common::deploy_safe_v141(&provider, owner, U256::from(1)).await?; + + let safe_account_with_module = Arc::new(SafeSmartAccount::new( + hex::encode(owner_signer.to_bytes()), + &safe_address_with_module.to_string(), + )?); + let processor_with_module = + Enable4337ModuleProcessor::new(safe_account_with_module); + + assert!( + !processor_with_module.is_applicable().await?, + "Processor should NOT be applicable when 4337 module is already enabled" + ); + + Ok(()) +} diff --git a/bedrock/tests/test_permit2_approval_processor.rs b/bedrock/tests/test_permit2_approval_processor.rs index a27c8fba..0e15ca62 100644 --- a/bedrock/tests/test_permit2_approval_processor.rs +++ b/bedrock/tests/test_permit2_approval_processor.rs @@ -6,7 +6,7 @@ use alloy::{ providers::{ext::AnvilApi, ProviderBuilder}, signers::local::PrivateKeySigner, }; -use common::{deploy_safe, setup_anvil, IERC20}; +use common::{deploy_safe_v141, setup_anvil, IERC20}; use bedrock::{ migration::{ @@ -40,7 +40,7 @@ async fn test_permit2_approval_processor_full_flow() -> anyhow::Result<()> { .await?; // 3) Deploy Safe with 4337 module enabled - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; // 4) Fund EntryPoint deposit for Safe let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); diff --git a/bedrock/tests/test_safe_upgrade_processor.rs b/bedrock/tests/test_safe_upgrade_processor.rs new file mode 100644 index 00000000..8b5865dd --- /dev/null +++ b/bedrock/tests/test_safe_upgrade_processor.rs @@ -0,0 +1,167 @@ +use std::sync::Arc; + +mod common; +use alloy::{ + primitives::U256, + providers::{ext::AnvilApi, ProviderBuilder}, + signers::local::PrivateKeySigner, +}; +use common::{deploy_safe_v130, setup_anvil}; + +use bedrock::{ + migration::{ + processors::safe_upgrade_processor::SafeUpgradeProcessor, MigrationProcessor, + ProcessorResult, + }, + primitives::http_client::set_http_client, + smart_account::{SafeSmartAccount, ENTRYPOINT_4337}, + test_utils::{AnvilBackedHttpClient, IEntryPoint}, +}; + +#[tokio::test] +async fn test_safe_upgrade_processor_is_applicable_v130() -> anyhow::Result<()> { + // 1) Spin up anvil fork of WorldChain + let anvil = setup_anvil(); + + // 2) Owner signer and provider + let owner_signer = PrivateKeySigner::random(); + let owner_key_hex = hex::encode(owner_signer.to_bytes()); + let owner = owner_signer.address(); + + let provider = ProviderBuilder::new() + .wallet(owner_signer.clone()) + .connect_http(anvil.endpoint_url()); + + provider + .anvil_set_balance(owner, U256::from(1e18 as u64)) + .await?; + + // 3) Deploy Safe with v1.3.0 singleton + let safe_address = deploy_safe_v130(&provider, owner, U256::ZERO).await?; + + // 4) Install mocked HTTP client + let client = AnvilBackedHttpClient::new(provider.clone()); + set_http_client(Arc::new(client)); + + // 5) Create the processor + let safe_account = Arc::new(SafeSmartAccount::new( + owner_key_hex, + &safe_address.to_string(), + )?); + let processor = SafeUpgradeProcessor::new(safe_account); + + // 6) Verify migration ID + assert_eq!(processor.migration_id(), "wallet.safe.upgrade.v1"); + + // 7) Verify is_applicable returns true (wallet is on v1.3.0) + assert!( + processor.is_applicable().await?, + "Processor should be applicable for v1.3.0 Safe" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_safe_upgrade_processor_not_applicable_v141() -> anyhow::Result<()> { + // 1) Spin up anvil fork of WorldChain + let anvil = setup_anvil(); + + // 2) Owner signer and provider + let owner_signer = PrivateKeySigner::random(); + let owner_key_hex = hex::encode(owner_signer.to_bytes()); + let owner = owner_signer.address(); + + let provider = ProviderBuilder::new() + .wallet(owner_signer.clone()) + .connect_http(anvil.endpoint_url()); + + provider + .anvil_set_balance(owner, U256::from(1e18 as u64)) + .await?; + + // 3) Deploy Safe with v1.4.1 singleton (default) + let safe_address = common::deploy_safe_v141(&provider, owner, U256::ZERO).await?; + + // 4) Install mocked HTTP client + let client = AnvilBackedHttpClient::new(provider.clone()); + set_http_client(Arc::new(client)); + + // 5) Create the processor + let safe_account = Arc::new(SafeSmartAccount::new( + owner_key_hex, + &safe_address.to_string(), + )?); + let processor = SafeUpgradeProcessor::new(safe_account); + + // 6) Verify is_applicable returns false (wallet is already on v1.4.1) + assert!( + !processor.is_applicable().await?, + "Processor should NOT be applicable for v1.4.1 Safe" + ); + + Ok(()) +} + +#[tokio::test] +async fn test_safe_upgrade_processor_execute() -> anyhow::Result<()> { + // 1) Spin up anvil fork of WorldChain + let anvil = setup_anvil(); + + // 2) Owner signer and provider + let owner_signer = PrivateKeySigner::random(); + let owner_key_hex = hex::encode(owner_signer.to_bytes()); + let owner = owner_signer.address(); + + let provider = ProviderBuilder::new() + .wallet(owner_signer.clone()) + .connect_http(anvil.endpoint_url()); + + provider + .anvil_set_balance(owner, U256::from(1e18 as u64)) + .await?; + + // 3) Deploy Safe with v1.3.0 singleton + 4337 module + let safe_address = deploy_safe_v130(&provider, owner, U256::ZERO).await?; + + // 4) Fund EntryPoint deposit for Safe + let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); + let deposit_tx = entry_point + .depositTo(safe_address) + .value(U256::from(1e18 as u64)) + .send() + .await?; + let _ = deposit_tx.get_receipt().await?; + + // 5) Install mocked HTTP client + let client = AnvilBackedHttpClient::new(provider.clone()); + set_http_client(Arc::new(client)); + + // 6) Create the processor + let safe_account = Arc::new(SafeSmartAccount::new( + owner_key_hex, + &safe_address.to_string(), + )?); + let processor = SafeUpgradeProcessor::new(safe_account); + + // 7) Verify is_applicable returns true before upgrade + assert!( + processor.is_applicable().await?, + "Processor should be applicable for v1.3.0 Safe" + ); + + // 8) Execute the upgrade + let result = processor.execute().await?; + assert!( + matches!(result, ProcessorResult::Success), + "Expected ProcessorResult::Success" + ); + + // 9) Verify is_applicable returns false after upgrade + assert!( + !processor.is_applicable().await?, + "Processor should NOT be applicable after upgrade" + ); + + Ok(()) +} diff --git a/bedrock/tests/test_smart_account_bundler_sponsored.rs b/bedrock/tests/test_smart_account_bundler_sponsored.rs index 164cc0ff..0599ed94 100644 --- a/bedrock/tests/test_smart_account_bundler_sponsored.rs +++ b/bedrock/tests/test_smart_account_bundler_sponsored.rs @@ -6,7 +6,7 @@ use alloy::{ providers::{ext::AnvilApi, ProviderBuilder}, signers::local::PrivateKeySigner, }; -use common::{deploy_safe, set_erc20_balance_for_safe, setup_anvil, IERC20}; +use common::{deploy_safe_v141, set_erc20_balance_for_safe, setup_anvil, IERC20}; use bedrock::{ primitives::http_client::set_http_client, @@ -105,7 +105,7 @@ async fn test_send_bundler_sponsored_user_operation() -> anyhow::Result<()> { .await?; // 3) Deploy Safe with 4337 module enabled - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; // 4) Fund EntryPoint deposit for Safe (needs enough to cover gas at zero fee) let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); diff --git a/bedrock/tests/test_smart_account_erc4337_transaction_execution.rs b/bedrock/tests/test_smart_account_erc4337_transaction_execution.rs index e1c9cd14..a573580b 100644 --- a/bedrock/tests/test_smart_account_erc4337_transaction_execution.rs +++ b/bedrock/tests/test_smart_account_erc4337_transaction_execution.rs @@ -15,7 +15,7 @@ use bedrock::{ transactions::foreign::UnparsedUserOperation, }; mod common; -use common::{deploy_safe, setup_anvil, ISafe4337Module}; +use common::{deploy_safe_v141, setup_anvil, ISafe4337Module}; /// Integration test for the encoding, signing and execution of a 4337 transaction. /// @@ -35,8 +35,8 @@ async fn test_integration_erc4337_transaction_execution() -> anyhow::Result<()> .connect_http(anvil.endpoint_url()); // Deploy Safes - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; - let safe_address2 = deploy_safe(&provider, owner, U256::from(1)).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; + let safe_address2 = deploy_safe_v141(&provider, owner, U256::from(1)).await?; // Fund the Safe, to be able to test the balance transfer provider diff --git a/bedrock/tests/test_smart_account_morpho.rs b/bedrock/tests/test_smart_account_morpho.rs index 8f0f29e8..4390667d 100644 --- a/bedrock/tests/test_smart_account_morpho.rs +++ b/bedrock/tests/test_smart_account_morpho.rs @@ -9,7 +9,7 @@ use alloy::{ signers::local::PrivateKeySigner, sol, }; -use common::{deploy_safe, set_erc20_balance_for_safe, setup_anvil, IERC20}; +use common::{deploy_safe_v141, set_erc20_balance_for_safe, setup_anvil, IERC20}; use std::str::FromStr; use bedrock::{ @@ -52,7 +52,7 @@ async fn test_erc4626_deposit_wld() -> anyhow::Result<()> { .await?; // 3) Deploy Safe with 4337 module enabled - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; println!("✓ Deployed Safe at: {safe_address}"); // 4) Fund EntryPoint deposit for Safe (for gas) diff --git a/bedrock/tests/test_smart_account_permit2_transfer.rs b/bedrock/tests/test_smart_account_permit2_transfer.rs index 21b20001..9ddc8088 100644 --- a/bedrock/tests/test_smart_account_permit2_transfer.rs +++ b/bedrock/tests/test_smart_account_permit2_transfer.rs @@ -17,7 +17,7 @@ use bedrock::smart_account::{ use chrono::Utc; mod common; -use common::{deploy_safe, set_erc20_balance_for_safe, setup_anvil, ISafe, IERC20}; +use common::{deploy_safe_v141, set_erc20_balance_for_safe, setup_anvil, ISafe, IERC20}; sol!( // NOTE: This is defined in the `permit2` module, but it cannot be easily re-used here. @@ -85,7 +85,7 @@ async fn test_integration_permit2_transfer() -> anyhow::Result<()> { provider.anvil_set_balance(owner, U256::from(1e18)).await?; // Step 2: Deploy a Safe (World App User) - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; let chain_id = Network::WorldChain as u32; let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; @@ -241,7 +241,7 @@ async fn test_integration_permit2_approve_and_allowance_transfer() -> anyhow::Re provider.anvil_set_balance(owner, U256::from(1e18)).await?; // Step 2: Deploy a Safe (World App User) - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; let chain_id = Network::WorldChain as u32; let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; diff --git a/bedrock/tests/test_smart_account_personal_sign.rs b/bedrock/tests/test_smart_account_personal_sign.rs index 9bb62965..df183305 100644 --- a/bedrock/tests/test_smart_account_personal_sign.rs +++ b/bedrock/tests/test_smart_account_personal_sign.rs @@ -6,7 +6,7 @@ use alloy::{ use bedrock::{primitives::Network, smart_account::SafeSmartAccount}; mod common; -use common::{deploy_safe, setup_anvil, ISafe}; +use common::{deploy_safe_v141, setup_anvil, ISafe}; #[tokio::test] async fn test_integration_personal_sign() { @@ -29,7 +29,7 @@ async fn test_integration_personal_sign() { println!("✓ Using owner address: {owner}"); // Deploy a Safe - let safe_address = deploy_safe(&provider, owner, U256::ZERO) + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO) .await .expect("Failed to deploy Safe"); @@ -110,7 +110,7 @@ async fn test_integration_personal_sign_failure_on_incorrect_chain_id() { .await .unwrap(); - let safe_address = deploy_safe(&provider, owner, U256::ZERO) + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO) .await .expect("Failed to deploy Safe"); @@ -171,7 +171,7 @@ async fn test_integration_personal_sign_failure_on_incorrect_eip_191_prefix() { .await .expect("Failed to set balance"); - let safe_address = deploy_safe(&provider, owner, U256::ZERO) + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO) .await .expect("Failed to deploy Safe"); diff --git a/bedrock/tests/test_smart_account_sign_typed_data.rs b/bedrock/tests/test_smart_account_sign_typed_data.rs index 284929e6..a34d93bf 100644 --- a/bedrock/tests/test_smart_account_sign_typed_data.rs +++ b/bedrock/tests/test_smart_account_sign_typed_data.rs @@ -8,7 +8,7 @@ use bedrock::{primitives::Network, smart_account::SafeSmartAccount}; use serde_json::json; mod common; -use common::{deploy_safe, setup_anvil, ISafe}; +use common::{deploy_safe_v141, setup_anvil, ISafe}; #[tokio::test] async fn test_integration_sign_typed_data() { @@ -31,7 +31,7 @@ async fn test_integration_sign_typed_data() { println!("✓ Using owner address: {owner}"); // Deploy a Safe - let safe_address = deploy_safe(&provider, owner, U256::ZERO) + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO) .await .expect("Failed to deploy Safe"); diff --git a/bedrock/tests/test_smart_account_transfer.rs b/bedrock/tests/test_smart_account_transfer.rs index e62c5bb5..0a3e8d58 100644 --- a/bedrock/tests/test_smart_account_transfer.rs +++ b/bedrock/tests/test_smart_account_transfer.rs @@ -6,7 +6,7 @@ use alloy::{ providers::{ext::AnvilApi, ProviderBuilder}, signers::local::PrivateKeySigner, }; -use common::{deploy_safe, set_erc20_balance_for_safe, setup_anvil, IERC20}; +use common::{deploy_safe_v141, set_erc20_balance_for_safe, setup_anvil, IERC20}; use bedrock::{ primitives::http_client::set_http_client, @@ -36,7 +36,7 @@ async fn test_transaction_transfer_full_flow_executes_user_operation( .await?; // 3) Deploy Safe with 4337 module enabled - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; // 4) Fund EntryPoint deposit for Safe let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); diff --git a/bedrock/tests/test_smart_account_usd_vault.rs b/bedrock/tests/test_smart_account_usd_vault.rs index 850bb2c7..cbb14668 100644 --- a/bedrock/tests/test_smart_account_usd_vault.rs +++ b/bedrock/tests/test_smart_account_usd_vault.rs @@ -10,7 +10,7 @@ use alloy::{ providers::{ext::AnvilApi, ProviderBuilder}, signers::local::PrivateKeySigner, }; -use common::{deploy_safe, set_erc20_balance_for_safe, setup_anvil, IERC20}; +use common::{deploy_safe_v141, set_erc20_balance_for_safe, setup_anvil, IERC20}; use std::str::FromStr; @@ -51,7 +51,7 @@ async fn test_usd_vault_migration() -> anyhow::Result<()> { let sdai = IERC20::new(sdai_address, &provider); let morpho_vault = IERC20::new(morpho_vault_address, &provider); - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; println!("✓ Deployed Safe at: {safe_address}"); let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; diff --git a/bedrock/tests/test_smart_account_wld_vault.rs b/bedrock/tests/test_smart_account_wld_vault.rs index 1a7496a0..d539d6b3 100644 --- a/bedrock/tests/test_smart_account_wld_vault.rs +++ b/bedrock/tests/test_smart_account_wld_vault.rs @@ -11,7 +11,7 @@ use alloy::{ signers::local::PrivateKeySigner, sol, }; -use common::{deploy_safe, set_erc20_balance_for_safe, setup_anvil, IERC20}; +use common::{deploy_safe_v141, set_erc20_balance_for_safe, setup_anvil, IERC20}; use std::str::FromStr; @@ -57,7 +57,7 @@ async fn test_wld_vault_migration() -> anyhow::Result<()> { let wld_vault = WLDVault::new(wld_vault_address, &provider); let morpho_vault = IERC20::new(morpho_vault_address, &provider); - let safe_address = deploy_safe(&provider, owner, U256::ZERO).await?; + let safe_address = deploy_safe_v141(&provider, owner, U256::ZERO).await?; println!("✓ Deployed Safe at: {safe_address}"); let safe_account = SafeSmartAccount::new(owner_key_hex, &safe_address.to_string())?; diff --git a/bedrock/tests/test_smart_account_world_gift_manager_gift_cancel.rs b/bedrock/tests/test_smart_account_world_gift_manager_gift_cancel.rs index 144a0b8c..1e2cf749 100644 --- a/bedrock/tests/test_smart_account_world_gift_manager_gift_cancel.rs +++ b/bedrock/tests/test_smart_account_world_gift_manager_gift_cancel.rs @@ -6,7 +6,7 @@ use alloy::{ providers::{ext::AnvilApi, ProviderBuilder}, signers::local::PrivateKeySigner, }; -use common::{deploy_safe, set_erc20_balance_for_safe, setup_anvil, IERC20}; +use common::{deploy_safe_v141, set_erc20_balance_for_safe, setup_anvil, IERC20}; use bedrock::{ primitives::http_client::set_http_client, @@ -31,8 +31,8 @@ async fn test_transaction_world_gift_manager_gift_cancel_user_operations( .anvil_set_balance(owner, U256::from(1e18 as u64)) .await?; - let safe_address_giftor = deploy_safe(&provider, owner, U256::ZERO).await?; - let safe_address_giftee = deploy_safe(&provider, owner, U256::from(1)).await?; + let safe_address_giftor = deploy_safe_v141(&provider, owner, U256::ZERO).await?; + let safe_address_giftee = deploy_safe_v141(&provider, owner, U256::from(1)).await?; let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); for safe in [safe_address_giftor, safe_address_giftee] { diff --git a/bedrock/tests/test_smart_account_world_gift_manager_gift_redeem.rs b/bedrock/tests/test_smart_account_world_gift_manager_gift_redeem.rs index 887644b1..7b43c913 100644 --- a/bedrock/tests/test_smart_account_world_gift_manager_gift_redeem.rs +++ b/bedrock/tests/test_smart_account_world_gift_manager_gift_redeem.rs @@ -1,7 +1,7 @@ use std::sync::Arc; mod common; -use common::{deploy_safe, set_erc20_balance_for_safe, setup_anvil, IERC20}; +use common::{deploy_safe_v141, set_erc20_balance_for_safe, setup_anvil, IERC20}; use alloy::{ primitives::{address, U256}, @@ -32,8 +32,8 @@ async fn test_transaction_world_gift_manager_gift_redeem_user_operations( .anvil_set_balance(owner, U256::from(1e18 as u64)) .await?; - let safe_address_giftor = deploy_safe(&provider, owner, U256::ZERO).await?; - let safe_address_giftee = deploy_safe(&provider, owner, U256::from(1)).await?; + let safe_address_giftor = deploy_safe_v141(&provider, owner, U256::ZERO).await?; + let safe_address_giftee = deploy_safe_v141(&provider, owner, U256::from(1)).await?; let entry_point = IEntryPoint::new(*ENTRYPOINT_4337, &provider); for safe in [safe_address_giftor, safe_address_giftee] {