diff --git a/bedrock/src/smart_account/nonce.rs b/bedrock/src/smart_account/nonce.rs index 5ccc8041..f6e7e02e 100644 --- a/bedrock/src/smart_account/nonce.rs +++ b/bedrock/src/smart_account/nonce.rs @@ -40,6 +40,10 @@ pub enum TransactionTypeId { MorphoDeposit = 136, /// Morpho-specific vault withdraw MorphoWithdraw = 137, + /// WLDVault migration to ERC-4626 vault + WLDVaultMigration = 138, + /// USDVault migration to ERC-4626 vault + USDVaultMigration = 139, } impl TransactionTypeId { diff --git a/bedrock/src/transactions/contracts/erc4626.rs b/bedrock/src/transactions/contracts/erc4626.rs index 0e8a117a..e8d6f446 100644 --- a/bedrock/src/transactions/contracts/erc4626.rs +++ b/bedrock/src/transactions/contracts/erc4626.rs @@ -58,7 +58,7 @@ pub struct Erc4626Vault { impl Erc4626Vault { /// Helper function to fetch and decode an asset address from an RPC call. /// Validates that the response is at least 32 bytes before extracting the address. - async fn fetch_asset_address( + pub async fn fetch_asset_address( rpc_client: &RpcClient, network: Network, contract_address: Address, @@ -84,7 +84,7 @@ impl Erc4626Vault { /// Helper function to fetch and decode a U256 value (balance) from an RPC call. /// Validates that the response is at least 32 bytes before decoding. - async fn fetch_balance( + pub async fn fetch_balance( rpc_client: &RpcClient, network: Network, contract_address: Address, diff --git a/bedrock/src/transactions/contracts/mod.rs b/bedrock/src/transactions/contracts/mod.rs index 724216d1..06f47245 100644 --- a/bedrock/src/transactions/contracts/mod.rs +++ b/bedrock/src/transactions/contracts/mod.rs @@ -7,3 +7,5 @@ pub mod erc4626; pub mod multisend; pub mod world_campaign_manager; pub mod world_gift_manager; +pub mod wld_vault; +pub mod usd_vault; diff --git a/bedrock/src/transactions/contracts/usd_vault.rs b/bedrock/src/transactions/contracts/usd_vault.rs new file mode 100644 index 00000000..aaf067ce --- /dev/null +++ b/bedrock/src/transactions/contracts/usd_vault.rs @@ -0,0 +1,253 @@ +//! This module introduces USD Vault contract interface. + +use alloy::{ + primitives::{Address, Bytes, U256}, + sol, + sol_types::SolCall, +}; + +use crate::transactions::contracts::erc20::{Erc20, IErc20}; +use crate::transactions::contracts::multisend::{MultiSend, MultiSendTx}; +use crate::transactions::rpc::{RpcClient, RpcError}; +use crate::{ + primitives::HexEncodedData, + smart_account::{ + ISafe4337Module, InstructionFlag, Is4337Encodable, NonceKeyV1, SafeOperation, + TransactionTypeId, UserOperation, + }, +}; +use crate::{ + primitives::{Network, PrimitiveError}, + transactions::contracts::erc4626::Erc4626Vault, +}; + +sol! { + #[derive(serde::Serialize)] + interface USDVault { + function USDC() public view returns (address); + function SDAI() public view returns (address); + + function getDSRConversionRate() public view returns (uint256); + + function redeemSDAI( + address recipient, + uint256 amountIn, + uint256 amountOutMin, + uint256 nonce, + uint256 deadline, + bytes signature + ) external; + } + + /// The ERC-4626 vault contract interface. + /// Reference: + /// Reference: + #[derive(serde::Serialize)] + interface IERC4626 { + function asset() public view returns (address assetTokenAddress); + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + } +} + +/// Represents a USD Vault migration transaction bundle. +#[derive(Debug)] +pub struct UsdVault { + /// The encoded call data for the operation. + pub call_data: Vec, + /// The action type. + action: TransactionTypeId, + /// The target address for the operation. + to: Address, + /// The Safe operation type for the operation. + operation: SafeOperation, + /// Metadata for nonce generation (protocol-specific). + metadata: [u8; 10], +} + +impl UsdVault { + /// Fetches the user's sDAI balance from the USD Vault. + pub async fn fetch_sdai_balance( + rpc_client: &RpcClient, + network: Network, + usd_vault_address: Address, + user: Address, + ) -> Result<(Address, U256), RpcError> { + let sdai_call_data = USDVault::SDAICall {}.abi_encode(); + let sdai_address = Erc4626Vault::fetch_asset_address( + rpc_client, + network, + usd_vault_address, + sdai_call_data, + ) + .await?; + + let balance_call_data = IErc20::balanceOfCall { account: user }.abi_encode(); + let balance = Erc4626Vault::fetch_balance( + rpc_client, + network, + sdai_address, + balance_call_data, + "balanceOf", + ) + .await?; + Ok((sdai_address, balance)) + } + + /// Constructs a WLD Vault migration transaction bundle. + pub async fn migrate( + rpc_client: &RpcClient, + network: Network, + usd_vault_address: Address, + erc4626_vault_address: Address, + sdai_amount: U256, + user: Address, + permit2_signature: HexEncodedData, + permit2_nonce: U256, + permit2_deadline: U256, + metadata: [u8; 10], + ) -> Result { + let usdc_call_data = USDVault::USDCCall {}.abi_encode(); + let usdc_address = Erc4626Vault::fetch_asset_address( + rpc_client, + network, + usd_vault_address, + usdc_call_data, + ) + .await?; + + let asset_call_data = IERC4626::assetCall {}.abi_encode(); + let asset_address = Erc4626Vault::fetch_asset_address( + rpc_client, + network, + erc4626_vault_address, + asset_call_data, + ) + .await?; + + if usdc_address != asset_address { + return Err(RpcError::InvalidResponse { + error_message: + "Asset address mismatch between USD Vault and ERC-4626 Vault" + .to_string(), + }); + } + + let rate_call_data = USDVault::getDSRConversionRateCall {}.abi_encode(); + let rate = Erc4626Vault::fetch_balance( + rpc_client, + network, + usd_vault_address, + rate_call_data, + "getDSRConversionRate", + ) + .await?; + + let decimal_factor = U256::from_str_radix( + "1000000000000000000000000000000000000000", // 1e39 + 10, + ) + .unwrap(); + let usdc_amount = sdai_amount + .checked_mul(rate) + .ok_or(RpcError::InvalidResponse { + error_message: "Multiplication overflow when calculating USDC amount" + .to_string(), + })? + .checked_div(decimal_factor) + .ok_or(RpcError::InvalidResponse { + error_message: "Division by zero when calculating USDC amount" + .to_string(), + })?; + + let withdraw_all_data = USDVault::redeemSDAICall { + recipient: user, + amountIn: sdai_amount, + amountOutMin: U256::ZERO, + nonce: permit2_nonce, + deadline: permit2_deadline, + signature: permit2_signature + .to_vec() + .map_err(|e| RpcError::InvalidResponse { + error_message: format!("Invalid permit signature: {e}"), + })? + .into(), + } + .abi_encode(); + + let approve_data = Erc20::encode_approve(erc4626_vault_address, usdc_amount); + + let deposit_data = IERC4626::depositCall { + assets: usdc_amount, + receiver: user, + } + .abi_encode(); + + // TODO: add permit2 approve call if needed + + let entries = vec![ + MultiSendTx { + operation: SafeOperation::Call as u8, + to: usd_vault_address, + value: U256::ZERO, + data_length: U256::from(withdraw_all_data.len()), + data: withdraw_all_data.into(), + }, + MultiSendTx { + operation: SafeOperation::Call as u8, + to: usdc_address, + value: U256::ZERO, + data_length: U256::from(approve_data.len()), + data: approve_data.into(), + }, + MultiSendTx { + operation: SafeOperation::Call as u8, + to: erc4626_vault_address, + value: U256::ZERO, + data_length: U256::from(deposit_data.len()), + data: deposit_data.into(), + }, + ]; + + let bundle = MultiSend::build_bundle(&entries); + + Ok(Self { + call_data: bundle.data, + action: TransactionTypeId::USDVaultMigration, + to: crate::transactions::contracts::multisend::MULTISEND_ADDRESS, + operation: SafeOperation::DelegateCall, + metadata, + }) + } +} + +impl Is4337Encodable for UsdVault { + type MetadataArg = (); + + fn as_execute_user_op_call_data(&self) -> Bytes { + ISafe4337Module::executeUserOpCall { + to: self.to, + value: U256::ZERO, + data: self.call_data.clone().into(), + operation: self.operation 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(self.action, InstructionFlag::Default, self.metadata); + let nonce = key.encode_with_sequence(0); + + Ok(UserOperation::new_with_defaults( + wallet_address, + nonce, + call_data, + )) + } +} diff --git a/bedrock/src/transactions/contracts/wld_vault.rs b/bedrock/src/transactions/contracts/wld_vault.rs new file mode 100644 index 00000000..ed555857 --- /dev/null +++ b/bedrock/src/transactions/contracts/wld_vault.rs @@ -0,0 +1,182 @@ +//! This module introduces WLD Vault contract interface. + +use alloy::{ + primitives::{Address, Bytes, U256}, + sol, + sol_types::SolCall, +}; + +use crate::smart_account::{ + ISafe4337Module, InstructionFlag, Is4337Encodable, NonceKeyV1, SafeOperation, + TransactionTypeId, UserOperation, +}; +use crate::transactions::contracts::erc20::{Erc20, IErc20}; +use crate::transactions::contracts::multisend::{MultiSend, MultiSendTx}; +use crate::transactions::rpc::{RpcClient, RpcError}; +use crate::{ + primitives::{Network, PrimitiveError}, + transactions::contracts::erc4626::Erc4626Vault, +}; + +sol! { + #[derive(serde::Serialize)] + interface WLDVault { + function token() public view returns (address); + function withdrawAll() external; + } + + /// The ERC-4626 vault contract interface. + /// Reference: + /// Reference: + #[derive(serde::Serialize)] + interface IERC4626 { + function asset() public view returns (address assetTokenAddress); + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + } +} + +/// Represents a WLD Vault migration transaction bundle. +#[derive(Debug)] +pub struct WldVault { + /// The encoded call data for the operation. + pub call_data: Vec, + /// The action type. + action: TransactionTypeId, + /// The target address for the operation. + to: Address, + /// The Safe operation type for the operation. + operation: SafeOperation, + /// Metadata for nonce generation (protocol-specific). + metadata: [u8; 10], +} + +impl WldVault { + /// Constructs a WLD Vault migration transaction bundle. + pub async fn migrate( + rpc_client: &RpcClient, + network: Network, + wld_vault_address: Address, + erc4626_vault_address: Address, + user: Address, + metadata: [u8; 10], + ) -> Result { + let token_call_data = WLDVault::tokenCall {}.abi_encode(); + let token_address = Erc4626Vault::fetch_asset_address( + rpc_client, + network, + wld_vault_address, + token_call_data, + ) + .await?; + + let asset_call_data = IERC4626::assetCall {}.abi_encode(); + let asset_address = Erc4626Vault::fetch_asset_address( + rpc_client, + network, + erc4626_vault_address, + asset_call_data, + ) + .await?; + + if token_address != asset_address { + return Err(RpcError::InvalidResponse { + error_message: + "Asset address mismatch between WLD Vault and ERC-4626 Vault" + .to_string(), + }); + } + + let balance_call_data = + IErc20::balanceOfCall { account: user }.abi_encode(); + let balance = Erc4626Vault::fetch_balance( + rpc_client, + network, + asset_address, + balance_call_data, + "balanceOf", + ) + .await?; + + if balance.is_zero() { + return Err(RpcError::InvalidResponse { + error_message: "Cannot deposit zero amount - user has no balance of the asset token".to_string(), + }); + } + + let withdraw_all_data = WLDVault::withdrawAllCall {}.abi_encode(); + + let approve_data = Erc20::encode_approve(erc4626_vault_address, balance); + + let deposit_data = IERC4626::depositCall { + assets: balance, + receiver: user, + } + .abi_encode(); + + let entries = vec![ + MultiSendTx { + operation: SafeOperation::Call as u8, + to: wld_vault_address, + value: U256::ZERO, + data_length: U256::from(withdraw_all_data.len()), + data: withdraw_all_data.into(), + }, + MultiSendTx { + operation: SafeOperation::Call as u8, + to: asset_address, + value: U256::ZERO, + data_length: U256::from(approve_data.len()), + data: approve_data.into(), + }, + MultiSendTx { + operation: SafeOperation::Call as u8, + to: erc4626_vault_address, + value: U256::ZERO, + data_length: U256::from(deposit_data.len()), + data: deposit_data.into(), + }, + ]; + + let bundle = MultiSend::build_bundle(&entries); + + Ok(Self { + call_data: bundle.data, + action: TransactionTypeId::WLDVaultMigration, + to: crate::transactions::contracts::multisend::MULTISEND_ADDRESS, + operation: SafeOperation::DelegateCall, + metadata, + }) + } +} + +impl Is4337Encodable for WldVault { + type MetadataArg = (); + + fn as_execute_user_op_call_data(&self) -> Bytes { + ISafe4337Module::executeUserOpCall { + to: self.to, + value: U256::ZERO, + data: self.call_data.clone().into(), + operation: self.operation 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(self.action, InstructionFlag::Default, self.metadata); + let nonce = key.encode_with_sequence(0); + + Ok(UserOperation::new_with_defaults( + wallet_address, + nonce, + call_data, + )) + } +} diff --git a/bedrock/src/transactions/mod.rs b/bedrock/src/transactions/mod.rs index cd07d268..ce34f88a 100644 --- a/bedrock/src/transactions/mod.rs +++ b/bedrock/src/transactions/mod.rs @@ -1,11 +1,15 @@ use alloy::primitives::{Address, U256}; use bedrock_macros::bedrock_export; +use chrono::Utc; use rand::RngCore; use std::sync::Arc; use crate::{ primitives::{HexEncodedData, Network, ParseFromForeignBinding}, - smart_account::{Is4337Encodable, SafeSmartAccount}, + smart_account::{ + Is4337Encodable, SafeSmartAccount, UnparsedPermitTransferFrom, + UnparsedTokenPermissions, + }, transactions::{ contracts::{ erc20::{Erc20, TransferAssociation}, @@ -409,6 +413,125 @@ impl SafeSmartAccount { Ok(HexEncodedData::new(&user_op_hash.to_string())?) } + /// Constructs and executes a WLD Vault migration transaction bundle on World Chain. + pub async fn transaction_wld_vault_migration( + &self, + wld_vault_address: &str, + erc4626_vault_address: &str, + ) -> Result { + let wld_vault_address = + Address::parse_from_ffi(wld_vault_address, "wld_vault_address")?; + let erc4626_vault_address = + Address::parse_from_ffi(erc4626_vault_address, "erc4626_vault_address")?; + + // Get the RPC client and create the ERC4626 deposit transaction + let rpc_client = get_rpc_client().map_err(|e| TransactionError::Generic { + error_message: format!("Failed to get RPC client: {e}"), + })?; + let transaction = crate::transactions::contracts::wld_vault::WldVault::migrate( + rpc_client, + Network::WorldChain, + wld_vault_address, + erc4626_vault_address, + self.wallet_address, + [0u8; 10], // metadata + ) + .await + .map_err(|e| TransactionError::Generic { + error_message: format!("Failed to create WLDVault migration: {e}"), + })?; + + let provider = RpcProviderName::Any; + + let user_op_hash = transaction + .sign_and_execute(self, Network::WorldChain, None, None, provider) + .await + .map_err(|e| TransactionError::Generic { + error_message: format!("Failed to execute WLDVault migration: {e}"), + })?; + + Ok(HexEncodedData::new(&user_op_hash.to_string())?) + } + + /// Constructs and executes a USD Vault migration transaction bundle on World Chain. + pub async fn transaction_usd_vault_migration( + &self, + usd_vault_address: &str, + erc4626_vault_address: &str, + ) -> Result { + let usd_vault_address = + Address::parse_from_ffi(usd_vault_address, "usd_vault_address")?; + let erc4626_vault_address = + Address::parse_from_ffi(erc4626_vault_address, "erc4626_vault_address")?; + + // Get the RPC client and create the ERC4626 deposit transaction + let rpc_client = get_rpc_client().map_err(|e| TransactionError::Generic { + error_message: format!("Failed to get RPC client: {e}"), + })?; + + let (sdai_address, sdai_amount) = + crate::transactions::contracts::usd_vault::UsdVault::fetch_sdai_balance( + rpc_client, + Network::WorldChain, + usd_vault_address, + self.wallet_address, + ) + .await + .map_err(|e| TransactionError::Generic { + error_message: format!("Failed to fetch sDAI balance: {e}"), + })?; + + let permitted = UnparsedTokenPermissions { + token: sdai_address.to_string(), + amount: sdai_amount.to_string(), + }; + + let nonce: u64 = Utc::now().timestamp_millis().try_into().unwrap(); + + let deadline = Utc::now().timestamp() + 180; // 3 minutes from now + + let transfer = UnparsedPermitTransferFrom { + permitted, + spender: usd_vault_address.to_string(), + nonce: nonce.to_string(), + deadline: deadline.to_string(), + }; + + let signature = self + .sign_permit2_transfer(Network::WorldChain as u32, transfer) + .map_err(|e| TransactionError::Generic { + error_message: format!("Failed to sign permit2 transfer: {e}"), + })?; + + let transaction = crate::transactions::contracts::usd_vault::UsdVault::migrate( + rpc_client, + Network::WorldChain, + usd_vault_address, + erc4626_vault_address, + sdai_amount, + self.wallet_address, + signature, + U256::from(nonce), + U256::from(deadline), + [0u8; 10], // metadata + ) + .await + .map_err(|e| TransactionError::Generic { + error_message: format!("Failed to create USDVault migration: {e}"), + })?; + + let provider = RpcProviderName::Any; + + let user_op_hash = transaction + .sign_and_execute(self, Network::WorldChain, None, None, provider) + .await + .map_err(|e| TransactionError::Generic { + error_message: format!("Failed to execute USDVault migration: {e}"), + })?; + + Ok(HexEncodedData::new(&user_op_hash.to_string())?) + } + /// Gets a custom user operation receipt for a given user operation hash via the global RPC client. /// /// This is a convenience wrapper around [`RpcClient::wa_get_user_operation_receipt`]