diff --git a/crates/common/src/deposit.rs b/crates/common/src/deposit.rs index c79a65fefb..232be83015 100644 --- a/crates/common/src/deposit.rs +++ b/crates/common/src/deposit.rs @@ -22,6 +22,9 @@ pub enum DepositError { #[error(transparent)] FloatError(#[from] FloatError), + #[error("vault-id 0 is vaultless and cannot be used for deposits")] + ZeroVaultId, + #[cfg(not(target_family = "wasm"))] #[error(transparent)] WriteTransactionError(#[from] crate::write_tx::WriteTransactionError), @@ -36,9 +39,13 @@ pub struct DepositArgs { } impl TryFrom for deposit4Call { - type Error = FloatError; + type Error = DepositError; fn try_from(val: DepositArgs) -> Result { + if val.vault_id == B256::ZERO { + return Err(DepositError::ZeroVaultId); + } + Ok(deposit4Call { token: val.token, vaultId: val.vault_id, @@ -97,9 +104,9 @@ impl DepositArgs { transaction_args: TransactionArgs, transaction_status_changed: S, ) -> Result<(), DepositError> { + let deposit_call: deposit4Call = self.clone().try_into()?; let (ledger_client, _) = transaction_args.clone().try_into_ledger_client().await?; - let deposit_call: deposit4Call = self.clone().try_into()?; let tx_request = transaction_args .try_into_transaction_request(deposit_call, transaction_args.raindex_address)?; @@ -108,3 +115,40 @@ impl DepositArgs { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deposit_call_rejects_zero_vault_id() { + let args = DepositArgs { + token: Address::ZERO, + vault_id: B256::ZERO, + amount: Float::parse("1".to_string()).unwrap(), + decimals: 18, + }; + + assert!(matches!( + deposit4Call::try_from(args), + Err(DepositError::ZeroVaultId) + )); + } + + #[cfg(not(target_family = "wasm"))] + #[tokio::test] + async fn test_execute_deposit_rejects_zero_vault_id_before_transaction_setup() { + let args = DepositArgs { + token: Address::ZERO, + vault_id: B256::ZERO, + amount: Float::parse("1".to_string()).unwrap(), + decimals: 18, + }; + + assert!(matches!( + args.execute_deposit(TransactionArgs::default(), |_| {}) + .await, + Err(DepositError::ZeroVaultId) + )); + } +} diff --git a/crates/common/src/raindex_client/vaults.rs b/crates/common/src/raindex_client/vaults.rs index b897965cbf..879e3e7122 100644 --- a/crates/common/src/raindex_client/vaults.rs +++ b/crates/common/src/raindex_client/vaults.rs @@ -502,13 +502,13 @@ impl RaindexVault { ) -> Result { self.validate_amount(amount)?; - let approval = self.build_approval_calldata(amount).await?; - let deposit_args = self.get_deposit_args(amount); - let deposit = Bytes::copy_from_slice(&deposit4Call::try_from(deposit_args)?.abi_encode()); - + let deposit_call = deposit4Call::try_from(deposit_args)?; let withdraw = self.build_withdraw_calldata(amount).await?; + let approval = self.build_approval_calldata(amount).await?; + let deposit = Bytes::copy_from_slice(&deposit_call.abi_encode()); + Ok(RaindexVaultCalldatas { approval, deposit, diff --git a/crates/common/src/raindex_client/vaults_list.rs b/crates/common/src/raindex_client/vaults_list.rs index 4703ac6553..831da9b795 100644 --- a/crates/common/src/raindex_client/vaults_list.rs +++ b/crates/common/src/raindex_client/vaults_list.rs @@ -1,4 +1,7 @@ -use alloy::{primitives::Bytes, sol_types::SolCall}; +use alloy::{ + primitives::{Bytes, U256}, + sol_types::SolCall, +}; use rain_math_float::Float; use raindex_bindings::Raindex::multicallCall; use serde::{Deserialize, Serialize}; @@ -21,7 +24,10 @@ impl RaindexVaultsList { pub fn get_withdrawable_vaults(&self) -> Vec<&RaindexVault> { self.0 .iter() - .filter(|vault| vault.balance().gt(*ZERO_FLOAT).unwrap_or(false)) + .filter(|vault| { + vault.raw_vault_id() != U256::ZERO + && vault.balance().gt(*ZERO_FLOAT).unwrap_or(false) + }) .collect() } @@ -298,6 +304,28 @@ mod tests { }) } + fn get_vault3_json() -> Value { + json!({ + "id": "0x0345", + "owner": "0x0000000000000000000000000000000000000000", + "vaultId": "0", + "balance": "0x0000000000000000000000000000000000000000000000000000000000000020", + "token": { + "id": "token2", + "address": "0x12e605bc104e93b45e1ad99f9e555f659051c2bb", + "name": "Token 2", + "symbol": "TKN2", + "decimals": "18" + }, + "raindex": { + "id": "0x0000000000000000000000000000000000000000" + }, + "ordersAsOutput": [], + "ordersAsInput": [], + "balanceChanges": [] + }) + } + async fn get_vaults() -> Vec { let sg_server = MockServer::start_async().await; sg_server.mock(|when, then| { @@ -312,7 +340,7 @@ mod tests { when.path("/sg2"); then.status(200).json_body_obj(&json!({ "data": { - "vaults": [get_vault2_json()] + "vaults": [get_vault2_json(), get_vault3_json()] } })); }); @@ -337,7 +365,7 @@ mod tests { #[tokio::test] async fn test_get_vaults_not_empty() { let vaults_list = RaindexVaultsList::new(get_vaults().await); - assert_eq!(vaults_list.0.len(), 2); + assert_eq!(vaults_list.0.len(), 3); } #[tokio::test] @@ -346,6 +374,26 @@ mod tests { let withdrawable_vaults = vaults_list.get_withdrawable_vaults(); assert_eq!(withdrawable_vaults.len(), 1); assert_eq!(withdrawable_vaults[0].id().to_string(), "0x0234"); // vault2 has non-zero balance + assert_ne!(withdrawable_vaults[0].vault_id(), U256::ZERO); + } + + #[tokio::test] + async fn test_zero_vault_get_calldatas_rejects_before_allowance_read() { + let vaults_list = RaindexVaultsList::new(get_vaults().await); + let zero_vault = vaults_list + .items() + .into_iter() + .find(|vault| vault.vault_id() == U256::ZERO) + .unwrap(); + + let err = zero_vault + .get_calldatas(&Float::parse("1".to_string()).unwrap()) + .await + .unwrap_err(); + + assert!(err + .to_string() + .contains("vault-id 0 is vaultless and cannot be used for deposits")); } #[tokio::test] diff --git a/crates/common/src/raindex_order_builder/order_operations.rs b/crates/common/src/raindex_order_builder/order_operations.rs index 6d8573dc15..d5c5270938 100644 --- a/crates/common/src/raindex_order_builder/order_operations.rs +++ b/crates/common/src/raindex_order_builder/order_operations.rs @@ -375,9 +375,7 @@ impl RaindexOrderBuilder { vault_id: vault_id.into(), decimals, }; - let calldata = deposit4Call::try_from(deposit_args) - .map_err(crate::deposit::DepositError::from)? - .abi_encode(); + let calldata = deposit4Call::try_from(deposit_args)?.abi_encode(); calldatas.push(Bytes::copy_from_slice(&calldata)); } diff --git a/crates/common/src/withdraw.rs b/crates/common/src/withdraw.rs index 822bdb94f1..58c4eced6c 100644 --- a/crates/common/src/withdraw.rs +++ b/crates/common/src/withdraw.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use rain_math_float::Float; use raindex_bindings::IRaindexV6::withdraw4Call; +use std::convert::TryFrom; #[derive(Clone, Deserialize, Serialize, Debug)] pub struct WithdrawArgs { @@ -17,14 +18,22 @@ pub struct WithdrawArgs { pub target_amount: Float, } -impl From for withdraw4Call { - fn from(val: WithdrawArgs) -> Self { - withdraw4Call { +impl TryFrom for withdraw4Call { + type Error = WritableTransactionExecuteError; + + fn try_from(val: WithdrawArgs) -> Result { + if val.vault_id == B256::ZERO { + return Err(WritableTransactionExecuteError::InvalidArgs( + "vault-id 0 is vaultless and cannot be used for withdrawals".to_string(), + )); + } + + Ok(withdraw4Call { token: val.token, vaultId: val.vault_id, targetAmount: val.target_amount.get_inner(), tasks: vec![], - } + }) } } @@ -35,9 +44,9 @@ impl WithdrawArgs { transaction_args: TransactionArgs, transaction_status_changed: S, ) -> Result<(), WritableTransactionExecuteError> { + let withdraw_call: withdraw4Call = self.clone().try_into()?; let (ledger_client, _) = transaction_args.clone().try_into_ledger_client().await?; - let withdraw_call: withdraw4Call = self.clone().into(); let tx_request = transaction_args .try_into_transaction_request(withdraw_call, transaction_args.raindex_address)?; @@ -47,7 +56,46 @@ impl WithdrawArgs { } pub async fn get_withdraw_calldata(&self) -> Result, WritableTransactionExecuteError> { - let withdraw_call: withdraw4Call = self.clone().into(); + let withdraw_call: withdraw4Call = self.clone().try_into()?; Ok(withdraw_call.abi_encode()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_withdraw_call_rejects_zero_vault_id() { + let args = WithdrawArgs { + token: Address::ZERO, + vault_id: B256::ZERO, + target_amount: Float::parse("1".to_string()).unwrap(), + }; + + let err = withdraw4Call::try_from(args).unwrap_err(); + assert_eq!( + err.to_string(), + "Invalid input args: vault-id 0 is vaultless and cannot be used for withdrawals" + ); + } + + #[cfg(not(target_family = "wasm"))] + #[tokio::test] + async fn test_execute_rejects_zero_vault_id_before_transaction_setup() { + let args = WithdrawArgs { + token: Address::ZERO, + vault_id: B256::ZERO, + target_amount: Float::parse("1".to_string()).unwrap(), + }; + + let err = args + .execute(TransactionArgs::default(), |_| {}) + .await + .unwrap_err(); + assert_eq!( + err.to_string(), + "Invalid input args: vault-id 0 is vaultless and cannot be used for withdrawals" + ); + } +}