From cbf2b4de5a68d98f37182b26f8872f4f512b356f Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Mon, 20 Jan 2025 13:12:31 +0000 Subject: [PATCH 1/2] feat: Implement ProtocolApprovalsManager I decided to make ProtocolApprovalsManager public method sync not to propagate asynceness early on onto the rest of the code. If later we decide that it is better to have this async, it should be easier to make that change than the opposite one. --- don't change below this line --- ENG-4064 Took 24 minutes --- Cargo.lock | 88 +++++++++++++++ Cargo.toml | 5 +- .../approvals/protocol_approvals_manager.rs | 103 +++++++++++++----- src/encoding/evm/swap_encoder/encoders.rs | 11 +- 4 files changed, 170 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c9ba4c..c8ba58d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -49,6 +58,7 @@ dependencies = [ "alloy-network", "alloy-provider", "alloy-rpc-client", + "alloy-rpc-types", "alloy-serde", "alloy-transport", "alloy-transport-http", @@ -332,6 +342,18 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "alloy-rpc-types" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea9bf1abdd506f985a53533f5ac01296bcd6102c5e139bbc5d40bc468d2c916" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-rpc-types-eth" version = "0.5.4" @@ -1247,6 +1269,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -2260,12 +2288,41 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.12" @@ -2326,6 +2383,36 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rstest" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version 0.4.1", +] + +[[package]] +name = "rstest_macros" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.1", + "syn 2.0.96", + "unicode-ident", +] + [[package]] name = "ruint" version = "1.12.4" @@ -3049,6 +3136,7 @@ dependencies = [ "lazy_static", "num-bigint", "num-traits", + "rstest", "serde", "serde_json", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 7f672b6..85e9b5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,14 @@ serde_json = "1.0.135" thiserror = "1.0.69" tokio = { version = "1.38.0", features = ["full"] } -alloy = { version = "0.5.4", features = ["providers"], optional = true } +alloy = { version = "0.5.4", features = ["providers", "rpc-types-eth"], optional = true } alloy-sol-types = { version = "0.8.14", optional = true } alloy-primitives = { version = "0.8.9", optional = true } tycho-core = { git = "https://github.com/propeller-heads/tycho-indexer.git", package = "tycho-core", tag = "0.46.0" } +[dev-dependencies] +rstest = "0.24.0" + [features] default = ["evm"] evm = ["alloy", "alloy-sol-types", "alloy-primitives"] diff --git a/src/encoding/evm/approvals/protocol_approvals_manager.rs b/src/encoding/evm/approvals/protocol_approvals_manager.rs index fde5442..eaa976b 100644 --- a/src/encoding/evm/approvals/protocol_approvals_manager.rs +++ b/src/encoding/evm/approvals/protocol_approvals_manager.rs @@ -1,49 +1,98 @@ use std::{env, sync::Arc}; use alloy::{ - providers::{ProviderBuilder, RootProvider}, + providers::{Provider, ProviderBuilder, RootProvider}, + rpc::types::{TransactionInput, TransactionRequest}, transports::BoxTransport, }; -use alloy_primitives::Address; +use alloy_primitives::{Address, Bytes, TxKind, U256}; +use alloy_sol_types::SolValue; use dotenv::dotenv; +use tokio::runtime::Runtime; + +use crate::encoding::{errors::EncodingError, evm::utils::encode_input}; #[allow(dead_code)] pub struct ProtocolApprovalsManager { client: Arc>, + runtime: Runtime, } impl ProtocolApprovalsManager { pub fn new() -> Self { - Self { client: get_client() } + let runtime = Runtime::new().expect("Failed to create runtime"); + let client = runtime.block_on(get_client()); + Self { client, runtime } } - pub async fn approval_needed( + pub fn approval_needed( &self, - _token: Address, - _spender_address: Address, - _router_address: Address, - ) -> bool { - todo!() - // should be something like - // let allowance = self - // .client - // .call(token, "allowance(address,address)(uint256)", (router_address, - // spender_address)) .await; - // - // allowance == U256::ZERO // If allowance is 0, approval is needed + token: Address, + owner_address: Address, + spender_address: Address, + ) -> Result { + let args = (owner_address, spender_address); + let data = encode_input("allowance(address,address)", args.abi_encode()); + let tx = TransactionRequest { + to: Some(TxKind::from(token)), + input: TransactionInput { input: Some(Bytes::from(data)), data: None }, + ..Default::default() + }; + + let output = self + .runtime + .block_on(async { self.client.call(&tx).await }); + match output { + Ok(response) => { + let allowance: U256 = U256::abi_decode(&response, true).map_err(|_| { + EncodingError::FatalError("Failed to decode response for allowance".to_string()) + })?; + + Ok(allowance.is_zero()) + } + Err(err) => { + Err(EncodingError::RecoverableError(format!("Call failed with error: {:?}", err))) + } + } } } -pub fn get_client() -> Arc> { +pub async fn get_client() -> Arc> { dotenv().ok(); let eth_rpc_url = env::var("ETH_RPC_URL").expect("Missing ETH_RPC_URL in environment"); - let runtime = tokio::runtime::Handle::try_current() - .is_err() - .then(|| tokio::runtime::Runtime::new().unwrap()) - .unwrap(); - let client = runtime.block_on(async { - ProviderBuilder::new() - .on_builtin(ð_rpc_url) - .await - .unwrap() - }); + let client = ProviderBuilder::new() + .on_builtin(ð_rpc_url) + .await + .expect("Failed to build provider"); Arc::new(client) } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use rstest::rstest; + + use super::*; + #[rstest] + #[case::approval_not_needed( + "0xba12222222228d8ba445958a75a0704d566bf2c8", + "0x2c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4", + false + )] + #[case::approval_needed( + "0x2c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4", + "0xba12222222228d8ba445958a75a0704d566bf2c8", + true + )] + fn test_approval_needed(#[case] spender: &str, #[case] owner: &str, #[case] expected: bool) { + let manager = ProtocolApprovalsManager::new(); + + let token = Address::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let spender = Address::from_str(spender).unwrap(); + let owner = Address::from_str(owner).unwrap(); + + let result = manager + .approval_needed(token, owner, spender) + .unwrap(); + assert_eq!(result, expected); + } +} diff --git a/src/encoding/evm/swap_encoder/encoders.rs b/src/encoding/evm/swap_encoder/encoders.rs index a397be3..da1b3aa 100644 --- a/src/encoding/evm/swap_encoder/encoders.rs +++ b/src/encoding/evm/swap_encoder/encoders.rs @@ -53,17 +53,10 @@ impl SwapEncoder for BalancerV2SwapEncoder { encoding_context: EncodingContext, ) -> Result, EncodingError> { let token_approvals_manager = ProtocolApprovalsManager::new(); - let runtime = tokio::runtime::Handle::try_current() - .is_err() - .then(|| tokio::runtime::Runtime::new().unwrap()) - .unwrap(); let token = bytes_to_address(&swap.token_in)?; let router_address = bytes_to_address(&encoding_context.address_for_approvals)?; - let approval_needed = runtime.block_on(async { - token_approvals_manager - .approval_needed(token, self.vault_address, router_address) - .await - }); + let approval_needed = + token_approvals_manager.approval_needed(token, router_address, self.vault_address)?; // should we return gas estimation here too?? if there is an approval needed, gas will be // higher. let args = ( From b155b273a09f1029142bfb77ff75fed41dffcbff Mon Sep 17 00:00:00 2001 From: Diana Carvalho Date: Mon, 20 Jan 2025 16:35:38 +0000 Subject: [PATCH 2/2] chore: Add more descriptive error message --- don't change below this line --- ENG-4063 Took 14 minutes --- src/encoding/evm/approvals/protocol_approvals_manager.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/encoding/evm/approvals/protocol_approvals_manager.rs b/src/encoding/evm/approvals/protocol_approvals_manager.rs index eaa976b..c74e8ce 100644 --- a/src/encoding/evm/approvals/protocol_approvals_manager.rs +++ b/src/encoding/evm/approvals/protocol_approvals_manager.rs @@ -48,9 +48,10 @@ impl ProtocolApprovalsManager { Ok(allowance.is_zero()) } - Err(err) => { - Err(EncodingError::RecoverableError(format!("Call failed with error: {:?}", err))) - } + Err(err) => Err(EncodingError::RecoverableError(format!( + "Allowance call failed with error: {:?}", + err + ))), } } }